From 2071d00fd25cee25fe881255d78e478f72749c7d Mon Sep 17 00:00:00 2001
From: Lukas Wirth <lukastw97@gmail.com>
Date: Sat, 29 Oct 2022 00:44:37 +0200
Subject: [PATCH] Always set up VSCode commands

---
 editors/code/src/commands.ts | 129 +++++++++++++++++------------------
 editors/code/src/ctx.ts      | 102 ++++++++++++++++-----------
 editors/code/src/main.ts     |  30 ++++----
 editors/code/src/run.ts      |   6 +-
 4 files changed, 139 insertions(+), 128 deletions(-)

diff --git a/editors/code/src/commands.ts b/editors/code/src/commands.ts
index 12ceb4f2df8..312087e4cff 100644
--- a/editors/code/src/commands.ts
+++ b/editors/code/src/commands.ts
@@ -3,7 +3,7 @@ import * as lc from "vscode-languageclient";
 import * as ra from "./lsp_ext";
 import * as path from "path";
 
-import { Ctx, Cmd } from "./ctx";
+import { Ctx, Cmd, CtxInit } from "./ctx";
 import { applySnippetWorkspaceEdit, applySnippetTextEdits } from "./snippets";
 import { spawnSync } from "child_process";
 import { RunnableQuickPick, selectRunnable, createTask, createArgs } from "./run";
@@ -16,14 +16,14 @@ import { LINKED_COMMANDS } from "./client";
 export * from "./ast_inspector";
 export * from "./run";
 
-export function analyzerStatus(ctx: Ctx): Cmd {
+export function analyzerStatus(ctx: CtxInit): Cmd {
     const tdcp = new (class implements vscode.TextDocumentContentProvider {
         readonly uri = vscode.Uri.parse("rust-analyzer-status://status");
         readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
 
         async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
             if (!vscode.window.activeTextEditor) return "";
-            const client = await ctx.getClient();
+            const client = ctx.client;
 
             const params: ra.AnalyzerStatusParams = {};
             const doc = ctx.activeRustEditor?.document;
@@ -52,7 +52,7 @@ export function analyzerStatus(ctx: Ctx): Cmd {
     };
 }
 
-export function memoryUsage(ctx: Ctx): Cmd {
+export function memoryUsage(ctx: CtxInit): Cmd {
     const tdcp = new (class implements vscode.TextDocumentContentProvider {
         readonly uri = vscode.Uri.parse("rust-analyzer-memory://memory");
         readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
@@ -60,14 +60,9 @@ export function memoryUsage(ctx: Ctx): Cmd {
         provideTextDocumentContent(_uri: vscode.Uri): vscode.ProviderResult<string> {
             if (!vscode.window.activeTextEditor) return "";
 
-            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)"
-                    );
-                });
+            return ctx.client.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> {
@@ -86,18 +81,18 @@ export function memoryUsage(ctx: Ctx): Cmd {
     };
 }
 
-export function shuffleCrateGraph(ctx: Ctx): Cmd {
+export function shuffleCrateGraph(ctx: CtxInit): Cmd {
     return async () => {
-        return ctx.getClient().then((it) => it.sendRequest(ra.shuffleCrateGraph));
+        return ctx.client.sendRequest(ra.shuffleCrateGraph);
     };
 }
 
-export function matchingBrace(ctx: Ctx): Cmd {
+export function matchingBrace(ctx: CtxInit): Cmd {
     return async () => {
         const editor = ctx.activeRustEditor;
         if (!editor) return;
 
-        const client = await ctx.getClient();
+        const client = ctx.client;
 
         const response = await client.sendRequest(ra.matchingBrace, {
             textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
@@ -114,12 +109,12 @@ export function matchingBrace(ctx: Ctx): Cmd {
     };
 }
 
-export function joinLines(ctx: Ctx): Cmd {
+export function joinLines(ctx: CtxInit): Cmd {
     return async () => {
         const editor = ctx.activeRustEditor;
         if (!editor) return;
 
-        const client = await ctx.getClient();
+        const client = ctx.client;
 
         const items: lc.TextEdit[] = await client.sendRequest(ra.joinLines, {
             ranges: editor.selections.map((it) => client.code2ProtocolConverter.asRange(it)),
@@ -134,19 +129,19 @@ export function joinLines(ctx: Ctx): Cmd {
     };
 }
 
-export function moveItemUp(ctx: Ctx): Cmd {
+export function moveItemUp(ctx: CtxInit): Cmd {
     return moveItem(ctx, ra.Direction.Up);
 }
 
-export function moveItemDown(ctx: Ctx): Cmd {
+export function moveItemDown(ctx: CtxInit): Cmd {
     return moveItem(ctx, ra.Direction.Down);
 }
 
-export function moveItem(ctx: Ctx, direction: ra.Direction): Cmd {
+export function moveItem(ctx: CtxInit, direction: ra.Direction): Cmd {
     return async () => {
         const editor = ctx.activeRustEditor;
         if (!editor) return;
-        const client = await ctx.getClient();
+        const client = ctx.client;
 
         const lcEdits = await client.sendRequest(ra.moveItem, {
             range: client.code2ProtocolConverter.asRange(editor.selection),
@@ -161,13 +156,13 @@ export function moveItem(ctx: Ctx, direction: ra.Direction): Cmd {
     };
 }
 
-export function onEnter(ctx: Ctx): Cmd {
+export function onEnter(ctx: CtxInit): Cmd {
     async function handleKeypress() {
         const editor = ctx.activeRustEditor;
 
         if (!editor) return false;
 
-        const client = await ctx.getClient();
+        const client = ctx.client;
         const lcEdits = await client
             .sendRequest(ra.onEnter, {
                 textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
@@ -193,13 +188,13 @@ export function onEnter(ctx: Ctx): Cmd {
     };
 }
 
-export function parentModule(ctx: Ctx): Cmd {
+export function parentModule(ctx: CtxInit): Cmd {
     return async () => {
         const editor = vscode.window.activeTextEditor;
         if (!editor) return;
         if (!(isRustDocument(editor.document) || isCargoTomlDocument(editor.document))) return;
 
-        const client = await ctx.getClient();
+        const client = ctx.client;
 
         const locations = await client.sendRequest(ra.parentModule, {
             textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
@@ -230,12 +225,12 @@ export function parentModule(ctx: Ctx): Cmd {
     };
 }
 
-export function openCargoToml(ctx: Ctx): Cmd {
+export function openCargoToml(ctx: CtxInit): Cmd {
     return async () => {
         const editor = ctx.activeRustEditor;
         if (!editor) return;
 
-        const client = await ctx.getClient();
+        const client = ctx.client;
         const response = await client.sendRequest(ra.openCargoToml, {
             textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(editor.document),
         });
@@ -251,12 +246,12 @@ export function openCargoToml(ctx: Ctx): Cmd {
     };
 }
 
-export function ssr(ctx: Ctx): Cmd {
+export function ssr(ctx: CtxInit): Cmd {
     return async () => {
         const editor = vscode.window.activeTextEditor;
         if (!editor) return;
 
-        const client = await ctx.getClient();
+        const client = ctx.client;
 
         const position = editor.selection.active;
         const selections = editor.selections;
@@ -308,7 +303,7 @@ export function ssr(ctx: Ctx): Cmd {
     };
 }
 
-export function serverVersion(ctx: Ctx): Cmd {
+export function serverVersion(ctx: CtxInit): Cmd {
     return async () => {
         if (!ctx.serverPath) {
             void vscode.window.showWarningMessage(`rust-analyzer server is not running`);
@@ -324,7 +319,7 @@ export function serverVersion(ctx: Ctx): Cmd {
 // Opens the virtual file that will show the syntax tree
 //
 // The contents of the file come from the `TextDocumentContentProvider`
-export function syntaxTree(ctx: Ctx): Cmd {
+export function syntaxTree(ctx: CtxInit): Cmd {
     const tdcp = new (class implements vscode.TextDocumentContentProvider {
         readonly uri = vscode.Uri.parse("rust-analyzer-syntax-tree://syntaxtree/tree.rast");
         readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
@@ -360,7 +355,7 @@ export function syntaxTree(ctx: Ctx): Cmd {
         ): Promise<string> {
             const rustEditor = ctx.activeRustEditor;
             if (!rustEditor) return "";
-            const client = await ctx.getClient();
+            const client = ctx.client;
 
             // When the range based query is enabled we take the range of the selection
             const range =
@@ -407,7 +402,7 @@ export function syntaxTree(ctx: Ctx): Cmd {
 // Opens the virtual file that will show the HIR of the function containing the cursor position
 //
 // The contents of the file come from the `TextDocumentContentProvider`
-export function viewHir(ctx: Ctx): Cmd {
+export function viewHir(ctx: CtxInit): Cmd {
     const tdcp = new (class implements vscode.TextDocumentContentProvider {
         readonly uri = vscode.Uri.parse("rust-analyzer-hir://viewHir/hir.rs");
         readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
@@ -444,7 +439,7 @@ export function viewHir(ctx: Ctx): Cmd {
             const rustEditor = ctx.activeRustEditor;
             if (!rustEditor) return "";
 
-            const client = await ctx.getClient();
+            const client = ctx.client;
             const params = {
                 textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
                     rustEditor.document
@@ -473,7 +468,7 @@ export function viewHir(ctx: Ctx): Cmd {
     };
 }
 
-export function viewFileText(ctx: Ctx): Cmd {
+export function viewFileText(ctx: CtxInit): Cmd {
     const tdcp = new (class implements vscode.TextDocumentContentProvider {
         readonly uri = vscode.Uri.parse("rust-analyzer-file-text://viewFileText/file.rs");
         readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
@@ -509,7 +504,7 @@ export function viewFileText(ctx: Ctx): Cmd {
         ): Promise<string> {
             const rustEditor = ctx.activeRustEditor;
             if (!rustEditor) return "";
-            const client = await ctx.getClient();
+            const client = ctx.client;
 
             const params = client.code2ProtocolConverter.asTextDocumentIdentifier(
                 rustEditor.document
@@ -536,7 +531,7 @@ export function viewFileText(ctx: Ctx): Cmd {
     };
 }
 
-export function viewItemTree(ctx: Ctx): Cmd {
+export function viewItemTree(ctx: CtxInit): Cmd {
     const tdcp = new (class implements vscode.TextDocumentContentProvider {
         readonly uri = vscode.Uri.parse("rust-analyzer-item-tree://viewItemTree/itemtree.rs");
         readonly eventEmitter = new vscode.EventEmitter<vscode.Uri>();
@@ -572,7 +567,7 @@ export function viewItemTree(ctx: Ctx): Cmd {
         ): Promise<string> {
             const rustEditor = ctx.activeRustEditor;
             if (!rustEditor) return "";
-            const client = await ctx.getClient();
+            const client = ctx.client;
 
             const params = {
                 textDocument: client.code2ProtocolConverter.asTextDocumentIdentifier(
@@ -601,7 +596,7 @@ export function viewItemTree(ctx: Ctx): Cmd {
     };
 }
 
-function crateGraph(ctx: Ctx, full: boolean): Cmd {
+function crateGraph(ctx: CtxInit, full: boolean): Cmd {
     return async () => {
         const nodeModulesPath = vscode.Uri.file(path.join(ctx.extensionPath, "node_modules"));
 
@@ -618,7 +613,7 @@ function crateGraph(ctx: Ctx, full: boolean): Cmd {
         const params = {
             full: full,
         };
-        const client = await ctx.getClient();
+        const client = ctx.client;
         const dot = await client.sendRequest(ra.viewCrateGraph, params);
         const uri = panel.webview.asWebviewUri(nodeModulesPath);
 
@@ -664,18 +659,18 @@ function crateGraph(ctx: Ctx, full: boolean): Cmd {
     };
 }
 
-export function viewCrateGraph(ctx: Ctx): Cmd {
+export function viewCrateGraph(ctx: CtxInit): Cmd {
     return crateGraph(ctx, false);
 }
 
-export function viewFullCrateGraph(ctx: Ctx): Cmd {
+export function viewFullCrateGraph(ctx: CtxInit): Cmd {
     return crateGraph(ctx, true);
 }
 
 // Opens the virtual file that will show the syntax tree
 //
 // The contents of the file come from the `TextDocumentContentProvider`
-export function expandMacro(ctx: Ctx): Cmd {
+export function expandMacro(ctx: CtxInit): Cmd {
     function codeFormat(expanded: ra.ExpandedMacro): string {
         let result = `// Recursive expansion of ${expanded.name}! macro\n`;
         result += "// " + "=".repeat(result.length - 3);
@@ -691,7 +686,7 @@ export function expandMacro(ctx: Ctx): Cmd {
         async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
             const editor = vscode.window.activeTextEditor;
             if (!editor) return "";
-            const client = await ctx.getClient();
+            const client = ctx.client;
 
             const position = editor.selection.active;
 
@@ -723,8 +718,8 @@ export function expandMacro(ctx: Ctx): Cmd {
     };
 }
 
-export function reloadWorkspace(ctx: Ctx): Cmd {
-    return async () => (await ctx.getClient()).sendRequest(ra.reloadWorkspace);
+export function reloadWorkspace(ctx: CtxInit): Cmd {
+    return async () => ctx.client.sendRequest(ra.reloadWorkspace);
 }
 
 async function showReferencesImpl(
@@ -743,13 +738,13 @@ async function showReferencesImpl(
     }
 }
 
-export function showReferences(ctx: Ctx): Cmd {
+export function showReferences(ctx: CtxInit): Cmd {
     return async (uri: string, position: lc.Position, locations: lc.Location[]) => {
-        await showReferencesImpl(await ctx.getClient(), uri, position, locations);
+        await showReferencesImpl(ctx.client, uri, position, locations);
     };
 }
 
-export function applyActionGroup(_ctx: Ctx): Cmd {
+export function applyActionGroup(_ctx: CtxInit): Cmd {
     return async (actions: { label: string; arguments: lc.CodeAction }[]) => {
         const selectedAction = await vscode.window.showQuickPick(actions);
         if (!selectedAction) return;
@@ -760,9 +755,9 @@ export function applyActionGroup(_ctx: Ctx): Cmd {
     };
 }
 
-export function gotoLocation(ctx: Ctx): Cmd {
+export function gotoLocation(ctx: CtxInit): Cmd {
     return async (locationLink: lc.LocationLink) => {
-        const client = await ctx.getClient();
+        const client = ctx.client;
         const uri = client.protocol2CodeConverter.asUri(locationLink.targetUri);
         let range = client.protocol2CodeConverter.asRange(locationLink.targetSelectionRange);
         // collapse the range to a cursor position
@@ -772,13 +767,13 @@ export function gotoLocation(ctx: Ctx): Cmd {
     };
 }
 
-export function openDocs(ctx: Ctx): Cmd {
+export function openDocs(ctx: CtxInit): Cmd {
     return async () => {
         const editor = vscode.window.activeTextEditor;
         if (!editor) {
             return;
         }
-        const client = await ctx.getClient();
+        const client = ctx.client;
 
         const position = editor.selection.active;
         const textDocument = { uri: editor.document.uri.toString() };
@@ -791,16 +786,16 @@ export function openDocs(ctx: Ctx): Cmd {
     };
 }
 
-export function cancelFlycheck(ctx: Ctx): Cmd {
+export function cancelFlycheck(ctx: CtxInit): Cmd {
     return async () => {
-        const client = await ctx.getClient();
+        const client = ctx.client;
         await client.sendRequest(ra.cancelFlycheck);
     };
 }
 
-export function resolveCodeAction(ctx: Ctx): Cmd {
+export function resolveCodeAction(ctx: CtxInit): Cmd {
     return async (params: lc.CodeAction) => {
-        const client = await ctx.getClient();
+        const client = ctx.client;
         params.command = undefined;
         const item = await client?.sendRequest(lc.CodeActionResolveRequest.type, params);
         if (!item?.edit) {
@@ -825,13 +820,13 @@ export function resolveCodeAction(ctx: Ctx): Cmd {
     };
 }
 
-export function applySnippetWorkspaceEditCommand(_ctx: Ctx): Cmd {
+export function applySnippetWorkspaceEditCommand(_ctx: CtxInit): Cmd {
     return async (edit: vscode.WorkspaceEdit) => {
         await applySnippetWorkspaceEdit(edit);
     };
 }
 
-export function run(ctx: Ctx): Cmd {
+export function run(ctx: CtxInit): Cmd {
     let prevRunnable: RunnableQuickPick | undefined;
 
     return async () => {
@@ -845,11 +840,11 @@ export function run(ctx: Ctx): Cmd {
     };
 }
 
-export function peekTests(ctx: Ctx): Cmd {
+export function peekTests(ctx: CtxInit): Cmd {
     return async () => {
         const editor = ctx.activeRustEditor;
         if (!editor) return;
-        const client = await ctx.getClient();
+        const client = ctx.client;
 
         await vscode.window.withProgress(
             {
@@ -878,7 +873,7 @@ export function peekTests(ctx: Ctx): Cmd {
     };
 }
 
-export function runSingle(ctx: Ctx): Cmd {
+export function runSingle(ctx: CtxInit): Cmd {
     return async (runnable: ra.Runnable) => {
         const editor = ctx.activeRustEditor;
         if (!editor) return;
@@ -895,7 +890,7 @@ export function runSingle(ctx: Ctx): Cmd {
     };
 }
 
-export function copyRunCommandLine(ctx: Ctx) {
+export function copyRunCommandLine(ctx: CtxInit) {
     let prevRunnable: RunnableQuickPick | undefined;
     return async () => {
         const item = await selectRunnable(ctx, prevRunnable);
@@ -907,7 +902,7 @@ export function copyRunCommandLine(ctx: Ctx) {
     };
 }
 
-export function debug(ctx: Ctx): Cmd {
+export function debug(ctx: CtxInit): Cmd {
     let prevDebuggee: RunnableQuickPick | undefined;
 
     return async () => {
@@ -920,13 +915,13 @@ export function debug(ctx: Ctx): Cmd {
     };
 }
 
-export function debugSingle(ctx: Ctx): Cmd {
+export function debugSingle(ctx: CtxInit): Cmd {
     return async (config: ra.Runnable) => {
         await startDebugSession(ctx, config);
     };
 }
 
-export function newDebugConfig(ctx: Ctx): Cmd {
+export function newDebugConfig(ctx: CtxInit): Cmd {
     return async () => {
         const item = await selectRunnable(ctx, undefined, true, false);
         if (!item) return;
diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts
index 8592950e102..d198d4e9383 100644
--- a/editors/code/src/ctx.ts
+++ b/editors/code/src/ctx.ts
@@ -10,6 +10,7 @@ import { PersistentState } from "./persistent_state";
 import { bootstrap } from "./bootstrap";
 
 export type Workspace =
+    | { kind: "Empty" }
     | {
           kind: "Workspace Folder";
       }
@@ -19,15 +20,20 @@ export type Workspace =
       };
 
 export type CommandFactory = {
-    enabled: (ctx: Ctx) => Cmd;
+    enabled: (ctx: CtxInit) => Cmd;
     disabled?: (ctx: Ctx) => Cmd;
 };
 
+export type CtxInit = Ctx & {
+    readonly client: lc.LanguageClient;
+};
+
 export class Ctx {
     readonly statusBar: vscode.StatusBarItem;
     readonly config: Config;
+    readonly workspace: Workspace;
 
-    private client: lc.LanguageClient | undefined;
+    private _client: lc.LanguageClient | undefined;
     private _serverPath: string | undefined;
     private traceOutputChannel: vscode.OutputChannel | undefined;
     private outputChannel: vscode.OutputChannel | undefined;
@@ -36,18 +42,17 @@ export class Ctx {
     private commandFactories: Record<string, CommandFactory>;
     private commandDisposables: Disposable[];
 
-    workspace: Workspace;
+    get client() {
+        return this._client;
+    }
 
     constructor(
         readonly extCtx: vscode.ExtensionContext,
-        workspace: Workspace,
-        commandFactories: Record<string, CommandFactory>
+        commandFactories: Record<string, CommandFactory>,
+        workspace: Workspace
     ) {
         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 = [];
@@ -57,7 +62,10 @@ export class Ctx {
         this.state = new PersistentState(extCtx.globalState);
         this.config = new Config(extCtx);
 
-        this.updateCommands();
+        this.updateCommands("disable");
+        this.setServerStatus({
+            health: "stopped",
+        });
     }
 
     dispose() {
@@ -67,16 +75,11 @@ export class Ctx {
         this.commandDisposables.forEach((disposable) => disposable.dispose());
     }
 
-    clientFetcher() {
-        const self = this;
-        return {
-            get client(): lc.LanguageClient | undefined {
-                return self.client;
-            },
-        };
-    }
+    private async getOrCreateClient() {
+        if (this.workspace.kind === "Empty") {
+            return;
+        }
 
-    async getClient() {
         if (!this.traceOutputChannel) {
             this.traceOutputChannel = vscode.window.createOutputChannel(
                 "Rust Analyzer Language Server Trace"
@@ -88,7 +91,7 @@ export class Ctx {
             this.pushExtCleanup(this.outputChannel);
         }
 
-        if (!this.client) {
+        if (!this._client) {
             this._serverPath = await bootstrap(this.extCtx, this.config, this.state).catch(
                 (err) => {
                     let message = "bootstrap error. ";
@@ -125,47 +128,55 @@ export class Ctx {
 
             const initializationOptions = substituteVSCodeVariables(rawInitializationOptions);
 
-            this.client = await createClient(
+            this._client = await createClient(
                 this.traceOutputChannel,
                 this.outputChannel,
                 initializationOptions,
                 serverOptions
             );
             this.pushClientCleanup(
-                this.client.onNotification(ra.serverStatus, (params) =>
+                this._client.onNotification(ra.serverStatus, (params) =>
                     this.setServerStatus(params)
                 )
             );
         }
-        return this.client;
+        return this._client;
     }
 
     async activate() {
         log.info("Activating language client");
-        const client = await this.getClient();
+        const client = await this.getOrCreateClient();
+        if (!client) {
+            return;
+        }
         await client.start();
         this.updateCommands();
-        return client;
     }
 
     async deactivate() {
+        if (!this._client) {
+            return;
+        }
         log.info("Deactivating language client");
-        await this.client?.stop();
-        this.updateCommands();
+        this.updateCommands("disable");
+        await this._client.stop();
     }
 
     async stop() {
+        if (!this._client) {
+            return;
+        }
         log.info("Stopping language client");
+        this.updateCommands("disable");
         await this.disposeClient();
-        this.updateCommands();
     }
 
     private async disposeClient() {
         this.clientSubscriptions?.forEach((disposable) => disposable.dispose());
         this.clientSubscriptions = [];
-        await this.client?.dispose();
+        await this._client?.dispose();
         this._serverPath = undefined;
-        this.client = undefined;
+        this._client = undefined;
     }
 
     get activeRustEditor(): RustEditor | undefined {
@@ -185,21 +196,30 @@ export class Ctx {
         return this._serverPath;
     }
 
-    private updateCommands() {
+    private updateCommands(forceDisable?: "disable") {
         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`
-                          ));
+
+        const clientRunning = (!forceDisable && this._client?.isRunning()) ?? false;
+        const isClientRunning = function (_ctx: Ctx): _ctx is CtxInit {
+            return clientRunning;
         };
+
         for (const [name, factory] of Object.entries(this.commandFactories)) {
             const fullName = `rust-analyzer.${name}`;
-            const callback = fetchFactory(factory, fullName)(this);
+            let callback;
+            if (isClientRunning(this)) {
+                // we asserted that `client` is defined
+                callback = factory.enabled(this);
+            } else if (factory.disabled) {
+                callback = factory.disabled(this);
+            } else {
+                callback = () =>
+                    vscode.window.showErrorMessage(
+                        `command ${fullName} failed: rust-analyzer server is not running`
+                    );
+            }
+
             this.commandDisposables.push(vscode.commands.registerCommand(fullName, callback));
         }
     }
@@ -209,7 +229,7 @@ export class Ctx {
         const statusBar = this.statusBar;
         switch (status.health) {
             case "ok":
-                statusBar.tooltip = (status.message ?? "Ready") + "Click to stop.";
+                statusBar.tooltip = (status.message ?? "Ready") + "\nClick to stop server.";
                 statusBar.command = "rust-analyzer.stopServer";
                 statusBar.color = undefined;
                 statusBar.backgroundColor = undefined;
@@ -235,7 +255,7 @@ export class Ctx {
                 icon = "$(error) ";
                 break;
             case "stopped":
-                statusBar.tooltip = "Server is stopped. Click to start.";
+                statusBar.tooltip = "Server is stopped.\nClick to start.";
                 statusBar.command = "rust-analyzer.startServer";
                 statusBar.color = undefined;
                 statusBar.backgroundColor = undefined;
diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts
index 59705334726..54e0c16e5e0 100644
--- a/editors/code/src/main.ts
+++ b/editors/code/src/main.ts
@@ -10,7 +10,6 @@ import { setContextValue } from "./util";
 const RUST_PROJECT_CONTEXT_NAME = "inRustProject";
 
 export interface RustAnalyzerExtensionApi {
-    // FIXME: this should be non-optional
     readonly client?: lc.LanguageClient;
 }
 
@@ -42,22 +41,18 @@ export async function activate(
         isRustDocument(document)
     );
 
-    if (folders.length === 0 && rustDocuments.length === 0) {
-        // FIXME: Ideally we would choose not to activate at all (and avoid registering
-        // non-functional editor commands), but VS Code doesn't seem to have a good way of doing
-        // that
-        return {};
-    }
-
+    // FIXME: This can change over time
     const workspace: Workspace =
         folders.length === 0
-            ? {
-                  kind: "Detached Files",
-                  files: rustDocuments,
-              }
+            ? rustDocuments.length === 0
+                ? { kind: "Empty" }
+                : {
+                      kind: "Detached Files",
+                      files: rustDocuments,
+                  }
             : { kind: "Workspace Folder" };
 
-    const ctx = new Ctx(context, workspace, createCommands());
+    const ctx = new Ctx(context, createCommands(), workspace);
     // 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) => {
@@ -77,16 +72,16 @@ async function activateServer(ctx: Ctx): Promise<RustAnalyzerExtensionApi> {
 
     vscode.workspace.onDidChangeConfiguration(
         async (_) => {
-            await ctx
-                .clientFetcher()
-                .client?.sendNotification("workspace/didChangeConfiguration", { settings: "" });
+            await ctx.client?.sendNotification("workspace/didChangeConfiguration", {
+                settings: "",
+            });
         },
         null,
         ctx.subscriptions
     );
 
     await ctx.activate();
-    return ctx.clientFetcher();
+    return ctx;
 }
 
 function createCommands(): Record<string, CommandFactory> {
@@ -123,6 +118,7 @@ function createCommands(): Record<string, CommandFactory> {
                     health: "stopped",
                 });
             },
+            disabled: (_) => async () => {},
         },
 
         analyzerStatus: { enabled: commands.analyzerStatus },
diff --git a/editors/code/src/run.ts b/editors/code/src/run.ts
index dadaa41b1d1..35627e2fc6b 100644
--- a/editors/code/src/run.ts
+++ b/editors/code/src/run.ts
@@ -3,7 +3,7 @@ import * as lc from "vscode-languageclient";
 import * as ra from "./lsp_ext";
 import * as tasks from "./tasks";
 
-import { Ctx } from "./ctx";
+import { CtxInit } from "./ctx";
 import { makeDebugConfig } from "./debug";
 import { Config, RunnableEnvCfg } from "./config";
 
@@ -12,7 +12,7 @@ const quickPickButtons = [
 ];
 
 export async function selectRunnable(
-    ctx: Ctx,
+    ctx: CtxInit,
     prevRunnable?: RunnableQuickPick,
     debuggeeOnly = false,
     showButtons: boolean = true
@@ -20,7 +20,7 @@ export async function selectRunnable(
     const editor = ctx.activeRustEditor;
     if (!editor) return;
 
-    const client = await ctx.getClient();
+    const client = ctx.client;
     const textDocument: lc.TextDocumentIdentifier = {
         uri: editor.document.uri.toString(),
     };