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, _uri: vscode.Uri,
): vscode.ProviderResult<string> { ): vscode.ProviderResult<string> {
const editor = vscode.window.activeTextEditor; 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', 'rust-analyzer/analyzerStatus',
null, null,
); );

View file

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

View file

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

View file

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

View file

@ -1,19 +1,38 @@
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import * as lc from 'vscode-languageclient'; import * as lc from 'vscode-languageclient';
import { Server } from './server';
import { Config } from './config'; import { Config } from './config';
import { createClient } from './client'
export class Ctx { export class Ctx {
readonly config: Config; 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 extCtx: vscode.ExtensionContext;
private onDidRestartHooks: Array<(client: lc.LanguageClient) => void> = [];
constructor(extCtx: vscode.ExtensionContext) { constructor(extCtx: vscode.ExtensionContext) {
this.config = new Config(extCtx) this.config = new Config(extCtx)
this.extCtx = extCtx; this.extCtx = extCtx;
} }
get client(): lc.LanguageClient { async restartServer() {
return Server.client; 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 { get activeRustEditor(): vscode.TextEditor | undefined {
@ -60,35 +79,34 @@ export class Ctx {
this.extCtx.subscriptions.push(d); this.extCtx.subscriptions.push(d);
} }
async sendRequestWithRetry<R>( onDidRestart(hook: (client: lc.LanguageClient) => void) {
method: string, this.onDidRestartHooks.push(hook)
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));
} catch (e) {
if (
e.code === lc.ErrorCodes.ContentModified &&
delay !== null
) {
await sleep(10 * (1 << delay));
continue;
}
throw e;
}
}
throw 'unreachable';
}
onNotification(method: string, handler: lc.GenericNotificationHandler) {
this.client.onReady()
.then(() => this.client.onNotification(method, handler))
} }
} }
export type Cmd = (...args: any[]) => any; export type Cmd = (...args: any[]) => any;
export async function sendRequestWithRetry<R>(
client: lc.LanguageClient,
method: string,
param: any,
token?: vscode.CancellationToken,
): Promise<R> {
for (const delay of [2, 4, 6, 8, 10, null]) {
try {
return await (token ? client.sendRequest(method, param, token) : client.sendRequest(method, param));
} catch (e) {
if (
e.code === lc.ErrorCodes.ContentModified &&
delay !== null
) {
await sleep(10 * (1 << delay));
continue;
}
throw e;
}
}
throw 'unreachable';
}
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

View file

@ -5,31 +5,32 @@ const seedrandom = seedrandom_; // https://github.com/jvandemo/generator-angular
import { ColorTheme, TextMateRuleSettings } from './color_theme'; import { ColorTheme, TextMateRuleSettings } from './color_theme';
import { Ctx } from './ctx'; import { Ctx, sendRequestWithRetry } from './ctx';
export function activateHighlighting(ctx: Ctx) { export function activateHighlighting(ctx: Ctx) {
const highlighter = new Highlighter(ctx); const highlighter = new Highlighter(ctx);
ctx.onDidRestart(client => {
client.onNotification(
'rust-analyzer/publishDecorations',
(params: PublishDecorationsParams) => {
if (!ctx.config.highlightingOn) return;
ctx.onNotification( const targetEditor = vscode.window.visibleTextEditors.find(
'rust-analyzer/publishDecorations', editor => {
(params: PublishDecorationsParams) => { const unescapedUri = unescape(
if (!ctx.config.highlightingOn) return; editor.document.uri.toString(),
);
// Unescaped URI looks like:
// file:///c:/Workspace/ra-test/src/main.rs
return unescapedUri === params.uri;
},
);
if (!targetEditor) return;
const targetEditor = vscode.window.visibleTextEditors.find( highlighter.setHighlights(targetEditor, params.decorations);
editor => { },
const unescapedUri = unescape( );
editor.document.uri.toString(), })
);
// Unescaped URI looks like:
// file:///c:/Workspace/ra-test/src/main.rs
return unescapedUri === params.uri;
},
);
if (!targetEditor) return;
highlighter.setHighlights(targetEditor, params.decorations);
},
);
vscode.workspace.onDidChangeConfiguration( vscode.workspace.onDidChangeConfiguration(
_ => highlighter.removeHighlights(), _ => highlighter.removeHighlights(),
@ -40,11 +41,14 @@ export function activateHighlighting(ctx: Ctx) {
async (editor: vscode.TextEditor | undefined) => { async (editor: vscode.TextEditor | undefined) => {
if (!editor || editor.document.languageId !== 'rust') return; if (!editor || editor.document.languageId !== 'rust') return;
if (!ctx.config.highlightingOn) return; if (!ctx.config.highlightingOn) return;
let client = ctx.client;
if (!client) return;
const params: lc.TextDocumentIdentifier = { const params: lc.TextDocumentIdentifier = {
uri: editor.document.uri.toString(), uri: editor.document.uri.toString(),
}; };
const decorations = await ctx.sendRequestWithRetry<Decoration[]>( const decorations = await sendRequestWithRetry<Decoration[]>(
client,
'rust-analyzer/decorationsRequest', 'rust-analyzer/decorationsRequest',
params, params,
); );
@ -103,6 +107,8 @@ class Highlighter {
} }
public setHighlights(editor: vscode.TextEditor, highlights: Decoration[]) { public setHighlights(editor: vscode.TextEditor, highlights: Decoration[]) {
let client = this.ctx.client;
if (!client) return;
// Initialize decorations if necessary // Initialize decorations if necessary
// //
// Note: decoration objects need to be kept around so we can dispose them // Note: decoration objects need to be kept around so we can dispose them
@ -135,13 +141,13 @@ class Highlighter {
colorfulIdents colorfulIdents
.get(d.bindingHash)![0] .get(d.bindingHash)![0]
.push( .push(
this.ctx.client.protocol2CodeConverter.asRange(d.range), client.protocol2CodeConverter.asRange(d.range),
); );
} else { } else {
byTag byTag
.get(d.tag)! .get(d.tag)!
.push( .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 vscode from 'vscode';
import * as lc from 'vscode-languageclient'; import * as lc from 'vscode-languageclient';
import { Ctx } from './ctx'; import { Ctx, sendRequestWithRetry } from './ctx';
export function activateInlayHints(ctx: Ctx) { export function activateInlayHints(ctx: Ctx) {
const hintsUpdater = new HintsUpdater(ctx); const hintsUpdater = new HintsUpdater(ctx);
@ -19,9 +19,7 @@ export function activateInlayHints(ctx: Ctx) {
hintsUpdater.setEnabled(ctx.config.displayInlayHints); hintsUpdater.setEnabled(ctx.config.displayInlayHints);
}, ctx.subscriptions); }, ctx.subscriptions);
// XXX: don't await here; ctx.onDidRestart(_ => hintsUpdater.setEnabled(ctx.config.displayInlayHints))
// Who knows what happens if an exception is thrown here...
hintsUpdater.refresh();
} }
interface InlayHintsParams { interface InlayHintsParams {
@ -97,6 +95,8 @@ class HintsUpdater {
} }
private async queryHints(documentUri: string): Promise<InlayHint[] | null> { private async queryHints(documentUri: string): Promise<InlayHint[] | null> {
let client = this.ctx.client;
if (!client) return null
const request: InlayHintsParams = { const request: InlayHintsParams = {
textDocument: { uri: documentUri }, textDocument: { uri: documentUri },
}; };
@ -105,7 +105,8 @@ class HintsUpdater {
if (prev) prev.cancel(); if (prev) prev.cancel();
this.pending.set(documentUri, tokenSource); this.pending.set(documentUri, tokenSource);
try { try {
return await this.ctx.sendRequestWithRetry<InlayHint[] | null>( return await sendRequestWithRetry<InlayHint[] | null>(
client,
'rust-analyzer/inlayHints', 'rust-analyzer/inlayHints',
request, request,
tokenSource.token, tokenSource.token,

View file

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

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

View file

@ -7,7 +7,9 @@ const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '
export function activateStatusDisplay(ctx: Ctx) { export function activateStatusDisplay(ctx: Ctx) {
const statusDisplay = new StatusDisplay(ctx.config.cargoWatchOptions.command); const statusDisplay = new StatusDisplay(ctx.config.cargoWatchOptions.command);
ctx.pushCleanup(statusDisplay); 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 { class StatusDisplay implements vscode.Disposable {