about summary refs log tree commit diff
path: root/src/tools/rust-analyzer/editors/code
diff options
context:
space:
mode:
authorLaurențiu Nicola <lnicola@dend.ro>2022-10-26 17:40:41 +0300
committerLaurențiu Nicola <lnicola@dend.ro>2022-10-26 17:40:41 +0300
commit22a6bc4da0ce15606e3a7fb01c5cc6ff175f79af (patch)
tree9832a32abb629ff63b4842c4ea706efd8740d750 /src/tools/rust-analyzer/editors/code
parent43dd3d514b6b11c5195de2fd8e665828801d0972 (diff)
parent43fb9563b2943d6abc5f3552195f3e27ac618966 (diff)
downloadrust-22a6bc4da0ce15606e3a7fb01c5cc6ff175f79af.tar.gz
rust-22a6bc4da0ce15606e3a7fb01c5cc6ff175f79af.zip
:arrow_up: rust-analyzer
Diffstat (limited to 'src/tools/rust-analyzer/editors/code')
-rw-r--r--src/tools/rust-analyzer/editors/code/package-lock.json64
-rw-r--r--src/tools/rust-analyzer/editors/code/package.json65
-rw-r--r--src/tools/rust-analyzer/editors/code/src/ast_inspector.ts8
-rw-r--r--src/tools/rust-analyzer/editors/code/src/bootstrap.ts148
-rw-r--r--src/tools/rust-analyzer/editors/code/src/client.ts65
-rw-r--r--src/tools/rust-analyzer/editors/code/src/commands.ts190
-rw-r--r--src/tools/rust-analyzer/editors/code/src/config.ts191
-rw-r--r--src/tools/rust-analyzer/editors/code/src/ctx.ts224
-rw-r--r--src/tools/rust-analyzer/editors/code/src/main.ts475
-rw-r--r--src/tools/rust-analyzer/editors/code/src/run.ts4
10 files changed, 805 insertions, 629 deletions
diff --git a/src/tools/rust-analyzer/editors/code/package-lock.json b/src/tools/rust-analyzer/editors/code/package-lock.json
index 3ff4b6897a1..a72865d4fe4 100644
--- a/src/tools/rust-analyzer/editors/code/package-lock.json
+++ b/src/tools/rust-analyzer/editors/code/package-lock.json
@@ -11,7 +11,7 @@
             "dependencies": {
                 "d3": "^7.6.1",
                 "d3-graphviz": "^4.1.1",
-                "vscode-languageclient": "^8.0.0-next.14"
+                "vscode-languageclient": "^8.0.2"
             },
             "devDependencies": {
                 "@types/node": "~16.11.7",
@@ -3791,39 +3791,39 @@
             }
         },
         "node_modules/vscode-jsonrpc": {
-            "version": "8.0.0-next.7",
-            "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.0-next.7.tgz",
-            "integrity": "sha512-JX/F31LEsims0dAlOTKFE4E+AJMiJvdRSRViifFJSqSN7EzeYyWlfuDchF7g91oRNPZOIWfibTkDf3/UMsQGzQ==",
+            "version": "8.0.2",
+            "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz",
+            "integrity": "sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ==",
             "engines": {
                 "node": ">=14.0.0"
             }
         },
         "node_modules/vscode-languageclient": {
-            "version": "8.0.0-next.14",
-            "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.0-next.14.tgz",
-            "integrity": "sha512-NqjkOuDTMu8uo+PhoMsV72VO9Gd3wBi/ZpOrkRUOrWKQo7yUdiIw183g8wjH8BImgbK9ZP51HM7TI0ZhCnI1Mw==",
+            "version": "8.0.2",
+            "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2.tgz",
+            "integrity": "sha512-lHlthJtphG9gibGb/y72CKqQUxwPsMXijJVpHEC2bvbFqxmkj9LwQ3aGU9dwjBLqsX1S4KjShYppLvg1UJDF/Q==",
             "dependencies": {
                 "minimatch": "^3.0.4",
                 "semver": "^7.3.5",
-                "vscode-languageserver-protocol": "3.17.0-next.16"
+                "vscode-languageserver-protocol": "3.17.2"
             },
             "engines": {
-                "vscode": "^1.66.0"
+                "vscode": "^1.67.0"
             }
         },
         "node_modules/vscode-languageserver-protocol": {
-            "version": "3.17.0-next.16",
-            "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.0-next.16.tgz",
-            "integrity": "sha512-tx4DnXw9u3N7vw+bx6n2NKp6FoxoNwiP/biH83AS30I2AnTGyLd7afSeH6Oewn2E8jvB7K15bs12sMppkKOVeQ==",
+            "version": "3.17.2",
+            "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2.tgz",
+            "integrity": "sha512-8kYisQ3z/SQ2kyjlNeQxbkkTNmVFoQCqkmGrzLH6A9ecPlgTbp3wDTnUNqaUxYr4vlAcloxx8zwy7G5WdguYNg==",
             "dependencies": {
-                "vscode-jsonrpc": "8.0.0-next.7",
-                "vscode-languageserver-types": "3.17.0-next.9"
+                "vscode-jsonrpc": "8.0.2",
+                "vscode-languageserver-types": "3.17.2"
             }
         },
         "node_modules/vscode-languageserver-types": {
-            "version": "3.17.0-next.9",
-            "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.0-next.9.tgz",
-            "integrity": "sha512-9/PeDNPYduaoXRUzYpqmu4ZV9L01HGo0wH9FUt+sSHR7IXwA7xoXBfNUlv8gB9H0D2WwEmMomSy1NmhjKQyn3A=="
+            "version": "3.17.2",
+            "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz",
+            "integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA=="
         },
         "node_modules/which": {
             "version": "2.0.2",
@@ -6634,33 +6634,33 @@
             }
         },
         "vscode-jsonrpc": {
-            "version": "8.0.0-next.7",
-            "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.0-next.7.tgz",
-            "integrity": "sha512-JX/F31LEsims0dAlOTKFE4E+AJMiJvdRSRViifFJSqSN7EzeYyWlfuDchF7g91oRNPZOIWfibTkDf3/UMsQGzQ=="
+            "version": "8.0.2",
+            "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.0.2.tgz",
+            "integrity": "sha512-RY7HwI/ydoC1Wwg4gJ3y6LpU9FJRZAUnTYMXthqhFXXu77ErDd/xkREpGuk4MyYkk4a+XDWAMqe0S3KkelYQEQ=="
         },
         "vscode-languageclient": {
-            "version": "8.0.0-next.14",
-            "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.0-next.14.tgz",
-            "integrity": "sha512-NqjkOuDTMu8uo+PhoMsV72VO9Gd3wBi/ZpOrkRUOrWKQo7yUdiIw183g8wjH8BImgbK9ZP51HM7TI0ZhCnI1Mw==",
+            "version": "8.0.2",
+            "resolved": "https://registry.npmjs.org/vscode-languageclient/-/vscode-languageclient-8.0.2.tgz",
+            "integrity": "sha512-lHlthJtphG9gibGb/y72CKqQUxwPsMXijJVpHEC2bvbFqxmkj9LwQ3aGU9dwjBLqsX1S4KjShYppLvg1UJDF/Q==",
             "requires": {
                 "minimatch": "^3.0.4",
                 "semver": "^7.3.5",
-                "vscode-languageserver-protocol": "3.17.0-next.16"
+                "vscode-languageserver-protocol": "3.17.2"
             }
         },
         "vscode-languageserver-protocol": {
-            "version": "3.17.0-next.16",
-            "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.0-next.16.tgz",
-            "integrity": "sha512-tx4DnXw9u3N7vw+bx6n2NKp6FoxoNwiP/biH83AS30I2AnTGyLd7afSeH6Oewn2E8jvB7K15bs12sMppkKOVeQ==",
+            "version": "3.17.2",
+            "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.2.tgz",
+            "integrity": "sha512-8kYisQ3z/SQ2kyjlNeQxbkkTNmVFoQCqkmGrzLH6A9ecPlgTbp3wDTnUNqaUxYr4vlAcloxx8zwy7G5WdguYNg==",
             "requires": {
-                "vscode-jsonrpc": "8.0.0-next.7",
-                "vscode-languageserver-types": "3.17.0-next.9"
+                "vscode-jsonrpc": "8.0.2",
+                "vscode-languageserver-types": "3.17.2"
             }
         },
         "vscode-languageserver-types": {
-            "version": "3.17.0-next.9",
-            "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.0-next.9.tgz",
-            "integrity": "sha512-9/PeDNPYduaoXRUzYpqmu4ZV9L01HGo0wH9FUt+sSHR7IXwA7xoXBfNUlv8gB9H0D2WwEmMomSy1NmhjKQyn3A=="
+            "version": "3.17.2",
+            "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.2.tgz",
+            "integrity": "sha512-zHhCWatviizPIq9B7Vh9uvrH6x3sK8itC84HkamnBWoDFJtzBf7SWlpLCZUit72b3os45h6RWQNC9xHRDF8dRA=="
         },
         "which": {
             "version": "2.0.2",
diff --git a/src/tools/rust-analyzer/editors/code/package.json b/src/tools/rust-analyzer/editors/code/package.json
index f1dd3aa79ff..6771cad28a7 100644
--- a/src/tools/rust-analyzer/editors/code/package.json
+++ b/src/tools/rust-analyzer/editors/code/package.json
@@ -37,7 +37,7 @@
     "dependencies": {
         "d3": "^7.6.1",
         "d3-graphviz": "^4.1.1",
-        "vscode-languageclient": "^8.0.0-next.14"
+        "vscode-languageclient": "^8.0.2"
     },
     "devDependencies": {
         "@types/node": "~16.11.7",
@@ -60,6 +60,7 @@
         "onCommand:rust-analyzer.analyzerStatus",
         "onCommand:rust-analyzer.memoryUsage",
         "onCommand:rust-analyzer.reloadWorkspace",
+        "onCommand:rust-analyzer.startServer",
         "workspaceContains:*/Cargo.toml",
         "workspaceContains:*/rust-project.json"
     ],
@@ -192,6 +193,16 @@
                 "category": "rust-analyzer"
             },
             {
+                "command": "rust-analyzer.startServer",
+                "title": "Start server",
+                "category": "rust-analyzer"
+            },
+            {
+                "command": "rust-analyzer.stopServer",
+                "title": "Stop server",
+                "category": "rust-analyzer"
+            },
+            {
                 "command": "rust-analyzer.onEnter",
                 "title": "Enhanced enter key",
                 "category": "rust-analyzer"
@@ -421,6 +432,32 @@
                     "default": true,
                     "type": "boolean"
                 },
+                "rust-analyzer.cargo.buildScripts.invocationLocation": {
+                    "markdownDescription": "Specifies the working directory for running build scripts.\n- \"workspace\": run build scripts for a workspace in the workspace's root directory.\n    This is incompatible with `#rust-analyzer.cargo.buildScripts.invocationStrategy#` set to `once`.\n- \"root\": run build scripts in the project's root directory.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.",
+                    "default": "workspace",
+                    "type": "string",
+                    "enum": [
+                        "workspace",
+                        "root"
+                    ],
+                    "enumDescriptions": [
+                        "The command will be executed in the corresponding workspace root.",
+                        "The command will be executed in the project root."
+                    ]
+                },
+                "rust-analyzer.cargo.buildScripts.invocationStrategy": {
+                    "markdownDescription": "Specifies the invocation strategy to use when running the build scripts command.\nIf `per_workspace` is set, the command will be executed for each workspace.\nIf `once` is set, the command will be executed once.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.",
+                    "default": "per_workspace",
+                    "type": "string",
+                    "enum": [
+                        "per_workspace",
+                        "once"
+                    ],
+                    "enumDescriptions": [
+                        "The command will be executed for each workspace.",
+                        "The command will be executed once."
+                    ]
+                },
                 "rust-analyzer.cargo.buildScripts.overrideCommand": {
                     "markdownDescription": "Override the command rust-analyzer uses to run build scripts and\nbuild procedural macros. The command is required to output json\nand should therefore include `--message-format=json` or a similar\noption.\n\nBy default, a cargo invocation will be constructed for the configured\ntargets and features, with the following base command line:\n\n```bash\ncargo check --quiet --workspace --message-format=json --all-targets\n```\n.",
                     "default": null,
@@ -546,6 +583,32 @@
                         }
                     ]
                 },
+                "rust-analyzer.checkOnSave.invocationLocation": {
+                    "markdownDescription": "Specifies the working directory for running checks.\n- \"workspace\": run checks for workspaces in the corresponding workspaces' root directories.\n    This falls back to \"root\" if `#rust-analyzer.cargo.checkOnSave.invocationStrategy#` is set to `once`.\n- \"root\": run checks in the project's root directory.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.",
+                    "default": "workspace",
+                    "type": "string",
+                    "enum": [
+                        "workspace",
+                        "root"
+                    ],
+                    "enumDescriptions": [
+                        "The command will be executed in the corresponding workspace root.",
+                        "The command will be executed in the project root."
+                    ]
+                },
+                "rust-analyzer.checkOnSave.invocationStrategy": {
+                    "markdownDescription": "Specifies the invocation strategy to use when running the checkOnSave command.\nIf `per_workspace` is set, the command will be executed for each workspace.\nIf `once` is set, the command will be executed once.\nThis config only has an effect when `#rust-analyzer.cargo.buildScripts.overrideCommand#`\nis set.",
+                    "default": "per_workspace",
+                    "type": "string",
+                    "enum": [
+                        "per_workspace",
+                        "once"
+                    ],
+                    "enumDescriptions": [
+                        "The command will be executed for each workspace.",
+                        "The command will be executed once."
+                    ]
+                },
                 "rust-analyzer.checkOnSave.noDefaultFeatures": {
                     "markdownDescription": "Whether to pass `--no-default-features` to Cargo. Defaults to\n`#rust-analyzer.cargo.noDefaultFeatures#`.",
                     "default": null,
diff --git a/src/tools/rust-analyzer/editors/code/src/ast_inspector.ts b/src/tools/rust-analyzer/editors/code/src/ast_inspector.ts
index e57fb20e2cf..176040120f4 100644
--- a/src/tools/rust-analyzer/editors/code/src/ast_inspector.ts
+++ b/src/tools/rust-analyzer/editors/code/src/ast_inspector.ts
@@ -35,8 +35,10 @@ export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProv
     });
 
     constructor(ctx: Ctx) {
-        ctx.pushCleanup(vscode.languages.registerHoverProvider({ scheme: "rust-analyzer" }, this));
-        ctx.pushCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
+        ctx.pushExtCleanup(
+            vscode.languages.registerHoverProvider({ scheme: "rust-analyzer" }, this)
+        );
+        ctx.pushExtCleanup(vscode.languages.registerDefinitionProvider({ language: "rust" }, this));
         vscode.workspace.onDidCloseTextDocument(
             this.onDidCloseTextDocument,
             this,
@@ -52,8 +54,6 @@ export class AstInspector implements vscode.HoverProvider, vscode.DefinitionProv
             this,
             ctx.subscriptions
         );
-
-        ctx.pushCleanup(this);
     }
     dispose() {
         this.setRustEditor(undefined);
diff --git a/src/tools/rust-analyzer/editors/code/src/bootstrap.ts b/src/tools/rust-analyzer/editors/code/src/bootstrap.ts
new file mode 100644
index 00000000000..374c3b8144c
--- /dev/null
+++ b/src/tools/rust-analyzer/editors/code/src/bootstrap.ts
@@ -0,0 +1,148 @@
+import * as vscode from "vscode";
+import * as os from "os";
+import { Config } from "./config";
+import { log, isValidExecutable } from "./util";
+import { PersistentState } from "./persistent_state";
+import { exec } from "child_process";
+
+export async function bootstrap(
+    context: vscode.ExtensionContext,
+    config: Config,
+    state: PersistentState
+): Promise<string> {
+    const path = await getServer(context, config, state);
+    if (!path) {
+        throw new Error(
+            "Rust Analyzer Language Server is not available. " +
+                "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
+        );
+    }
+
+    log.info("Using server binary at", path);
+
+    if (!isValidExecutable(path)) {
+        if (config.serverPath) {
+            throw new Error(`Failed to execute ${path} --version. \`config.server.path\` or \`config.serverPath\` has been set explicitly.\
+            Consider removing this config or making a valid server binary available at that path.`);
+        } else {
+            throw new Error(`Failed to execute ${path} --version`);
+        }
+    }
+
+    return path;
+}
+
+async function patchelf(dest: vscode.Uri): Promise<void> {
+    await vscode.window.withProgress(
+        {
+            location: vscode.ProgressLocation.Notification,
+            title: "Patching rust-analyzer for NixOS",
+        },
+        async (progress, _) => {
+            const expression = `
+            {srcStr, pkgs ? import <nixpkgs> {}}:
+                pkgs.stdenv.mkDerivation {
+                    name = "rust-analyzer";
+                    src = /. + srcStr;
+                    phases = [ "installPhase" "fixupPhase" ];
+                    installPhase = "cp $src $out";
+                    fixupPhase = ''
+                    chmod 755 $out
+                    patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
+                    '';
+                }
+            `;
+            const origFile = vscode.Uri.file(dest.fsPath + "-orig");
+            await vscode.workspace.fs.rename(dest, origFile, { overwrite: true });
+            try {
+                progress.report({ message: "Patching executable", increment: 20 });
+                await new Promise((resolve, reject) => {
+                    const handle = exec(
+                        `nix-build -E - --argstr srcStr '${origFile.fsPath}' -o '${dest.fsPath}'`,
+                        (err, stdout, stderr) => {
+                            if (err != null) {
+                                reject(Error(stderr));
+                            } else {
+                                resolve(stdout);
+                            }
+                        }
+                    );
+                    handle.stdin?.write(expression);
+                    handle.stdin?.end();
+                });
+            } finally {
+                await vscode.workspace.fs.delete(origFile);
+            }
+        }
+    );
+}
+
+async function getServer(
+    context: vscode.ExtensionContext,
+    config: Config,
+    state: PersistentState
+): Promise<string | undefined> {
+    const explicitPath = serverPath(config);
+    if (explicitPath) {
+        if (explicitPath.startsWith("~/")) {
+            return os.homedir() + explicitPath.slice("~".length);
+        }
+        return explicitPath;
+    }
+    if (config.package.releaseTag === null) return "rust-analyzer";
+
+    const ext = process.platform === "win32" ? ".exe" : "";
+    const bundled = vscode.Uri.joinPath(context.extensionUri, "server", `rust-analyzer${ext}`);
+    const bundledExists = await vscode.workspace.fs.stat(bundled).then(
+        () => true,
+        () => false
+    );
+    if (bundledExists) {
+        let server = bundled;
+        if (await isNixOs()) {
+            await vscode.workspace.fs.createDirectory(config.globalStorageUri).then();
+            const dest = vscode.Uri.joinPath(config.globalStorageUri, `rust-analyzer${ext}`);
+            let exists = await vscode.workspace.fs.stat(dest).then(
+                () => true,
+                () => false
+            );
+            if (exists && config.package.version !== state.serverVersion) {
+                await vscode.workspace.fs.delete(dest);
+                exists = false;
+            }
+            if (!exists) {
+                await vscode.workspace.fs.copy(bundled, dest);
+                await patchelf(dest);
+            }
+            server = dest;
+        }
+        await state.updateServerVersion(config.package.version);
+        return server.fsPath;
+    }
+
+    await state.updateServerVersion(undefined);
+    await vscode.window.showErrorMessage(
+        "Unfortunately we don't ship binaries for your platform yet. " +
+            "You need to manually clone the rust-analyzer repository and " +
+            "run `cargo xtask install --server` to build the language server from sources. " +
+            "If you feel that your platform should be supported, please create an issue " +
+            "about that [here](https://github.com/rust-lang/rust-analyzer/issues) and we " +
+            "will consider it."
+    );
+    return undefined;
+}
+function serverPath(config: Config): string | null {
+    return process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
+}
+
+async function isNixOs(): Promise<boolean> {
+    try {
+        const contents = (
+            await vscode.workspace.fs.readFile(vscode.Uri.file("/etc/os-release"))
+        ).toString();
+        const idString = contents.split("\n").find((a) => a.startsWith("ID=")) || "ID=linux";
+        return idString.indexOf("nixos") !== -1;
+    } catch {
+        return false;
+    }
+}
diff --git a/src/tools/rust-analyzer/editors/code/src/client.ts b/src/tools/rust-analyzer/editors/code/src/client.ts
index 05d4d08f70b..fb667619c86 100644
--- a/src/tools/rust-analyzer/editors/code/src/client.ts
+++ b/src/tools/rust-analyzer/editors/code/src/client.ts
@@ -4,9 +4,7 @@ import * as ra from "../src/lsp_ext";
 import * as Is from "vscode-languageclient/lib/common/utils/is";
 import { assert } from "./util";
 import { WorkspaceEdit } from "vscode";
-import { Workspace } from "./ctx";
-import { substituteVariablesInEnv } from "./config";
-import { outputChannel, traceOutputChannel } from "./main";
+import { substituteVSCodeVariables } from "./config";
 import { randomUUID } from "crypto";
 
 export interface Env {
@@ -65,40 +63,42 @@ function renderHoverActions(actions: ra.CommandLinkGroup[]): vscode.MarkdownStri
 }
 
 export async function createClient(
-    serverPath: string,
-    workspace: Workspace,
-    extraEnv: Env
+    traceOutputChannel: vscode.OutputChannel,
+    outputChannel: vscode.OutputChannel,
+    initializationOptions: vscode.WorkspaceConfiguration,
+    serverOptions: lc.ServerOptions
 ): Promise<lc.LanguageClient> {
-    // '.' Is the fallback if no folder is open
-    // TODO?: Workspace folders support Uri's (eg: file://test.txt).
-    // It might be a good idea to test if the uri points to a file.
-
-    const newEnv = substituteVariablesInEnv(Object.assign({}, process.env, extraEnv));
-    const run: lc.Executable = {
-        command: serverPath,
-        options: { env: newEnv },
-    };
-    const serverOptions: lc.ServerOptions = {
-        run,
-        debug: run,
-    };
-
-    let initializationOptions = vscode.workspace.getConfiguration("rust-analyzer");
-
-    if (workspace.kind === "Detached Files") {
-        initializationOptions = {
-            detachedFiles: workspace.files.map((file) => file.uri.fsPath),
-            ...initializationOptions,
-        };
-    }
-
     const clientOptions: lc.LanguageClientOptions = {
         documentSelector: [{ scheme: "file", language: "rust" }],
         initializationOptions,
         diagnosticCollectionName: "rustc",
-        traceOutputChannel: traceOutputChannel(),
-        outputChannel: outputChannel(),
+        traceOutputChannel,
+        outputChannel,
         middleware: {
+            workspace: {
+                // HACK: This is a workaround, when the client has been disposed, VSCode
+                // continues to emit events to the client and the default one for this event
+                // attempt to restart the client for no reason
+                async didChangeWatchedFile(event, next) {
+                    if (client.isRunning()) {
+                        await next(event);
+                    }
+                },
+                async configuration(
+                    params: lc.ConfigurationParams,
+                    token: vscode.CancellationToken,
+                    next: lc.ConfigurationRequest.HandlerSignature
+                ) {
+                    const resp = await next(params, token);
+                    if (resp && Array.isArray(resp)) {
+                        return resp.map((val) => {
+                            return substituteVSCodeVariables(val);
+                        });
+                    } else {
+                        return resp;
+                    }
+                },
+            },
             async provideHover(
                 document: vscode.TextDocument,
                 position: vscode.Position,
@@ -255,6 +255,9 @@ export async function createClient(
 }
 
 class ExperimentalFeatures implements lc.StaticFeature {
+    getState(): lc.FeatureState {
+        return { kind: "static" };
+    }
     fillClientCapabilities(capabilities: lc.ClientCapabilities): void {
         const caps: any = capabilities.experimental ?? {};
         caps.snippetTextEdit = true;
diff --git a/src/tools/rust-analyzer/editors/code/src/commands.ts b/src/tools/rust-analyzer/editors/code/src/commands.ts
index b9ad525e361..12ceb4f2df8 100644
--- a/src/tools/rust-analyzer/editors/code/src/commands.ts
+++ b/src/tools/rust-analyzer/editors/code/src/commands.ts
@@ -21,16 +21,16 @@ export function analyzerStatus(ctx: Ctx): Cmd {
         readonly uri = vscode.Uri.parse("rust-analyzer-status://status");
         readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
 
-        provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> {
+        async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
             if (!vscode.window.activeTextEditor) return "";
+            const client = await ctx.getClient();
 
             const params: ra.AnalyzerStatusParams = {};
             const doc = ctx.activeRustEditor?.document;
             if (doc != null) {
-                params.textDocument =
-                    ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(doc);
+                params.textDocument = client.code2ProtocolConverter.asTextDocumentIdentifier(doc);
             }
-            return ctx.client.sendRequest(ra.analyzerStatus, params);
+            return await client.sendRequest(ra.analyzerStatus, params);
         }
 
         get onDidChange(): vscode.Event<vscode.Uri> {
@@ -38,7 +38,7 @@ export function analyzerStatus(ctx: Ctx): Cmd {
         }
     })();
 
-    ctx.pushCleanup(
+    ctx.pushExtCleanup(
         vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-status", tdcp)
     );
 
@@ -60,9 +60,14 @@ export function memoryUsage(ctx: Ctx): Cmd {
         provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> {
             if (!vscode.window.activeTextEditor) return "";
 
-            return ctx.client.sendRequest(ra.memoryUsage).then((mem: any) => {
-                return "Per-query memory usage:\n" + mem + "\n(note: database has been cleared)";
-            });
+            return ctx
+                .getClient()
+                .then((it) => it.sendRequest(ra.memoryUsage))
+                .then((mem: any) => {
+                    return (
+                        "Per-query memory usage:\n" + mem + "\n(note: database has been cleared)"
+                    );
+                });
         }
 
         get onDidChange(): vscode.Event<vscode.Uri> {
@@ -70,7 +75,7 @@ export function memoryUsage(ctx: Ctx): Cmd {
         }
     })();
 
-    ctx.pushCleanup(
+    ctx.pushExtCleanup(
         vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-memory", tdcp)
     );
 
@@ -83,23 +88,19 @@ export function memoryUsage(ctx: Ctx): Cmd {
 
 export function shuffleCrateGraph(ctx: Ctx): Cmd {
     return async () => {
-        const client = ctx.client;
-        if (!client) return;
-
-        await client.sendRequest(ra.shuffleCrateGraph);
+        return ctx.getClient().then((it) => it.sendRequest(ra.shuffleCrateGraph));
     };
 }
 
 export function matchingBrace(ctx: Ctx): Cmd {
     return async () => {
         const editor = ctx.activeRustEditor;
-        const client = ctx.client;
-        if (!editor || !client) return;
+        if (!editor) return;
+
+        const client = await ctx.getClient();
 
         const response = await client.sendRequest(ra.matchingBrace, {
-            textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
-                editor.document
-            ),
+            textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
             positions: editor.selections.map((s) =>
                 client.code2ProtocolConverter.asPosition(s.active)
             ),
@@ -116,14 +117,13 @@ export function matchingBrace(ctx: Ctx): Cmd {
 export function joinLines(ctx: Ctx): Cmd {
     return async () => {
         const editor = ctx.activeRustEditor;
-        const client = ctx.client;
-        if (!editor || !client) return;
+        if (!editor) return;
+
+        const client = await ctx.getClient();
 
         const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
             ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
-            textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
-                editor.document
-            ),
+            textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
         });
         const textEdits = await client.protocol2CodeConverter.asTextEdits(items);
         await editor.edit((builder) => {
@@ -145,14 +145,12 @@ export function moveItemDown(ctx: Ctx): Cmd {
 export function moveItem(ctx: Ctx, direction: ra.Direction): Cmd {
     return async () => {
         const editor = ctx.activeRustEditor;
-        const client = ctx.client;
-        if (!editor || !client) return;
+        if (!editor) return;
+        const client = await ctx.getClient();
 
         const lcEdits = await client.sendRequest(ra.moveItem, {
             range: client.code2ProtocolConverter.asRange(editor.selection),
-            textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
-                editor.document
-            ),
+            textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
             direction,
         });
 
@@ -166,13 +164,13 @@ export function moveItem(ctx: Ctx, direction: ra.Direction): Cmd {
 export function onEnter(ctx: Ctx): Cmd {
     async function handleKeypress() {
         const editor = ctx.activeRustEditor;
-        const client = ctx.client;
 
-        if (!editor || !client) return false;
+        if (!editor) return false;
 
+        const client = await ctx.getClient();
         const lcEdits = await client
             .sendRequest(ra.onEnter, {
-                textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
+                textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
                     editor.document
                 ),
                 position: client.code2ProtocolConverter.asPosition(editor.selection.active),
@@ -198,14 +196,13 @@ export function onEnter(ctx: Ctx): Cmd {
 export function parentModule(ctx: Ctx): Cmd {
     return async () => {
         const editor = vscode.window.activeTextEditor;
-        const client = ctx.client;
-        if (!editor || !client) return;
+        if (!editor) return;
         if (!(isRustDocument(editor.document) || isCargoTomlDocument(editor.document))) return;
 
+        const client = await ctx.getClient();
+
         const locations = await client.sendRequest(ra.parentModule, {
-            textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
-                editor.document
-            ),
+            textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
             position: client.code2ProtocolConverter.asPosition(editor.selection.active),
         });
         if (!locations) return;
@@ -236,13 +233,11 @@ export function parentModule(ctx: Ctx): Cmd {
 export function openCargoToml(ctx: Ctx): Cmd {
     return async () => {
         const editor = ctx.activeRustEditor;
-        const client = ctx.client;
-        if (!editor || !client) return;
+        if (!editor) return;
 
+        const client = await ctx.getClient();
         const response = await client.sendRequest(ra.openCargoToml, {
-            textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
-                editor.document
-            ),
+            textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
         });
         if (!response) return;
 
@@ -259,12 +254,13 @@ export function openCargoToml(ctx: Ctx): Cmd {
 export function ssr(ctx: Ctx): Cmd {
     return async () => {
         const editor = vscode.window.activeTextEditor;
-        const client = ctx.client;
-        if (!editor || !client) return;
+        if (!editor) return;
+
+        const client = await ctx.getClient();
 
         const position = editor.selection.active;
         const selections = editor.selections;
-        const textDocument = ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
+        const textDocument = client.code2ProtocolConverter.asTextDocumentIdentifier(
             editor.document
         );
 
@@ -314,6 +310,10 @@ export function ssr(ctx: Ctx): Cmd {
 
 export function serverVersion(ctx: Ctx): Cmd {
     return async () => {
+        if (!ctx.serverPath) {
+            void vscode.window.showWarningMessage(`rust-analyzer server is not running`);
+            return;
+        }
         const { stdout } = spawnSync(ctx.serverPath, ["--version"], { encoding: "utf8" });
         const versionString = stdout.slice(`rust-analyzer `.length).trim();
 
@@ -354,21 +354,22 @@ export function syntaxTree(ctx: Ctx): Cmd {
             }
         }
 
-        provideTextDocumentContent(
+        async provideTextDocumentContent(
             uri: vscode.Uri,
             ct: vscode.CancellationToken
-        ): vscode.ProviderResult<string> {
+        ): Promise<string> {
             const rustEditor = ctx.activeRustEditor;
             if (!rustEditor) return "";
+            const client = await ctx.getClient();
 
             // When the range based query is enabled we take the range of the selection
             const range =
                 uri.query === "range=true" && !rustEditor.selection.isEmpty
-                    ? ctx.client.code2ProtocolConverter.asRange(rustEditor.selection)
+                    ? client.code2ProtocolConverter.asRange(rustEditor.selection)
                     : null;
 
             const params = { textDocument: { uri: rustEditor.document.uri.toString() }, range };
-            return ctx.client.sendRequest(ra.syntaxTree, params, ct);
+            return client.sendRequest(ra.syntaxTree, params, ct);
         }
 
         get onDidChange(): vscode.Event<vscode.Uri> {
@@ -376,12 +377,11 @@ export function syntaxTree(ctx: Ctx): Cmd {
         }
     })();
 
-    void new AstInspector(ctx);
-
-    ctx.pushCleanup(
+    ctx.pushExtCleanup(new AstInspector(ctx));
+    ctx.pushExtCleanup(
         vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-syntax-tree", tdcp)
     );
-    ctx.pushCleanup(
+    ctx.pushExtCleanup(
         vscode.languages.setLanguageConfiguration("ra_syntax_tree", {
             brackets: [["[", ")"]],
         })
@@ -437,14 +437,14 @@ export function viewHir(ctx: Ctx): Cmd {
             }
         }
 
-        provideTextDocumentContent(
+        async provideTextDocumentContent(
             _uri: vscode.Uri,
             ct: vscode.CancellationToken
-        ): vscode.ProviderResult<string> {
+        ): Promise<string> {
             const rustEditor = ctx.activeRustEditor;
-            const client = ctx.client;
-            if (!rustEditor || !client) return "";
+            if (!rustEditor) return "";
 
+            const client = await ctx.getClient();
             const params = {
                 textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
                     rustEditor.document
@@ -459,7 +459,7 @@ export function viewHir(ctx: Ctx): Cmd {
         }
     })();
 
-    ctx.pushCleanup(
+    ctx.pushExtCleanup(
         vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-hir", tdcp)
     );
 
@@ -503,13 +503,13 @@ export function viewFileText(ctx: Ctx): Cmd {
             }
         }
 
-        provideTextDocumentContent(
+        async provideTextDocumentContent(
             _uri: vscode.Uri,
             ct: vscode.CancellationToken
-        ): vscode.ProviderResult<string> {
+        ): Promise<string> {
             const rustEditor = ctx.activeRustEditor;
-            const client = ctx.client;
-            if (!rustEditor || !client) return "";
+            if (!rustEditor) return "";
+            const client = await ctx.getClient();
 
             const params = client.code2ProtocolConverter.asTextDocumentIdentifier(
                 rustEditor.document
@@ -522,7 +522,7 @@ export function viewFileText(ctx: Ctx): Cmd {
         }
     })();
 
-    ctx.pushCleanup(
+    ctx.pushExtCleanup(
         vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-file-text", tdcp)
     );
 
@@ -566,13 +566,13 @@ export function viewItemTree(ctx: Ctx): Cmd {
             }
         }
 
-        provideTextDocumentContent(
+        async provideTextDocumentContent(
             _uri: vscode.Uri,
             ct: vscode.CancellationToken
-        ): vscode.ProviderResult<string> {
+        ): Promise<string> {
             const rustEditor = ctx.activeRustEditor;
-            const client = ctx.client;
-            if (!rustEditor || !client) return "";
+            if (!rustEditor) return "";
+            const client = await ctx.getClient();
 
             const params = {
                 textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
@@ -587,7 +587,7 @@ export function viewItemTree(ctx: Ctx): Cmd {
         }
     })();
 
-    ctx.pushCleanup(
+    ctx.pushExtCleanup(
         vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-item-tree", tdcp)
     );
 
@@ -618,8 +618,8 @@ function crateGraph(ctx: Ctx, full: boolean): Cmd {
         const params = {
             full: full,
         };
-
-        const dot = await ctx.client.sendRequest(ra.viewCrateGraph, params);
+        const client = await ctx.getClient();
+        const dot = await client.sendRequest(ra.viewCrateGraph, params);
         const uri = panel.webview.asWebviewUri(nodeModulesPath);
 
         const html = `
@@ -690,13 +690,13 @@ export function expandMacro(ctx: Ctx): Cmd {
         eventEmitter = new vscode.EventEmitter<vscode.Uri>();
         async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
             const editor = vscode.window.activeTextEditor;
-            const client = ctx.client;
-            if (!editor || !client) return "";
+            if (!editor) return "";
+            const client = await ctx.getClient();
 
             const position = editor.selection.active;
 
             const expanded = await client.sendRequest(ra.expandMacro, {
-                textDocument: ctx.client.code2ProtocolConverter.asTextDocumentIdentifier(
+                textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
                     editor.document
                 ),
                 position,
@@ -712,7 +712,7 @@ export function expandMacro(ctx: Ctx): Cmd {
         }
     })();
 
-    ctx.pushCleanup(
+    ctx.pushExtCleanup(
         vscode.workspace.registerTextDocumentContentProvider("rust-analyzer-expand-macro", tdcp)
     );
 
@@ -724,11 +724,11 @@ export function expandMacro(ctx: Ctx): Cmd {
 }
 
 export function reloadWorkspace(ctx: Ctx): Cmd {
-    return async () => ctx.client.sendRequest(ra.reloadWorkspace);
+    return async () => (await ctx.getClient()).sendRequest(ra.reloadWorkspace);
 }
 
 async function showReferencesImpl(
-    client: LanguageClient,
+    client: LanguageClient | undefined,
     uri: string,
     position: lc.Position,
     locations: lc.Location[]
@@ -745,7 +745,7 @@ async function showReferencesImpl(
 
 export function showReferences(ctx: Ctx): Cmd {
     return async (uri: string, position: lc.Position, locations: lc.Location[]) => {
-        await showReferencesImpl(ctx.client, uri, position, locations);
+        await showReferencesImpl(await ctx.getClient(), uri, position, locations);
     };
 }
 
@@ -762,25 +762,23 @@ export function applyActionGroup(_ctx: Ctx): Cmd {
 
 export function gotoLocation(ctx: Ctx): Cmd {
     return async (locationLink: lc.LocationLink) => {
-        const client = ctx.client;
-        if (client) {
-            const uri = client.protocol2CodeConverter.asUri(locationLink.targetUri);
-            let range = client.protocol2CodeConverter.asRange(locationLink.targetSelectionRange);
-            // collapse the range to a cursor position
-            range = range.with({ end: range.start });
+        const client = await ctx.getClient();
+        const uri = client.protocol2CodeConverter.asUri(locationLink.targetUri);
+        let range = client.protocol2CodeConverter.asRange(locationLink.targetSelectionRange);
+        // collapse the range to a cursor position
+        range = range.with({ end: range.start });
 
-            await vscode.window.showTextDocument(uri, { selection: range });
-        }
+        await vscode.window.showTextDocument(uri, { selection: range });
     };
 }
 
 export function openDocs(ctx: Ctx): Cmd {
     return async () => {
-        const client = ctx.client;
         const editor = vscode.window.activeTextEditor;
-        if (!editor || !client) {
+        if (!editor) {
             return;
         }
+        const client = await ctx.getClient();
 
         const position = editor.selection.active;
         const textDocument = { uri: editor.document.uri.toString() };
@@ -795,20 +793,21 @@ export function openDocs(ctx: Ctx): Cmd {
 
 export function cancelFlycheck(ctx: Ctx): Cmd {
     return async () => {
-        await ctx.client.sendRequest(ra.cancelFlycheck);
+        const client = await ctx.getClient();
+        await client.sendRequest(ra.cancelFlycheck);
     };
 }
 
 export function resolveCodeAction(ctx: Ctx): Cmd {
-    const client = ctx.client;
     return async (params: lc.CodeAction) => {
+        const client = await ctx.getClient();
         params.command = undefined;
-        const item = await client.sendRequest(lc.CodeActionResolveRequest.type, params);
-        if (!item.edit) {
+        const item = await client?.sendRequest(lc.CodeActionResolveRequest.type, params);
+        if (!item?.edit) {
             return;
         }
         const itemEdit = item.edit;
-        const edit = await client.protocol2CodeConverter.asWorkspaceEdit(itemEdit);
+        const edit = await client?.protocol2CodeConverter.asWorkspaceEdit(itemEdit);
         // filter out all text edits and recreate the WorkspaceEdit without them so we can apply
         // snippet edits on our own
         const lcFileSystemEdit = {
@@ -847,11 +846,10 @@ export function run(ctx: Ctx): Cmd {
 }
 
 export function peekTests(ctx: Ctx): Cmd {
-    const client = ctx.client;
-
     return async () => {
         const editor = ctx.activeRustEditor;
-        if (!editor || !client) return;
+        if (!editor) return;
+        const client = await ctx.getClient();
 
         await vscode.window.withProgress(
             {
@@ -937,10 +935,10 @@ export function newDebugConfig(ctx: Ctx): Cmd {
     };
 }
 
-export function linkToCommand(ctx: Ctx): Cmd {
+export function linkToCommand(_: Ctx): Cmd {
     return async (commandId: string) => {
         const link = LINKED_COMMANDS.get(commandId);
-        if (ctx.client && link) {
+        if (link) {
             const { command, arguments: args = [] } = link;
             await vscode.commands.executeCommand(command, ...args);
         }
diff --git a/src/tools/rust-analyzer/editors/code/src/config.ts b/src/tools/rust-analyzer/editors/code/src/config.ts
index 15846a5e864..632a7d86faa 100644
--- a/src/tools/rust-analyzer/editors/code/src/config.ts
+++ b/src/tools/rust-analyzer/editors/code/src/config.ts
@@ -1,4 +1,5 @@
-import path = require("path");
+import * as path from "path";
+import * as os from "os";
 import * as vscode from "vscode";
 import { Env } from "./client";
 import { log } from "./util";
@@ -10,23 +11,17 @@ export type RunnableEnvCfg =
 
 export class Config {
     readonly extensionId = "rust-lang.rust-analyzer";
+    configureLang: vscode.Disposable | undefined;
 
     readonly rootSection = "rust-analyzer";
-    private readonly requiresWorkspaceReloadOpts = [
-        "serverPath",
-        "server",
-        // FIXME: This shouldn't be here, changing this setting should reload
-        // `continueCommentsOnNewline` behavior without restart
-        "typing",
-    ].map((opt) => `${this.rootSection}.${opt}`);
     private readonly requiresReloadOpts = [
         "cargo",
         "procMacro",
+        "serverPath",
+        "server",
         "files",
         "lens", // works as lens.*
-    ]
-        .map((opt) => `${this.rootSection}.${opt}`)
-        .concat(this.requiresWorkspaceReloadOpts);
+    ].map((opt) => `${this.rootSection}.${opt}`);
 
     readonly package: {
         version: string;
@@ -44,6 +39,11 @@ export class Config {
             ctx.subscriptions
         );
         this.refreshLogging();
+        this.configureLanguage();
+    }
+
+    dispose() {
+        this.configureLang?.dispose();
     }
 
     private refreshLogging() {
@@ -57,33 +57,86 @@ export class Config {
     private async onDidChangeConfiguration(event: vscode.ConfigurationChangeEvent) {
         this.refreshLogging();
 
+        this.configureLanguage();
+
         const requiresReloadOpt = this.requiresReloadOpts.find((opt) =>
             event.affectsConfiguration(opt)
         );
 
         if (!requiresReloadOpt) return;
 
-        const requiresWorkspaceReloadOpt = this.requiresWorkspaceReloadOpts.find((opt) =>
-            event.affectsConfiguration(opt)
-        );
-
-        if (!requiresWorkspaceReloadOpt && this.restartServerOnConfigChange) {
+        if (this.restartServerOnConfigChange) {
             await vscode.commands.executeCommand("rust-analyzer.reload");
             return;
         }
 
-        const message = requiresWorkspaceReloadOpt
-            ? `Changing "${requiresWorkspaceReloadOpt}" requires a window reload`
-            : `Changing "${requiresReloadOpt}" requires a reload`;
-        const userResponse = await vscode.window.showInformationMessage(message, "Reload now");
-
-        if (userResponse === "Reload now") {
-            const command = requiresWorkspaceReloadOpt
-                ? "workbench.action.reloadWindow"
-                : "rust-analyzer.reload";
-            if (userResponse === "Reload now") {
-                await vscode.commands.executeCommand(command);
-            }
+        const message = `Changing "${requiresReloadOpt}" requires a server restart`;
+        const userResponse = await vscode.window.showInformationMessage(message, "Restart now");
+
+        if (userResponse) {
+            const command = "rust-analyzer.reload";
+            await vscode.commands.executeCommand(command);
+        }
+    }
+
+    /**
+     * Sets up additional language configuration that's impossible to do via a
+     * separate language-configuration.json file. See [1] for more information.
+     *
+     * [1]: https://github.com/Microsoft/vscode/issues/11514#issuecomment-244707076
+     */
+    private configureLanguage() {
+        if (this.typingContinueCommentsOnNewline && !this.configureLang) {
+            const indentAction = vscode.IndentAction.None;
+
+            this.configureLang = vscode.languages.setLanguageConfiguration("rust", {
+                onEnterRules: [
+                    {
+                        // Doc single-line comment
+                        // e.g. ///|
+                        beforeText: /^\s*\/{3}.*$/,
+                        action: { indentAction, appendText: "/// " },
+                    },
+                    {
+                        // Parent doc single-line comment
+                        // e.g. //!|
+                        beforeText: /^\s*\/{2}\!.*$/,
+                        action: { indentAction, appendText: "//! " },
+                    },
+                    {
+                        // Begins an auto-closed multi-line comment (standard or parent doc)
+                        // e.g. /** | */ or /*! | */
+                        beforeText: /^\s*\/\*(\*|\!)(?!\/)([^\*]|\*(?!\/))*$/,
+                        afterText: /^\s*\*\/$/,
+                        action: {
+                            indentAction: vscode.IndentAction.IndentOutdent,
+                            appendText: " * ",
+                        },
+                    },
+                    {
+                        // Begins a multi-line comment (standard or parent doc)
+                        // e.g. /** ...| or /*! ...|
+                        beforeText: /^\s*\/\*(\*|\!)(?!\/)([^\*]|\*(?!\/))*$/,
+                        action: { indentAction, appendText: " * " },
+                    },
+                    {
+                        // Continues a multi-line comment
+                        // e.g.  * ...|
+                        beforeText: /^(\ \ )*\ \*(\ ([^\*]|\*(?!\/))*)?$/,
+                        action: { indentAction, appendText: "* " },
+                    },
+                    {
+                        // Dedents after closing a multi-line comment
+                        // e.g.  */|
+                        beforeText: /^(\ \ )*\ \*\/\s*$/,
+                        action: { indentAction, removeText: 1 },
+                    },
+                ],
+            });
+        }
+        if (!this.typingContinueCommentsOnNewline && this.configureLang) {
+            this.configureLang.dispose();
+            this.configureLang = undefined;
         }
     }
 
@@ -187,6 +240,37 @@ export class Config {
     }
 }
 
+const VarRegex = new RegExp(/\$\{(.+?)\}/g);
+
+export function substituteVSCodeVariableInString(val: string): string {
+    return val.replace(VarRegex, (substring: string, varName) => {
+        if (typeof varName === "string") {
+            return computeVscodeVar(varName) || substring;
+        } else {
+            return substring;
+        }
+    });
+}
+
+export function substituteVSCodeVariables(resp: any): any {
+    if (typeof resp === "string") {
+        return substituteVSCodeVariableInString(resp);
+    } else if (resp && Array.isArray(resp)) {
+        return resp.map((val) => {
+            return substituteVSCodeVariables(val);
+        });
+    } else if (resp && typeof resp === "object") {
+        const res: { [key: string]: any } = {};
+        for (const key in resp) {
+            const val = resp[key];
+            res[key] = substituteVSCodeVariables(val);
+        }
+        return res;
+    } else if (typeof resp === "function") {
+        return null;
+    }
+    return resp;
+}
 export function substituteVariablesInEnv(env: Env): Env {
     const missingDeps = new Set<string>();
     // vscode uses `env:ENV_NAME` for env vars resolution, and it's easier
@@ -233,7 +317,7 @@ export function substituteVariablesInEnv(env: Env): Env {
             }
         } else {
             envWithDeps[dep] = {
-                value: computeVscodeVar(dep),
+                value: computeVscodeVar(dep) || "${" + dep + "}",
                 deps: [],
             };
         }
@@ -264,37 +348,34 @@ export function substituteVariablesInEnv(env: Env): Env {
     return resolvedEnv;
 }
 
-function computeVscodeVar(varName: string): string {
+function computeVscodeVar(varName: string): string | null {
+    const workspaceFolder = () => {
+        const folders = vscode.workspace.workspaceFolders ?? [];
+        if (folders.length === 1) {
+            // TODO: support for remote workspaces?
+            return folders[0].uri.fsPath;
+        } else if (folders.length > 1) {
+            // could use currently opened document to detect the correct
+            // workspace. However, that would be determined by the document
+            // user has opened on Editor startup. Could lead to
+            // unpredictable workspace selection in practice.
+            // It's better to pick the first one
+            return folders[0].uri.fsPath;
+        } else {
+            // no workspace opened
+            return "";
+        }
+    };
     // https://code.visualstudio.com/docs/editor/variables-reference
     const supportedVariables: { [k: string]: () => string } = {
-        workspaceFolder: () => {
-            const folders = vscode.workspace.workspaceFolders ?? [];
-            if (folders.length === 1) {
-                // TODO: support for remote workspaces?
-                return folders[0].uri.fsPath;
-            } else if (folders.length > 1) {
-                // could use currently opened document to detect the correct
-                // workspace. However, that would be determined by the document
-                // user has opened on Editor startup. Could lead to
-                // unpredictable workspace selection in practice.
-                // It's better to pick the first one
-                return folders[0].uri.fsPath;
-            } else {
-                // no workspace opened
-                return "";
-            }
-        },
+        workspaceFolder,
 
         workspaceFolderBasename: () => {
-            const workspaceFolder = computeVscodeVar("workspaceFolder");
-            if (workspaceFolder) {
-                return path.basename(workspaceFolder);
-            } else {
-                return "";
-            }
+            return path.basename(workspaceFolder());
         },
 
         cwd: () => process.cwd(),
+        userHome: () => os.homedir(),
 
         // see
         // https://github.com/microsoft/vscode/blob/08ac1bb67ca2459496b272d8f4a908757f24f56f/src/vs/workbench/api/common/extHostVariableResolverService.ts#L81
@@ -308,7 +389,7 @@ function computeVscodeVar(varName: string): string {
     if (varName in supportedVariables) {
         return supportedVariables[varName]();
     } else {
-        // can't resolve, keep the expression as is
-        return "${" + varName + "}";
+        // return "${" + varName + "}";
+        return null;
     }
 }
diff --git a/src/tools/rust-analyzer/editors/code/src/ctx.ts b/src/tools/rust-analyzer/editors/code/src/ctx.ts
index 26510011d43..044a9470aa9 100644
--- a/src/tools/rust-analyzer/editors/code/src/ctx.ts
+++ b/src/tools/rust-analyzer/editors/code/src/ctx.ts
@@ -2,10 +2,12 @@ import * as vscode from "vscode";
 import * as lc from "vscode-languageclient/node";
 import * as ra from "./lsp_ext";
 
-import { Config } from "./config";
+import { Config, substituteVariablesInEnv, substituteVSCodeVariables } from "./config";
 import { createClient } from "./client";
-import { isRustEditor, RustEditor } from "./util";
+import { isRustEditor, log, RustEditor } from "./util";
 import { ServerStatusParams } from "./lsp_ext";
+import { PersistentState } from "./persistent_state";
+import { bootstrap } from "./bootstrap";
 
 export type Workspace =
     | {
@@ -16,66 +18,192 @@ export type Workspace =
           files: vscode.TextDocument[];
       };
 
+export type CommandFactory = {
+    enabled: (ctx: Ctx) => Cmd;
+    disabled?: (ctx: Ctx) => Cmd;
+};
+
 export class Ctx {
-    private constructor(
-        readonly config: Config,
-        private readonly extCtx: vscode.ExtensionContext,
-        readonly client: lc.LanguageClient,
-        readonly serverPath: string,
-        readonly statusBar: vscode.StatusBarItem
-    ) {}
-
-    static async create(
-        config: Config,
-        extCtx: vscode.ExtensionContext,
-        serverPath: string,
-        workspace: Workspace
-    ): Promise<Ctx> {
-        const client = await createClient(serverPath, workspace, config.serverExtraEnv);
-
-        const statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
-        extCtx.subscriptions.push(statusBar);
-        statusBar.text = "rust-analyzer";
-        statusBar.tooltip = "ready";
-        statusBar.command = "rust-analyzer.analyzerStatus";
-        statusBar.show();
-
-        const res = new Ctx(config, extCtx, client, serverPath, statusBar);
-
-        res.pushCleanup(client.start());
-        await client.onReady();
-        client.onNotification(ra.serverStatus, (params) => res.setServerStatus(params));
-        return res;
+    readonly statusBar: vscode.StatusBarItem;
+    readonly config: Config;
+
+    private client: lc.LanguageClient | undefined;
+    private _serverPath: string | undefined;
+    private traceOutputChannel: vscode.OutputChannel | undefined;
+    private outputChannel: vscode.OutputChannel | undefined;
+    private clientSubscriptions: Disposable[];
+    private state: PersistentState;
+    private commandFactories: Record<string, CommandFactory>;
+    private commandDisposables: Disposable[];
+
+    workspace: Workspace;
+
+    constructor(
+        readonly extCtx: vscode.ExtensionContext,
+        workspace: Workspace,
+        commandFactories: Record<string, CommandFactory>
+    ) {
+        extCtx.subscriptions.push(this);
+        this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
+        this.statusBar.text = "rust-analyzer";
+        this.statusBar.tooltip = "ready";
+        this.statusBar.command = "rust-analyzer.analyzerStatus";
+        this.statusBar.show();
+        this.workspace = workspace;
+        this.clientSubscriptions = [];
+        this.commandDisposables = [];
+        this.commandFactories = commandFactories;
+
+        this.state = new PersistentState(extCtx.globalState);
+        this.config = new Config(extCtx);
+
+        this.updateCommands();
     }
 
-    get activeRustEditor(): RustEditor | undefined {
-        const editor = vscode.window.activeTextEditor;
-        return editor && isRustEditor(editor) ? editor : undefined;
+    dispose() {
+        this.config.dispose();
+        this.statusBar.dispose();
+        void this.disposeClient();
+        this.commandDisposables.forEach((disposable) => disposable.dispose());
     }
 
-    get visibleRustEditors(): RustEditor[] {
-        return vscode.window.visibleTextEditors.filter(isRustEditor);
+    clientFetcher() {
+        const self = this;
+        return {
+            get client(): lc.LanguageClient | undefined {
+                return self.client;
+            },
+        };
     }
 
-    registerCommand(name: string, factory: (ctx: Ctx) => Cmd) {
-        const fullName = `rust-analyzer.${name}`;
-        const cmd = factory(this);
-        const d = vscode.commands.registerCommand(fullName, cmd);
-        this.pushCleanup(d);
+    async getClient() {
+        if (!this.traceOutputChannel) {
+            this.traceOutputChannel = vscode.window.createOutputChannel(
+                "Rust Analyzer Language Server Trace"
+            );
+            this.pushExtCleanup(this.traceOutputChannel);
+        }
+        if (!this.outputChannel) {
+            this.outputChannel = vscode.window.createOutputChannel("Rust Analyzer Language Server");
+            this.pushExtCleanup(this.outputChannel);
+        }
+
+        if (!this.client) {
+            this._serverPath = await bootstrap(this.extCtx, this.config, this.state).catch(
+                (err) => {
+                    let message = "bootstrap error. ";
+
+                    message +=
+                        'See the logs in "OUTPUT > Rust Analyzer Client" (should open automatically). ';
+                    message +=
+                        'To enable verbose logs use { "rust-analyzer.trace.extension": true }';
+
+                    log.error("Bootstrap error", err);
+                    throw new Error(message);
+                }
+            );
+            const newEnv = substituteVariablesInEnv(
+                Object.assign({}, process.env, this.config.serverExtraEnv)
+            );
+            const run: lc.Executable = {
+                command: this._serverPath,
+                options: { env: newEnv },
+            };
+            const serverOptions = {
+                run,
+                debug: run,
+            };
+
+            let rawInitializationOptions = vscode.workspace.getConfiguration("rust-analyzer");
+
+            if (this.workspace.kind === "Detached Files") {
+                rawInitializationOptions = {
+                    detachedFiles: this.workspace.files.map((file) => file.uri.fsPath),
+                    ...rawInitializationOptions,
+                };
+            }
+
+            const initializationOptions = substituteVSCodeVariables(rawInitializationOptions);
+
+            this.client = await createClient(
+                this.traceOutputChannel,
+                this.outputChannel,
+                initializationOptions,
+                serverOptions
+            );
+            this.pushClientCleanup(
+                this.client.onNotification(ra.serverStatus, (params) =>
+                    this.setServerStatus(params)
+                )
+            );
+        }
+        return this.client;
     }
 
-    get extensionPath(): string {
-        return this.extCtx.extensionPath;
+    async activate() {
+        log.info("Activating language client");
+        const client = await this.getClient();
+        await client.start();
+        this.updateCommands();
+        return client;
+    }
+
+    async deactivate() {
+        log.info("Deactivating language client");
+        await this.client?.stop();
+        this.updateCommands();
+    }
+
+    async stop() {
+        log.info("Stopping language client");
+        await this.disposeClient();
+        this.updateCommands();
+    }
+
+    private async disposeClient() {
+        this.clientSubscriptions?.forEach((disposable) => disposable.dispose());
+        this.clientSubscriptions = [];
+        await this.client?.dispose();
+        this._serverPath = undefined;
+        this.client = undefined;
+    }
+
+    get activeRustEditor(): RustEditor | undefined {
+        const editor = vscode.window.activeTextEditor;
+        return editor && isRustEditor(editor) ? editor : undefined;
     }
 
-    get globalState(): vscode.Memento {
-        return this.extCtx.globalState;
+    get extensionPath(): string {
+        return this.extCtx.extensionPath;
     }
 
     get subscriptions(): Disposable[] {
         return this.extCtx.subscriptions;
     }
 
+    get serverPath(): string | undefined {
+        return this._serverPath;
+    }
+
+    private updateCommands() {
+        this.commandDisposables.forEach((disposable) => disposable.dispose());
+        this.commandDisposables = [];
+        const fetchFactory = (factory: CommandFactory, fullName: string) => {
+            return this.client && this.client.isRunning()
+                ? factory.enabled
+                : factory.disabled ||
+                      ((_) => () =>
+                          vscode.window.showErrorMessage(
+                              `command ${fullName} failed: rust-analyzer server is not running`
+                          ));
+        };
+        for (const [name, factory] of Object.entries(this.commandFactories)) {
+            const fullName = `rust-analyzer.${name}`;
+            const callback = fetchFactory(factory, fullName)(this);
+            this.commandDisposables.push(vscode.commands.registerCommand(fullName, callback));
+        }
+    }
+
     setServerStatus(status: ServerStatusParams) {
         let icon = "";
         const statusBar = this.statusBar;
@@ -111,9 +239,13 @@ export class Ctx {
         statusBar.text = `${icon}rust-analyzer`;
     }
 
-    pushCleanup(d: Disposable) {
+    pushExtCleanup(d: Disposable) {
         this.extCtx.subscriptions.push(d);
     }
+
+    private pushClientCleanup(d: Disposable) {
+        this.clientSubscriptions.push(d);
+    }
 }
 
 export interface Disposable {
diff --git a/src/tools/rust-analyzer/editors/code/src/main.ts b/src/tools/rust-analyzer/editors/code/src/main.ts
index 41bde4195e0..8c3a676ffb0 100644
--- a/src/tools/rust-analyzer/editors/code/src/main.ts
+++ b/src/tools/rust-analyzer/editors/code/src/main.ts
@@ -1,53 +1,37 @@
 import * as vscode from "vscode";
 import * as lc from "vscode-languageclient/node";
-import * as os from "os";
 
 import * as commands from "./commands";
-import { Ctx } from "./ctx";
-import { Config } from "./config";
-import { log, isValidExecutable, isRustDocument } from "./util";
-import { PersistentState } from "./persistent_state";
+import { CommandFactory, Ctx, Workspace } from "./ctx";
+import { isRustDocument } from "./util";
 import { activateTaskProvider } from "./tasks";
 import { setContextValue } from "./util";
-import { exec } from "child_process";
-
-let ctx: Ctx | undefined;
 
 const RUST_PROJECT_CONTEXT_NAME = "inRustProject";
 
-let TRACE_OUTPUT_CHANNEL: vscode.OutputChannel | null = null;
-export function traceOutputChannel() {
-    if (!TRACE_OUTPUT_CHANNEL) {
-        TRACE_OUTPUT_CHANNEL = vscode.window.createOutputChannel(
-            "Rust Analyzer Language Server Trace"
-        );
-    }
-    return TRACE_OUTPUT_CHANNEL;
-}
-let OUTPUT_CHANNEL: vscode.OutputChannel | null = null;
-export function outputChannel() {
-    if (!OUTPUT_CHANNEL) {
-        OUTPUT_CHANNEL = vscode.window.createOutputChannel("Rust Analyzer Language Server");
-    }
-    return OUTPUT_CHANNEL;
+export interface RustAnalyzerExtensionApi {
+    // FIXME: this should be non-optional
+    readonly client?: lc.LanguageClient;
 }
 
-export interface RustAnalyzerExtensionApi {
-    client?: lc.LanguageClient;
+export async function deactivate() {
+    await setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
 }
 
 export async function activate(
     context: vscode.ExtensionContext
 ): Promise<RustAnalyzerExtensionApi> {
-    // VS Code doesn't show a notification when an extension fails to activate
-    // so we do it ourselves.
-    return await tryActivate(context).catch((err) => {
-        void vscode.window.showErrorMessage(`Cannot activate rust-analyzer: ${err.message}`);
-        throw err;
-    });
-}
+    if (vscode.extensions.getExtension("rust-lang.rust")) {
+        vscode.window
+            .showWarningMessage(
+                `You have both the rust-analyzer (rust-lang.rust-analyzer) and Rust (rust-lang.rust) ` +
+                    "plugins enabled. These are known to conflict and cause various functions of " +
+                    "both plugins to not work correctly. You should disable one of them.",
+                "Got it"
+            )
+            .then(() => {}, console.error);
+    }
 
-async function tryActivate(context: vscode.ExtensionContext): Promise<RustAnalyzerExtensionApi> {
     // We only support local folders, not eg. Live Share (`vlsl:` scheme), so don't activate if
     // only those are in use.
     // (r-a still somewhat works with Live Share, because commands are tunneled to the host)
@@ -65,351 +49,118 @@ async function tryActivate(context: vscode.ExtensionContext): Promise<RustAnalyz
         return {};
     }
 
-    const config = new Config(context);
-    const state = new PersistentState(context.globalState);
-    const serverPath = await bootstrap(context, config, state).catch((err) => {
-        let message = "bootstrap error. ";
-
-        message += 'See the logs in "OUTPUT > Rust Analyzer Client" (should open automatically). ';
-        message += 'To enable verbose logs use { "rust-analyzer.trace.extension": true }';
+    const workspace: Workspace =
+        folders.length === 0
+            ? {
+                  kind: "Detached Files",
+                  files: rustDocuments,
+              }
+            : { kind: "Workspace Folder" };
 
-        log.error("Bootstrap error", err);
-        throw new Error(message);
+    const ctx = new Ctx(context, workspace, createCommands());
+    // VS Code doesn't show a notification when an extension fails to activate
+    // so we do it ourselves.
+    const api = await activateServer(ctx).catch((err) => {
+        void vscode.window.showErrorMessage(
+            `Cannot activate rust-analyzer extension: ${err.message}`
+        );
+        throw err;
     });
+    await setContextValue(RUST_PROJECT_CONTEXT_NAME, true);
+    return api;
+}
 
-    if (folders.length === 0) {
-        ctx = await Ctx.create(config, context, serverPath, {
-            kind: "Detached Files",
-            files: rustDocuments,
-        });
-    } else {
-        // Note: we try to start the server before we activate type hints so that it
-        // registers its `onDidChangeDocument` handler before us.
-        //
-        // This a horribly, horribly wrong way to deal with this problem.
-        ctx = await Ctx.create(config, context, serverPath, { kind: "Workspace Folder" });
-        ctx.pushCleanup(activateTaskProvider(ctx.config));
-    }
-    await initCommonContext(context, ctx);
-
-    warnAboutExtensionConflicts();
-
-    if (config.typingContinueCommentsOnNewline) {
-        ctx.pushCleanup(configureLanguage());
+async function activateServer(ctx: Ctx): Promise<RustAnalyzerExtensionApi> {
+    if (ctx.workspace.kind === "Workspace Folder") {
+        ctx.pushExtCleanup(activateTaskProvider(ctx.config));
     }
 
     vscode.workspace.onDidChangeConfiguration(
-        (_) =>
-            ctx?.client
-                ?.sendNotification("workspace/didChangeConfiguration", { settings: "" })
-                .catch(log.error),
+        async (_) => {
+            await ctx
+                .clientFetcher()
+                .client?.sendNotification("workspace/didChangeConfiguration", { settings: "" });
+        },
         null,
         ctx.subscriptions
     );
 
-    return {
-        client: ctx.client,
-    };
-}
-
-async function initCommonContext(context: vscode.ExtensionContext, ctx: Ctx) {
-    // Register a "dumb" onEnter command for the case where server fails to
-    // start.
-    //
-    // FIXME: refactor command registration code such that commands are
-    // **always** registered, even if the server does not start. Use API like
-    // this perhaps?
-    //
-    // ```TypeScript
-    // registerCommand(
-    //    factory: (Ctx) => ((Ctx) => any),
-    //    fallback: () => any = () => vscode.window.showErrorMessage(
-    //        "rust-analyzer is not available"
-    //    ),
-    // )
-    const defaultOnEnter = vscode.commands.registerCommand("rust-analyzer.onEnter", () =>
-        vscode.commands.executeCommand("default:type", { text: "\n" })
-    );
-    context.subscriptions.push(defaultOnEnter);
-
-    await setContextValue(RUST_PROJECT_CONTEXT_NAME, true);
-
-    // Commands which invokes manually via command palette, shortcut, etc.
-
-    // Reloading is inspired by @DanTup maneuver: https://github.com/microsoft/vscode/issues/45774#issuecomment-373423895
-    ctx.registerCommand("reload", (_) => async () => {
-        void vscode.window.showInformationMessage("Reloading rust-analyzer...");
-        await doDeactivate();
-        while (context.subscriptions.length > 0) {
-            try {
-                context.subscriptions.pop()!.dispose();
-            } catch (err) {
-                log.error("Dispose error:", err);
-            }
-        }
-        await activate(context).catch(log.error);
-    });
-
-    ctx.registerCommand("analyzerStatus", commands.analyzerStatus);
-    ctx.registerCommand("memoryUsage", commands.memoryUsage);
-    ctx.registerCommand("shuffleCrateGraph", commands.shuffleCrateGraph);
-    ctx.registerCommand("reloadWorkspace", commands.reloadWorkspace);
-    ctx.registerCommand("matchingBrace", commands.matchingBrace);
-    ctx.registerCommand("joinLines", commands.joinLines);
-    ctx.registerCommand("parentModule", commands.parentModule);
-    ctx.registerCommand("syntaxTree", commands.syntaxTree);
-    ctx.registerCommand("viewHir", commands.viewHir);
-    ctx.registerCommand("viewFileText", commands.viewFileText);
-    ctx.registerCommand("viewItemTree", commands.viewItemTree);
-    ctx.registerCommand("viewCrateGraph", commands.viewCrateGraph);
-    ctx.registerCommand("viewFullCrateGraph", commands.viewFullCrateGraph);
-    ctx.registerCommand("expandMacro", commands.expandMacro);
-    ctx.registerCommand("run", commands.run);
-    ctx.registerCommand("copyRunCommandLine", commands.copyRunCommandLine);
-    ctx.registerCommand("debug", commands.debug);
-    ctx.registerCommand("newDebugConfig", commands.newDebugConfig);
-    ctx.registerCommand("openDocs", commands.openDocs);
-    ctx.registerCommand("openCargoToml", commands.openCargoToml);
-    ctx.registerCommand("peekTests", commands.peekTests);
-    ctx.registerCommand("moveItemUp", commands.moveItemUp);
-    ctx.registerCommand("moveItemDown", commands.moveItemDown);
-    ctx.registerCommand("cancelFlycheck", commands.cancelFlycheck);
-
-    defaultOnEnter.dispose();
-    ctx.registerCommand("onEnter", commands.onEnter);
-
-    ctx.registerCommand("ssr", commands.ssr);
-    ctx.registerCommand("serverVersion", commands.serverVersion);
-
-    // Internal commands which are invoked by the server.
-    ctx.registerCommand("runSingle", commands.runSingle);
-    ctx.registerCommand("debugSingle", commands.debugSingle);
-    ctx.registerCommand("showReferences", commands.showReferences);
-    ctx.registerCommand("applySnippetWorkspaceEdit", commands.applySnippetWorkspaceEditCommand);
-    ctx.registerCommand("resolveCodeAction", commands.resolveCodeAction);
-    ctx.registerCommand("applyActionGroup", commands.applyActionGroup);
-    ctx.registerCommand("gotoLocation", commands.gotoLocation);
-
-    ctx.registerCommand("linkToCommand", commands.linkToCommand);
-}
-
-export async function deactivate() {
-    TRACE_OUTPUT_CHANNEL?.dispose();
-    TRACE_OUTPUT_CHANNEL = null;
-    OUTPUT_CHANNEL?.dispose();
-    OUTPUT_CHANNEL = null;
-    await doDeactivate();
-}
-
-async function doDeactivate() {
-    await setContextValue(RUST_PROJECT_CONTEXT_NAME, undefined);
-    await ctx?.client.stop();
-    ctx = undefined;
-}
-
-async function bootstrap(
-    context: vscode.ExtensionContext,
-    config: Config,
-    state: PersistentState
-): Promise<string> {
-    const path = await getServer(context, config, state);
-    if (!path) {
-        throw new Error(
-            "Rust Analyzer Language Server is not available. " +
-                "Please, ensure its [proper installation](https://rust-analyzer.github.io/manual.html#installation)."
-        );
-    }
-
-    log.info("Using server binary at", path);
-
-    if (!isValidExecutable(path)) {
-        if (config.serverPath) {
-            throw new Error(`Failed to execute ${path} --version. \`config.server.path\` or \`config.serverPath\` has been set explicitly.\
-            Consider removing this config or making a valid server binary available at that path.`);
-        } else {
-            throw new Error(`Failed to execute ${path} --version`);
-        }
-    }
-
-    return path;
+    await ctx.activate();
+    return ctx.clientFetcher();
 }
 
-async function patchelf(dest: vscode.Uri): Promise<void> {
-    await vscode.window.withProgress(
-        {
-            location: vscode.ProgressLocation.Notification,
-            title: "Patching rust-analyzer for NixOS",
+function createCommands(): Record<string, CommandFactory> {
+    return {
+        onEnter: {
+            enabled: commands.onEnter,
+            disabled: (_) => () => vscode.commands.executeCommand("default:type", { text: "\n" }),
         },
-        async (progress, _) => {
-            const expression = `
-            {srcStr, pkgs ? import <nixpkgs> {}}:
-                pkgs.stdenv.mkDerivation {
-                    name = "rust-analyzer";
-                    src = /. + srcStr;
-                    phases = [ "installPhase" "fixupPhase" ];
-                    installPhase = "cp $src $out";
-                    fixupPhase = ''
-                    chmod 755 $out
-                    patchelf --set-interpreter "$(cat $NIX_CC/nix-support/dynamic-linker)" $out
-                    '';
-                }
-            `;
-            const origFile = vscode.Uri.file(dest.fsPath + "-orig");
-            await vscode.workspace.fs.rename(dest, origFile, { overwrite: true });
-            try {
-                progress.report({ message: "Patching executable", increment: 20 });
-                await new Promise((resolve, reject) => {
-                    const handle = exec(
-                        `nix-build -E - --argstr srcStr '${origFile.fsPath}' -o '${dest.fsPath}'`,
-                        (err, stdout, stderr) => {
-                            if (err != null) {
-                                reject(Error(stderr));
-                            } else {
-                                resolve(stdout);
-                            }
-                        }
-                    );
-                    handle.stdin?.write(expression);
-                    handle.stdin?.end();
-                });
-            } finally {
-                await vscode.workspace.fs.delete(origFile);
-            }
-        }
-    );
-}
-
-async function getServer(
-    context: vscode.ExtensionContext,
-    config: Config,
-    state: PersistentState
-): Promise<string | undefined> {
-    const explicitPath = serverPath(config);
-    if (explicitPath) {
-        if (explicitPath.startsWith("~/")) {
-            return os.homedir() + explicitPath.slice("~".length);
-        }
-        return explicitPath;
-    }
-    if (config.package.releaseTag === null) return "rust-analyzer";
-
-    const ext = process.platform === "win32" ? ".exe" : "";
-    const bundled = vscode.Uri.joinPath(context.extensionUri, "server", `rust-analyzer${ext}`);
-    const bundledExists = await vscode.workspace.fs.stat(bundled).then(
-        () => true,
-        () => false
-    );
-    if (bundledExists) {
-        let server = bundled;
-        if (await isNixOs()) {
-            await vscode.workspace.fs.createDirectory(config.globalStorageUri).then();
-            const dest = vscode.Uri.joinPath(config.globalStorageUri, `rust-analyzer${ext}`);
-            let exists = await vscode.workspace.fs.stat(dest).then(
-                () => true,
-                () => false
-            );
-            if (exists && config.package.version !== state.serverVersion) {
-                await vscode.workspace.fs.delete(dest);
-                exists = false;
-            }
-            if (!exists) {
-                await vscode.workspace.fs.copy(bundled, dest);
-                await patchelf(dest);
-            }
-            server = dest;
-        }
-        await state.updateServerVersion(config.package.version);
-        return server.fsPath;
-    }
-
-    await state.updateServerVersion(undefined);
-    await vscode.window.showErrorMessage(
-        "Unfortunately we don't ship binaries for your platform yet. " +
-            "You need to manually clone the rust-analyzer repository and " +
-            "run `cargo xtask install --server` to build the language server from sources. " +
-            "If you feel that your platform should be supported, please create an issue " +
-            "about that [here](https://github.com/rust-lang/rust-analyzer/issues) and we " +
-            "will consider it."
-    );
-    return undefined;
-}
-
-function serverPath(config: Config): string | null {
-    return process.env.__RA_LSP_SERVER_DEBUG ?? config.serverPath;
-}
-
-async function isNixOs(): Promise<boolean> {
-    try {
-        const contents = (
-            await vscode.workspace.fs.readFile(vscode.Uri.file("/etc/os-release"))
-        ).toString();
-        const idString = contents.split("\n").find((a) => a.startsWith("ID=")) || "ID=linux";
-        return idString.indexOf("nixos") !== -1;
-    } catch {
-        return false;
-    }
-}
-
-function warnAboutExtensionConflicts() {
-    if (vscode.extensions.getExtension("rust-lang.rust")) {
-        vscode.window
-            .showWarningMessage(
-                `You have both the rust-analyzer (rust-lang.rust-analyzer) and Rust (rust-lang.rust) ` +
-                    "plugins enabled. These are known to conflict and cause various functions of " +
-                    "both plugins to not work correctly. You should disable one of them.",
-                "Got it"
-            )
-            .then(() => {}, console.error);
-    }
-}
-
-/**
- * Sets up additional language configuration that's impossible to do via a
- * separate language-configuration.json file. See [1] for more information.
- *
- * [1]: https://github.com/Microsoft/vscode/issues/11514#issuecomment-244707076
- */
-function configureLanguage(): vscode.Disposable {
-    const indentAction = vscode.IndentAction.None;
-    return vscode.languages.setLanguageConfiguration("rust", {
-        onEnterRules: [
-            {
-                // Doc single-line comment
-                // e.g. ///|
-                beforeText: /^\s*\/{3}.*$/,
-                action: { indentAction, appendText: "/// " },
-            },
-            {
-                // Parent doc single-line comment
-                // e.g. //!|
-                beforeText: /^\s*\/{2}\!.*$/,
-                action: { indentAction, appendText: "//! " },
+        reload: {
+            enabled: (ctx) => async () => {
+                void vscode.window.showInformationMessage("Reloading rust-analyzer...");
+                // FIXME: We should re-use the client, that is ctx.deactivate() if none of the configs have changed
+                await ctx.stop();
+                await ctx.activate();
             },
-            {
-                // Begins an auto-closed multi-line comment (standard or parent doc)
-                // e.g. /** | */ or /*! | */
-                beforeText: /^\s*\/\*(\*|\!)(?!\/)([^\*]|\*(?!\/))*$/,
-                afterText: /^\s*\*\/$/,
-                action: { indentAction: vscode.IndentAction.IndentOutdent, appendText: " * " },
+            disabled: (ctx) => async () => {
+                void vscode.window.showInformationMessage("Reloading rust-analyzer...");
+                await ctx.activate();
             },
-            {
-                // Begins a multi-line comment (standard or parent doc)
-                // e.g. /** ...| or /*! ...|
-                beforeText: /^\s*\/\*(\*|\!)(?!\/)([^\*]|\*(?!\/))*$/,
-                action: { indentAction, appendText: " * " },
+        },
+        startServer: {
+            enabled: (ctx) => async () => {
+                await ctx.activate();
             },
-            {
-                // Continues a multi-line comment
-                // e.g.  * ...|
-                beforeText: /^(\ \ )*\ \*(\ ([^\*]|\*(?!\/))*)?$/,
-                action: { indentAction, appendText: "* " },
+            disabled: (ctx) => async () => {
+                await ctx.activate();
             },
-            {
-                // Dedents after closing a multi-line comment
-                // e.g.  */|
-                beforeText: /^(\ \ )*\ \*\/\s*$/,
-                action: { indentAction, removeText: 1 },
+        },
+        stopServer: {
+            enabled: (ctx) => async () => {
+                // FIXME: We should re-use the client, that is ctx.deactivate() if none of the configs have changed
+                await ctx.stop();
+                ctx.setServerStatus({
+                    health: "ok",
+                    quiescent: true,
+                    message: "server is not running",
+                });
             },
-        ],
-    });
+        },
+
+        analyzerStatus: { enabled: commands.analyzerStatus },
+        memoryUsage: { enabled: commands.memoryUsage },
+        shuffleCrateGraph: { enabled: commands.shuffleCrateGraph },
+        reloadWorkspace: { enabled: commands.reloadWorkspace },
+        matchingBrace: { enabled: commands.matchingBrace },
+        joinLines: { enabled: commands.joinLines },
+        parentModule: { enabled: commands.parentModule },
+        syntaxTree: { enabled: commands.syntaxTree },
+        viewHir: { enabled: commands.viewHir },
+        viewFileText: { enabled: commands.viewFileText },
+        viewItemTree: { enabled: commands.viewItemTree },
+        viewCrateGraph: { enabled: commands.viewCrateGraph },
+        viewFullCrateGraph: { enabled: commands.viewFullCrateGraph },
+        expandMacro: { enabled: commands.expandMacro },
+        run: { enabled: commands.run },
+        copyRunCommandLine: { enabled: commands.copyRunCommandLine },
+        debug: { enabled: commands.debug },
+        newDebugConfig: { enabled: commands.newDebugConfig },
+        openDocs: { enabled: commands.openDocs },
+        openCargoToml: { enabled: commands.openCargoToml },
+        peekTests: { enabled: commands.peekTests },
+        moveItemUp: { enabled: commands.moveItemUp },
+        moveItemDown: { enabled: commands.moveItemDown },
+        cancelFlycheck: { enabled: commands.cancelFlycheck },
+        ssr: { enabled: commands.ssr },
+        serverVersion: { enabled: commands.serverVersion },
+        // Internal commands which are invoked by the server.
+        applyActionGroup: { enabled: commands.applyActionGroup },
+        applySnippetWorkspaceEdit: { enabled: commands.applySnippetWorkspaceEditCommand },
+        debugSingle: { enabled: commands.debugSingle },
+        gotoLocation: { enabled: commands.gotoLocation },
+        linkToCommand: { enabled: commands.linkToCommand },
+        resolveCodeAction: { enabled: commands.resolveCodeAction },
+        runSingle: { enabled: commands.runSingle },
+        showReferences: { enabled: commands.showReferences },
+    };
 }
diff --git a/src/tools/rust-analyzer/editors/code/src/run.ts b/src/tools/rust-analyzer/editors/code/src/run.ts
index 22e5eda6827..dadaa41b1d1 100644
--- a/src/tools/rust-analyzer/editors/code/src/run.ts
+++ b/src/tools/rust-analyzer/editors/code/src/run.ts
@@ -18,9 +18,9 @@ export async function selectRunnable(
     showButtons: boolean = true
 ): Promise<RunnableQuickPick | undefined> {
     const editor = ctx.activeRustEditor;
-    const client = ctx.client;
-    if (!editor || !client) return;
+    if (!editor) return;
 
+    const client = await ctx.getClient();
     const textDocument: lc.TextDocumentIdentifier = {
         uri: editor.document.uri.toString(),
     };