]> git.lizzy.rs Git - rust.git/blobdiff - src/tools/rust-analyzer/editors/code/src/ctx.ts
:arrow_up: rust-analyzer
[rust.git] / src / tools / rust-analyzer / editors / code / src / ctx.ts
index 044a9470aa94782748ced04c732f59aa7412da0e..3e366525ee295392fcdef55b019aa6d511ba5bea 100644 (file)
@@ -4,12 +4,17 @@ import * as ra from "./lsp_ext";
 
 import { Config, substituteVariablesInEnv, substituteVSCodeVariables } from "./config";
 import { createClient } from "./client";
-import { isRustEditor, log, RustEditor } from "./util";
+import { isRustDocument, isRustEditor, log, RustEditor } from "./util";
 import { ServerStatusParams } from "./lsp_ext";
 import { PersistentState } from "./persistent_state";
 import { bootstrap } from "./bootstrap";
 
+// We only support local folders, not eg. Live Share (`vlsl:` scheme), so don't activate if
+// only those are in use. We use "Empty" to represent these scenarios
+// (r-a still somewhat works with Live Share, because commands are tunneled to the host)
+
 export type Workspace =
+    | { kind: "Empty" }
     | {
           kind: "Workspace Folder";
       }
@@ -18,16 +23,39 @@ export type Workspace =
           files: vscode.TextDocument[];
       };
 
+export function fetchWorkspace(): Workspace {
+    const folders = (vscode.workspace.workspaceFolders || []).filter(
+        (folder) => folder.uri.scheme === "file"
+    );
+    const rustDocuments = vscode.workspace.textDocuments.filter((document) =>
+        isRustDocument(document)
+    );
+
+    return folders.length === 0
+        ? rustDocuments.length === 0
+            ? { kind: "Empty" }
+            : {
+                  kind: "Detached Files",
+                  files: rustDocuments,
+              }
+        : { kind: "Workspace Folder" };
+}
+
 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 +64,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 +84,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 +97,36 @@ export class Ctx {
         this.commandDisposables.forEach((disposable) => disposable.dispose());
     }
 
-    clientFetcher() {
-        const self = this;
-        return {
-            get client(): lc.LanguageClient | undefined {
-                return self.client;
-            },
-        };
+    async onWorkspaceFolderChanges() {
+        const workspace = fetchWorkspace();
+        if (workspace.kind === "Detached Files" && this.workspace.kind === "Detached Files") {
+            if (workspace.files !== this.workspace.files) {
+                if (this.client?.isRunning()) {
+                    // Ideally we wouldn't need to tear down the server here, but currently detached files
+                    // are only specified at server start
+                    await this.stopAndDispose();
+                    await this.start();
+                }
+                return;
+            }
+        }
+        if (workspace.kind === "Workspace Folder" && this.workspace.kind === "Workspace Folder") {
+            return;
+        }
+        if (workspace.kind === "Empty") {
+            await this.stopAndDispose();
+            return;
+        }
+        if (this.client?.isRunning()) {
+            await this.restart();
+        }
     }
 
-    async getClient() {
+    private async getOrCreateClient() {
+        if (this.workspace.kind === "Empty") {
+            return;
+        }
+
         if (!this.traceOutputChannel) {
             this.traceOutputChannel = vscode.window.createOutputChannel(
                 "Rust Analyzer Language Server Trace"
@@ -88,7 +138,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 +175,61 @@ 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();
+    async start() {
+        log.info("Starting language client");
+        const client = await this.getOrCreateClient();
+        if (!client) {
+            return;
+        }
         await client.start();
         this.updateCommands();
-        return client;
     }
 
-    async deactivate() {
-        log.info("Deactivating language client");
-        await this.client?.stop();
-        this.updateCommands();
+    async restart() {
+        // FIXME: We should re-use the client, that is ctx.deactivate() if none of the configs have changed
+        await this.stopAndDispose();
+        await this.start();
     }
 
     async stop() {
+        if (!this._client) {
+            return;
+        }
         log.info("Stopping language client");
+        this.updateCommands("disable");
+        await this._client.stop();
+    }
+
+    async stopAndDispose() {
+        if (!this._client) {
+            return;
+        }
+        log.info("Disposing 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,32 +249,41 @@ 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));
         }
     }
 
-    setServerStatus(status: ServerStatusParams) {
+    setServerStatus(status: ServerStatusParams | { health: "stopped" }) {
         let icon = "";
         const statusBar = this.statusBar;
         switch (status.health) {
             case "ok":
-                statusBar.tooltip = status.message ?? "Ready";
-                statusBar.command = undefined;
+                statusBar.tooltip = (status.message ?? "Ready") + "\nClick to stop server.";
+                statusBar.command = "rust-analyzer.stopServer";
                 statusBar.color = undefined;
                 statusBar.backgroundColor = undefined;
                 break;
@@ -234,6 +307,13 @@ export class Ctx {
                 statusBar.backgroundColor = new vscode.ThemeColor("statusBarItem.errorBackground");
                 icon = "$(error) ";
                 break;
+            case "stopped":
+                statusBar.tooltip = "Server is stopped.\nClick to start.";
+                statusBar.command = "rust-analyzer.startServer";
+                statusBar.color = undefined;
+                statusBar.backgroundColor = undefined;
+                statusBar.text = `$(stop-circle) rust-analyzer`;
+                return;
         }
         if (!status.quiescent) icon = "$(sync~spin) ";
         statusBar.text = `${icon}rust-analyzer`;