Skip to content

internal: Properly handle commands in the VSCode client when the server is stopped #13453

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 21, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 61 additions & 19 deletions editors/code/src/ctx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ export type Workspace =
files: vscode.TextDocument[];
};

export type CommandFactory = {
enabled: (ctx: Ctx) => Cmd;
disabled?: (ctx: Ctx) => Cmd;
};

export class Ctx {
readonly statusBar: vscode.StatusBarItem;
readonly config: Config;
Expand All @@ -26,31 +31,40 @@ export class Ctx {
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) {
this.statusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left);
extCtx.subscriptions.push(this.statusBar);
extCtx.subscriptions.push({
dispose() {
this.dispose();
},
});
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();
}

dispose() {
this.config.dispose();
this.statusBar.dispose();
void this.disposeClient();
this.commandDisposables.forEach((disposable) => disposable.dispose());
}

clientFetcher() {
Expand All @@ -63,7 +77,6 @@ export class Ctx {
}

async getClient() {
// if server path changes -> dispose
if (!this.traceOutputChannel) {
this.traceOutputChannel = vscode.window.createOutputChannel(
"Rust Analyzer Language Server Trace"
Expand Down Expand Up @@ -118,7 +131,11 @@ export class Ctx {
initializationOptions,
serverOptions
);
this.client.onNotification(ra.serverStatus, (params) => this.setServerStatus(params));
this.pushClientCleanup(
this.client.onNotification(ra.serverStatus, (params) =>
this.setServerStatus(params)
)
);
}
return this.client;
}
Expand All @@ -127,16 +144,25 @@ export class Ctx {
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 disposeClient() {
log.info("Deactivating language client");
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;
Expand All @@ -159,6 +185,25 @@ export class Ctx {
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;
Expand Down Expand Up @@ -194,16 +239,13 @@ export class Ctx {
statusBar.text = `${icon}rust-analyzer`;
}

registerCommand(name: string, factory: (ctx: Ctx) => Cmd) {
const fullName = `rust-analyzer.${name}`;
const cmd = factory(this);
const d = vscode.commands.registerCommand(fullName, cmd);
this.pushExtCleanup(d);
}

pushExtCleanup(d: Disposable) {
this.extCtx.subscriptions.push(d);
}

private pushClientCleanup(d: Disposable) {
this.clientSubscriptions.push(d);
}
}

export interface Disposable {
Expand Down
159 changes: 75 additions & 84 deletions editors/code/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as vscode from "vscode";
import * as lc from "vscode-languageclient/node";

import * as commands from "./commands";
import { Ctx, Workspace } from "./ctx";
import { CommandFactory, Ctx, Workspace } from "./ctx";
import { isRustDocument } from "./util";
import { activateTaskProvider } from "./tasks";
import { setContextValue } from "./util";
Expand Down Expand Up @@ -57,7 +57,7 @@ export async function activate(
}
: { kind: "Workspace Folder" };

const ctx = new Ctx(context, workspace);
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) => {
Expand All @@ -75,8 +75,6 @@ async function activateServer(ctx: Ctx): Promise<RustAnalyzerExtensionApi> {
ctx.pushExtCleanup(activateTaskProvider(ctx.config));
}

await initCommonContext(ctx);

vscode.workspace.onDidChangeConfiguration(
async (_) => {
await ctx
Expand All @@ -91,85 +89,78 @@ async function activateServer(ctx: Ctx): Promise<RustAnalyzerExtensionApi> {
return ctx.clientFetcher();
}

async function initCommonContext(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" })
);
ctx.pushExtCleanup(defaultOnEnter);

// Commands which invokes manually via command palette, shortcut, etc.
ctx.registerCommand("reload", (_) => 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.disposeClient();
await ctx.activate();
});

ctx.registerCommand("startServer", (_) => async () => {
await ctx.activate();
});
ctx.registerCommand("stopServer", (_) => async () => {
// FIXME: We should re-use the client, that is ctx.deactivate() if none of the configs have changed
await ctx.disposeClient();
ctx.setServerStatus({
health: "ok",
quiescent: true,
message: "server is not running",
});
});
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);

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);
function createCommands(): Record<string, CommandFactory> {
return {
onEnter: {
enabled: commands.onEnter,
disabled: (_) => () => vscode.commands.executeCommand("default:type", { text: "\n" }),
},
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();
},
disabled: (ctx) => async () => {
void vscode.window.showInformationMessage("Reloading rust-analyzer...");
await ctx.activate();
},
},
startServer: {
enabled: (ctx) => async () => {
await ctx.activate();
},
disabled: (ctx) => async () => {
await ctx.activate();
},
},
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",
});
},
},

defaultOnEnter.dispose();
ctx.registerCommand("onEnter", commands.onEnter);
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 },
};
}