From fccf8eb1fd582a4539e6a149fe376685caf43b22 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Sat, 29 Oct 2022 01:28:32 +0200 Subject: [PATCH] Properly handle vscode workspace changes --- editors/code/src/ctx.ts | 73 ++++++++++++++++++++++++++++++++++------ editors/code/src/main.ts | 45 +++++++------------------ 2 files changed, 76 insertions(+), 42 deletions(-) diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index d198d4e938..3e366525ee 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -4,11 +4,15 @@ 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" } | { @@ -19,6 +23,24 @@ 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: CtxInit) => Cmd; disabled?: (ctx: Ctx) => Cmd; @@ -75,6 +97,31 @@ export class Ctx { this.commandDisposables.forEach((disposable) => disposable.dispose()); } + 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(); + } + } + private async getOrCreateClient() { if (this.workspace.kind === "Empty") { return; @@ -143,8 +190,8 @@ export class Ctx { return this._client; } - async activate() { - log.info("Activating language client"); + async start() { + log.info("Starting language client"); const client = await this.getOrCreateClient(); if (!client) { return; @@ -153,13 +200,10 @@ export class Ctx { this.updateCommands(); } - async deactivate() { - if (!this._client) { - return; - } - log.info("Deactivating language client"); - this.updateCommands("disable"); - await this._client.stop(); + 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() { @@ -168,6 +212,15 @@ export class Ctx { } 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(); } diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 54e0c16e5e..e76b657c1b 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -2,8 +2,7 @@ import * as vscode from "vscode"; import * as lc from "vscode-languageclient/node"; import * as commands from "./commands"; -import { CommandFactory, Ctx, Workspace } from "./ctx"; -import { isRustDocument } from "./util"; +import { CommandFactory, Ctx, fetchWorkspace } from "./ctx"; import { activateTaskProvider } from "./tasks"; import { setContextValue } from "./util"; @@ -31,28 +30,7 @@ export async function activate( .then(() => {}, console.error); } - // 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) - const folders = (vscode.workspace.workspaceFolders || []).filter( - (folder) => folder.uri.scheme === "file" - ); - const rustDocuments = vscode.workspace.textDocuments.filter((document) => - isRustDocument(document) - ); - - // FIXME: This can change over time - const workspace: Workspace = - folders.length === 0 - ? rustDocuments.length === 0 - ? { kind: "Empty" } - : { - kind: "Detached Files", - files: rustDocuments, - } - : { kind: "Workspace Folder" }; - - const ctx = new Ctx(context, createCommands(), workspace); + const ctx = new Ctx(context, createCommands(), fetchWorkspace()); // 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) => { @@ -70,6 +48,11 @@ async function activateServer(ctx: Ctx): Promise { ctx.pushExtCleanup(activateTaskProvider(ctx.config)); } + vscode.workspace.onDidChangeWorkspaceFolders( + async (_) => ctx.onWorkspaceFolderChanges(), + null, + ctx.subscriptions + ); vscode.workspace.onDidChangeConfiguration( async (_) => { await ctx.client?.sendNotification("workspace/didChangeConfiguration", { @@ -80,7 +63,7 @@ async function activateServer(ctx: Ctx): Promise { ctx.subscriptions ); - await ctx.activate(); + await ctx.start(); return ctx; } @@ -93,27 +76,25 @@ function createCommands(): Record { 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(); + await ctx.restart(); }, disabled: (ctx) => async () => { void vscode.window.showInformationMessage("Reloading rust-analyzer..."); - await ctx.activate(); + await ctx.start(); }, }, startServer: { enabled: (ctx) => async () => { - await ctx.activate(); + await ctx.start(); }, disabled: (ctx) => async () => { - await ctx.activate(); + await ctx.start(); }, }, 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(); + await ctx.stopAndDispose(); ctx.setServerStatus({ health: "stopped", });