Refactor server lifecycle

This commit is contained in:
Aleksey Kladov 2019-12-31 18:14:00 +01:00
parent 0849f7001c
commit 087af54069
12 changed files with 216 additions and 199 deletions

View file

@ -0,0 +1,90 @@
import { homedir } from 'os';
import * as lc from 'vscode-languageclient';
import { window, workspace } from 'vscode';
import { Config } from './config';
export function createClient(config: Config): 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.
let folder: string = '.';
if (workspace.workspaceFolders !== undefined) {
folder = workspace.workspaceFolders[0].uri.fsPath.toString();
}
const command = expandPathResolving(config.raLspServerPath);
const run: lc.Executable = {
command,
options: { cwd: folder },
};
const serverOptions: lc.ServerOptions = {
run,
debug: run,
};
const traceOutputChannel = window.createOutputChannel(
'Rust Analyzer Language Server Trace',
);
const clientOptions: lc.LanguageClientOptions = {
documentSelector: [{ scheme: 'file', language: 'rust' }],
initializationOptions: {
publishDecorations: true,
lruCapacity: config.lruCapacity,
maxInlayHintLength: config.maxInlayHintLength,
cargoWatchEnable: config.cargoWatchOptions.enable,
cargoWatchArgs: config.cargoWatchOptions.arguments,
cargoWatchCommand: config.cargoWatchOptions.command,
cargoWatchAllTargets:
config.cargoWatchOptions.allTargets,
excludeGlobs: config.excludeGlobs,
useClientWatching: config.useClientWatching,
featureFlags: config.featureFlags,
withSysroot: config.withSysroot,
cargoFeatures: config.cargoFeatures,
},
traceOutputChannel,
};
const res = new lc.LanguageClient(
'rust-analyzer',
'Rust Analyzer Language Server',
serverOptions,
clientOptions,
);
// HACK: This is an awful way of filtering out the decorations notifications
// However, pending proper support, this is the most effecitve approach
// Proper support for this would entail a change to vscode-languageclient to allow not notifying on certain messages
// Or the ability to disable the serverside component of highlighting (but this means that to do tracing we need to disable hihlighting)
// This also requires considering our settings strategy, which is work which needs doing
// @ts-ignore The tracer is private to vscode-languageclient, but we need access to it to not log publishDecorations requests
res._tracer = {
log: (messageOrDataObject: string | any, data?: string) => {
if (typeof messageOrDataObject === 'string') {
if (
messageOrDataObject.includes(
'rust-analyzer/publishDecorations',
) ||
messageOrDataObject.includes(
'rust-analyzer/decorationsRequest',
)
) {
// Don't log publish decorations requests
} else {
// @ts-ignore This is just a utility function
res.logTrace(messageOrDataObject, data);
}
} else {
// @ts-ignore
res.logObjectTrace(messageOrDataObject);
}
},
};
res.registerProposedFeatures()
return res;
}
function expandPathResolving(path: string) {
if (path.startsWith('~/')) {
return path.replace('~', homedir());
}
return path;
}

View file

@ -49,9 +49,10 @@ class TextDocumentContentProvider
_uri: vscode.Uri,
): vscode.ProviderResult<string> {
const editor = vscode.window.activeTextEditor;
if (editor == null) return '';
const client = this.ctx.client
if (!editor || !client) return '';
return this.ctx.client.sendRequest<string>(
return client.sendRequest<string>(
'rust-analyzer/analyzerStatus',
null,
);

View file

@ -52,14 +52,15 @@ class TextDocumentContentProvider
async provideTextDocumentContent(_uri: vscode.Uri): Promise<string> {
const editor = vscode.window.activeTextEditor;
if (editor == null) return '';
const client = this.ctx.client
if (!editor || !client) return '';
const position = editor.selection.active;
const request: lc.TextDocumentPositionParams = {
textDocument: { uri: editor.document.uri.toString() },
position,
};
const expanded = await this.ctx.client.sendRequest<ExpandedMacro>(
const expanded = await client.sendRequest<ExpandedMacro>(
'rust-analyzer/expandMacro',
request,
);

View file

@ -15,18 +15,21 @@ import { run, runSingle } from './runnables';
function collectGarbage(ctx: Ctx): Cmd {
return async () => {
ctx.client.sendRequest<null>('rust-analyzer/collectGarbage', null);
ctx.client?.sendRequest<null>('rust-analyzer/collectGarbage', null);
};
}
function showReferences(ctx: Ctx): Cmd {
return (uri: string, position: lc.Position, locations: lc.Location[]) => {
let client = ctx.client;
if (client) {
vscode.commands.executeCommand(
'editor.action.showReferences',
vscode.Uri.parse(uri),
ctx.client.protocol2CodeConverter.asPosition(position),
locations.map(ctx.client.protocol2CodeConverter.asLocation),
client.protocol2CodeConverter.asPosition(position),
locations.map(client.protocol2CodeConverter.asLocation),
);
}
};
}
@ -36,6 +39,13 @@ function applySourceChange(ctx: Ctx): Cmd {
}
}
function reload(ctx: Ctx): Cmd {
return async () => {
vscode.window.showInformationMessage('Reloading rust-analyzer...');
await ctx.restartServer();
}
}
export {
analyzerStatus,
expandMacro,
@ -49,4 +59,5 @@ export {
runSingle,
showReferences,
applySourceChange,
reload
};

View file

@ -6,13 +6,14 @@ import { applySourceChange, SourceChange } from '../source_change';
export function joinLines(ctx: Ctx): Cmd {
return async () => {
const editor = ctx.activeRustEditor;
if (!editor) return;
const client = ctx.client;
if (!editor || !client) return;
const request: JoinLinesParams = {
range: ctx.client.code2ProtocolConverter.asRange(editor.selection),
range: client.code2ProtocolConverter.asRange(editor.selection),
textDocument: { uri: editor.document.uri.toString() },
};
const change = await ctx.client.sendRequest<SourceChange>(
const change = await client.sendRequest<SourceChange>(
'rust-analyzer/joinLines',
request,
);

View file

@ -1,19 +1,38 @@
import * as vscode from 'vscode';
import * as lc from 'vscode-languageclient';
import { Server } from './server';
import { Config } from './config';
import { createClient } from './client'
export class Ctx {
readonly config: Config;
// Because we have "reload server" action, various listeners **will** face a
// situation where the client is not ready yet, and should be prepared to
// deal with it.
//
// Ideally, this should be replaced with async getter though.
client: lc.LanguageClient | null = null
private extCtx: vscode.ExtensionContext;
private onDidRestartHooks: Array<(client: lc.LanguageClient) => void> = [];
constructor(extCtx: vscode.ExtensionContext) {
this.config = new Config(extCtx)
this.extCtx = extCtx;
}
get client(): lc.LanguageClient {
return Server.client;
async restartServer() {
let old = this.client;
if (old) {
await old.stop()
}
this.client = null;
const client = createClient(this.config);
this.pushCleanup(client.start());
await client.onReady();
this.client = client
for (const hook of this.onDidRestartHooks) {
hook(client)
}
}
get activeRustEditor(): vscode.TextEditor | undefined {
@ -60,15 +79,22 @@ export class Ctx {
this.extCtx.subscriptions.push(d);
}
async sendRequestWithRetry<R>(
onDidRestart(hook: (client: lc.LanguageClient) => void) {
this.onDidRestartHooks.push(hook)
}
}
export type Cmd = (...args: any[]) => any;
export async function sendRequestWithRetry<R>(
client: lc.LanguageClient,
method: string,
param: any,
token?: vscode.CancellationToken,
): Promise<R> {
await this.client.onReady();
for (const delay of [2, 4, 6, 8, 10, null]) {
try {
return await (token ? this.client.sendRequest(method, param, token) : this.client.sendRequest(method, param));
return await (token ? client.sendRequest(method, param, token) : client.sendRequest(method, param));
} catch (e) {
if (
e.code === lc.ErrorCodes.ContentModified &&
@ -83,12 +109,4 @@ export class Ctx {
throw 'unreachable';
}
onNotification(method: string, handler: lc.GenericNotificationHandler) {
this.client.onReady()
.then(() => this.client.onNotification(method, handler))
}
}
export type Cmd = (...args: any[]) => any;
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

View file

@ -5,12 +5,12 @@ const seedrandom = seedrandom_; // https://github.com/jvandemo/generator-angular
import { ColorTheme, TextMateRuleSettings } from './color_theme';
import { Ctx } from './ctx';
import { Ctx, sendRequestWithRetry } from './ctx';
export function activateHighlighting(ctx: Ctx) {
const highlighter = new Highlighter(ctx);
ctx.onNotification(
ctx.onDidRestart(client => {
client.onNotification(
'rust-analyzer/publishDecorations',
(params: PublishDecorationsParams) => {
if (!ctx.config.highlightingOn) return;
@ -30,6 +30,7 @@ export function activateHighlighting(ctx: Ctx) {
highlighter.setHighlights(targetEditor, params.decorations);
},
);
})
vscode.workspace.onDidChangeConfiguration(
_ => highlighter.removeHighlights(),
@ -40,11 +41,14 @@ export function activateHighlighting(ctx: Ctx) {
async (editor: vscode.TextEditor | undefined) => {
if (!editor || editor.document.languageId !== 'rust') return;
if (!ctx.config.highlightingOn) return;
let client = ctx.client;
if (!client) return;
const params: lc.TextDocumentIdentifier = {
uri: editor.document.uri.toString(),
};
const decorations = await ctx.sendRequestWithRetry<Decoration[]>(
const decorations = await sendRequestWithRetry<Decoration[]>(
client,
'rust-analyzer/decorationsRequest',
params,
);
@ -103,6 +107,8 @@ class Highlighter {
}
public setHighlights(editor: vscode.TextEditor, highlights: Decoration[]) {
let client = this.ctx.client;
if (!client) return;
// Initialize decorations if necessary
//
// Note: decoration objects need to be kept around so we can dispose them
@ -135,13 +141,13 @@ class Highlighter {
colorfulIdents
.get(d.bindingHash)![0]
.push(
this.ctx.client.protocol2CodeConverter.asRange(d.range),
client.protocol2CodeConverter.asRange(d.range),
);
} else {
byTag
.get(d.tag)!
.push(
this.ctx.client.protocol2CodeConverter.asRange(d.range),
client.protocol2CodeConverter.asRange(d.range),
);
}
}

View file

@ -1,7 +1,7 @@
import * as vscode from 'vscode';
import * as lc from 'vscode-languageclient';
import { Ctx } from './ctx';
import { Ctx, sendRequestWithRetry } from './ctx';
export function activateInlayHints(ctx: Ctx) {
const hintsUpdater = new HintsUpdater(ctx);
@ -19,9 +19,7 @@ export function activateInlayHints(ctx: Ctx) {
hintsUpdater.setEnabled(ctx.config.displayInlayHints);
}, ctx.subscriptions);
// XXX: don't await here;
// Who knows what happens if an exception is thrown here...
hintsUpdater.refresh();
ctx.onDidRestart(_ => hintsUpdater.setEnabled(ctx.config.displayInlayHints))
}
interface InlayHintsParams {
@ -97,6 +95,8 @@ class HintsUpdater {
}
private async queryHints(documentUri: string): Promise<InlayHint[] | null> {
let client = this.ctx.client;
if (!client) return null
const request: InlayHintsParams = {
textDocument: { uri: documentUri },
};
@ -105,7 +105,8 @@ class HintsUpdater {
if (prev) prev.cancel();
this.pending.set(documentUri, tokenSource);
try {
return await this.ctx.sendRequestWithRetry<InlayHint[] | null>(
return await sendRequestWithRetry<InlayHint[] | null>(
client,
'rust-analyzer/inlayHints',
request,
tokenSource.token,

View file

@ -3,7 +3,6 @@ import * as vscode from 'vscode';
import * as commands from './commands';
import { activateInlayHints } from './inlay_hints';
import { activateStatusDisplay } from './status_display';
import { Server } from './server';
import { Ctx } from './ctx';
import { activateHighlighting } from './highlighting';
@ -21,6 +20,7 @@ export async function activate(context: vscode.ExtensionContext) {
ctx.registerCommand('syntaxTree', commands.syntaxTree);
ctx.registerCommand('expandMacro', commands.expandMacro);
ctx.registerCommand('run', commands.run);
ctx.registerCommand('reload', commands.reload);
// Internal commands which are invoked by the server.
ctx.registerCommand('runSingle', commands.runSingle);
@ -30,38 +30,17 @@ export async function activate(context: vscode.ExtensionContext) {
if (ctx.config.enableEnhancedTyping) {
ctx.overrideCommand('type', commands.onEnter);
}
const startServer = () => Server.start(ctx.config);
const reloadCommand = () => reloadServer(startServer);
vscode.commands.registerCommand('rust-analyzer.reload', reloadCommand);
activateStatusDisplay(ctx);
activateHighlighting(ctx);
activateInlayHints(ctx);
// Start the language server, finally!
try {
await startServer();
await ctx.restartServer();
} catch (e) {
vscode.window.showErrorMessage(e.message);
}
activateStatusDisplay(ctx);
activateHighlighting(ctx);
if (ctx.config.displayInlayHints) {
activateInlayHints(ctx);
}
}
export function deactivate(): Thenable<void> {
if (!Server.client) {
return Promise.resolve();
}
return Server.client.stop();
}
async function reloadServer(startServer: () => Promise<void>) {
if (Server.client != null) {
vscode.window.showInformationMessage('Reloading rust-analyzer...');
await Server.client.stop();
await startServer();
}
export async function deactivate() {
await ctx?.client?.stop();
}

View file

@ -1,96 +0,0 @@
import { homedir } from 'os';
import * as lc from 'vscode-languageclient';
import { window, workspace } from 'vscode';
import { Config } from './config';
function expandPathResolving(path: string) {
if (path.startsWith('~/')) {
return path.replace('~', homedir());
}
return path;
}
export class Server {
static config: Config;
public static client: lc.LanguageClient;
public static async start(config: Config) {
// '.' 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.
let folder: string = '.';
if (workspace.workspaceFolders !== undefined) {
folder = workspace.workspaceFolders[0].uri.fsPath.toString();
}
this.config = config;
const command = expandPathResolving(this.config.raLspServerPath);
const run: lc.Executable = {
command,
options: { cwd: folder },
};
const serverOptions: lc.ServerOptions = {
run,
debug: run,
};
const traceOutputChannel = window.createOutputChannel(
'Rust Analyzer Language Server Trace',
);
const clientOptions: lc.LanguageClientOptions = {
documentSelector: [{ scheme: 'file', language: 'rust' }],
initializationOptions: {
publishDecorations: true,
lruCapacity: Server.config.lruCapacity,
maxInlayHintLength: Server.config.maxInlayHintLength,
cargoWatchEnable: Server.config.cargoWatchOptions.enable,
cargoWatchArgs: Server.config.cargoWatchOptions.arguments,
cargoWatchCommand: Server.config.cargoWatchOptions.command,
cargoWatchAllTargets:
Server.config.cargoWatchOptions.allTargets,
excludeGlobs: Server.config.excludeGlobs,
useClientWatching: Server.config.useClientWatching,
featureFlags: Server.config.featureFlags,
withSysroot: Server.config.withSysroot,
cargoFeatures: Server.config.cargoFeatures,
},
traceOutputChannel,
};
Server.client = new lc.LanguageClient(
'rust-analyzer',
'Rust Analyzer Language Server',
serverOptions,
clientOptions,
);
// HACK: This is an awful way of filtering out the decorations notifications
// However, pending proper support, this is the most effecitve approach
// Proper support for this would entail a change to vscode-languageclient to allow not notifying on certain messages
// Or the ability to disable the serverside component of highlighting (but this means that to do tracing we need to disable hihlighting)
// This also requires considering our settings strategy, which is work which needs doing
// @ts-ignore The tracer is private to vscode-languageclient, but we need access to it to not log publishDecorations requests
Server.client._tracer = {
log: (messageOrDataObject: string | any, data?: string) => {
if (typeof messageOrDataObject === 'string') {
if (
messageOrDataObject.includes(
'rust-analyzer/publishDecorations',
) ||
messageOrDataObject.includes(
'rust-analyzer/decorationsRequest',
)
) {
// Don't log publish decorations requests
} else {
// @ts-ignore This is just a utility function
Server.client.logTrace(messageOrDataObject, data);
}
} else {
// @ts-ignore
Server.client.logObjectTrace(messageOrDataObject);
}
},
};
Server.client.registerProposedFeatures();
Server.client.start();
}
}

View file

@ -10,7 +10,10 @@ export interface SourceChange {
}
export async function applySourceChange(ctx: Ctx, change: SourceChange) {
const wsEdit = ctx.client.protocol2CodeConverter.asWorkspaceEdit(
const client = ctx.client;
if (!client) return
const wsEdit = client.protocol2CodeConverter.asWorkspaceEdit(
change.workspaceEdit,
);
let created;
@ -32,10 +35,10 @@ export async function applySourceChange(ctx: Ctx, change: SourceChange) {
const doc = await vscode.workspace.openTextDocument(toOpenUri);
await vscode.window.showTextDocument(doc);
} else if (toReveal) {
const uri = ctx.client.protocol2CodeConverter.asUri(
const uri = client.protocol2CodeConverter.asUri(
toReveal.textDocument.uri,
);
const position = ctx.client.protocol2CodeConverter.asPosition(
const position = client.protocol2CodeConverter.asPosition(
toReveal.position,
);
const editor = vscode.window.activeTextEditor;

View file

@ -7,7 +7,9 @@ const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '
export function activateStatusDisplay(ctx: Ctx) {
const statusDisplay = new StatusDisplay(ctx.config.cargoWatchOptions.command);
ctx.pushCleanup(statusDisplay);
ctx.onNotification('$/progress', params => statusDisplay.handleProgressNotification(params));
ctx.onDidRestart(client => {
client.onNotification('$/progress', params => statusDisplay.handleProgressNotification(params));
})
}
class StatusDisplay implements vscode.Disposable {