diff --git a/editors/code/src/client.ts b/editors/code/src/client.ts new file mode 100644 index 00000000000..94948b10fe3 --- /dev/null +++ b/editors/code/src/client.ts @@ -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; +} diff --git a/editors/code/src/commands/analyzer_status.ts b/editors/code/src/commands/analyzer_status.ts index 2c8362286d0..cf37dc6f03b 100644 --- a/editors/code/src/commands/analyzer_status.ts +++ b/editors/code/src/commands/analyzer_status.ts @@ -49,9 +49,10 @@ class TextDocumentContentProvider _uri: vscode.Uri, ): vscode.ProviderResult { const editor = vscode.window.activeTextEditor; - if (editor == null) return ''; + const client = this.ctx.client + if (!editor || !client) return ''; - return this.ctx.client.sendRequest( + return client.sendRequest( 'rust-analyzer/analyzerStatus', null, ); diff --git a/editors/code/src/commands/expand_macro.ts b/editors/code/src/commands/expand_macro.ts index da208257a2a..472f43b8db4 100644 --- a/editors/code/src/commands/expand_macro.ts +++ b/editors/code/src/commands/expand_macro.ts @@ -52,14 +52,15 @@ class TextDocumentContentProvider async provideTextDocumentContent(_uri: vscode.Uri): Promise { 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( + const expanded = await client.sendRequest( 'rust-analyzer/expandMacro', request, ); diff --git a/editors/code/src/commands/index.ts b/editors/code/src/commands/index.ts index c28709c8a02..4431fdcf68f 100644 --- a/editors/code/src/commands/index.ts +++ b/editors/code/src/commands/index.ts @@ -15,18 +15,21 @@ import { run, runSingle } from './runnables'; function collectGarbage(ctx: Ctx): Cmd { return async () => { - ctx.client.sendRequest('rust-analyzer/collectGarbage', null); + ctx.client?.sendRequest('rust-analyzer/collectGarbage', null); }; } function showReferences(ctx: Ctx): Cmd { return (uri: string, position: lc.Position, locations: lc.Location[]) => { - vscode.commands.executeCommand( - 'editor.action.showReferences', - vscode.Uri.parse(uri), - ctx.client.protocol2CodeConverter.asPosition(position), - locations.map(ctx.client.protocol2CodeConverter.asLocation), - ); + let client = ctx.client; + if (client) { + vscode.commands.executeCommand( + 'editor.action.showReferences', + 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 { analyzerStatus, expandMacro, @@ -49,4 +59,5 @@ export { runSingle, showReferences, applySourceChange, + reload }; diff --git a/editors/code/src/commands/join_lines.ts b/editors/code/src/commands/join_lines.ts index f4f902cf96e..7b08c325503 100644 --- a/editors/code/src/commands/join_lines.ts +++ b/editors/code/src/commands/join_lines.ts @@ -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( + const change = await client.sendRequest( 'rust-analyzer/joinLines', request, ); diff --git a/editors/code/src/config.ts b/editors/code/src/config.ts index ccb0ee2b7ec..ec2790b63c1 100644 --- a/editors/code/src/config.ts +++ b/editors/code/src/config.ts @@ -16,25 +16,25 @@ export interface CargoFeatures { } export class Config { - public highlightingOn = true; - public rainbowHighlightingOn = false; - public enableEnhancedTyping = true; - public raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server'; - public lruCapacity: null | number = null; - public displayInlayHints = true; - public maxInlayHintLength: null | number = null; - public excludeGlobs = []; - public useClientWatching = true; - public featureFlags = {}; + highlightingOn = true; + rainbowHighlightingOn = false; + enableEnhancedTyping = true; + raLspServerPath = RA_LSP_DEBUG || 'ra_lsp_server'; + lruCapacity: null | number = null; + displayInlayHints = true; + maxInlayHintLength: null | number = null; + excludeGlobs = []; + useClientWatching = true; + featureFlags = {}; // for internal use - public withSysroot: null | boolean = null; - public cargoWatchOptions: CargoWatchOptions = { + withSysroot: null | boolean = null; + cargoWatchOptions: CargoWatchOptions = { enable: true, arguments: [], command: '', allTargets: true, }; - public cargoFeatures: CargoFeatures = { + cargoFeatures: CargoFeatures = { noDefaultFeatures: false, allFeatures: true, features: [], @@ -43,14 +43,12 @@ export class Config { private prevEnhancedTyping: null | boolean = null; private prevCargoFeatures: null | CargoFeatures = null; - constructor() { - vscode.workspace.onDidChangeConfiguration(_ => - this.userConfigChanged(), - ); - this.userConfigChanged(); + constructor(ctx: vscode.ExtensionContext) { + vscode.workspace.onDidChangeConfiguration(_ => this.refresh(), ctx.subscriptions); + this.refresh(); } - public userConfigChanged() { + private refresh() { const config = vscode.workspace.getConfiguration('rust-analyzer'); let requireReloadMessage = null; diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index 693ce05ed4a..13988056aa7 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -1,21 +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(); - get config(): Config { - return Server.config; + this.client = client + for (const hook of this.onDidRestartHooks) { + hook(client) + } } get activeRustEditor(): vscode.TextEditor | undefined { @@ -62,30 +79,34 @@ export class Ctx { this.extCtx.subscriptions.push(d); } - async sendRequestWithRetry( - method: string, - param: any, - token?: vscode.CancellationToken, - ): Promise { - 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'; + onDidRestart(hook: (client: lc.LanguageClient) => void) { + this.onDidRestartHooks.push(hook) } } export type Cmd = (...args: any[]) => any; +export async function sendRequestWithRetry( + client: lc.LanguageClient, + method: string, + param: any, + token?: vscode.CancellationToken, +): Promise { + 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)); diff --git a/editors/code/src/highlighting.ts b/editors/code/src/highlighting.ts index d383d87ef93..f9d2e9d909c 100644 --- a/editors/code/src/highlighting.ts +++ b/editors/code/src/highlighting.ts @@ -5,13 +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.client.onReady().then(() => { - ctx.client.onNotification( + ctx.onDidRestart(client => { + client.onNotification( 'rust-analyzer/publishDecorations', (params: PublishDecorationsParams) => { if (!ctx.config.highlightingOn) return; @@ -31,7 +30,7 @@ export function activateHighlighting(ctx: Ctx) { highlighter.setHighlights(targetEditor, params.decorations); }, ); - }); + }) vscode.workspace.onDidChangeConfiguration( _ => highlighter.removeHighlights(), @@ -42,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( + const decorations = await sendRequestWithRetry( + client, 'rust-analyzer/decorationsRequest', params, ); @@ -105,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 @@ -137,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), ); } } diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts index b6eb70168a8..e74d6996f84 100644 --- a/editors/code/src/inlay_hints.ts +++ b/editors/code/src/inlay_hints.ts @@ -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 { + 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( + return await sendRequestWithRetry( + client, 'rust-analyzer/inlayHints', request, tokenSource.token, diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 0c4abdac89f..51dedd5efc7 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -1,10 +1,8 @@ import * as vscode from 'vscode'; -import * as lc from 'vscode-languageclient'; import * as commands from './commands'; import { activateInlayHints } from './inlay_hints'; -import { StatusDisplay } from './status_display'; -import { Server } from './server'; +import { activateStatusDisplay } from './status_display'; import { Ctx } from './ctx'; import { activateHighlighting } from './highlighting'; @@ -13,6 +11,17 @@ let ctx!: Ctx; export async function activate(context: vscode.ExtensionContext) { ctx = new Ctx(context); + // Note: we try to start the server before we register various commands, so + // that it registers its `onDidChangeDocument` handler before us. + // + // This a horribly, horribly wrong way to deal with this problem. + try { + await ctx.restartServer(); + } catch (e) { + vscode.window.showErrorMessage(e.message); + } + + // Commands which invokes manually via command pallet, shortcut, etc. ctx.registerCommand('analyzerStatus', commands.analyzerStatus); ctx.registerCommand('collectGarbage', commands.collectGarbage); @@ -22,6 +31,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); @@ -31,48 +41,11 @@ export async function activate(context: vscode.ExtensionContext) { if (ctx.config.enableEnhancedTyping) { ctx.overrideCommand('type', commands.onEnter); } - - const watchStatus = new StatusDisplay(ctx.config.cargoWatchOptions.command); - ctx.pushCleanup(watchStatus); - - // Notifications are events triggered by the language server - const allNotifications: [string, lc.GenericNotificationHandler][] = [ - [ - '$/progress', - params => watchStatus.handleProgressNotification(params), - ], - ]; - - const startServer = () => Server.start(allNotifications); - const reloadCommand = () => reloadServer(startServer); - - vscode.commands.registerCommand('rust-analyzer.reload', reloadCommand); - - // Start the language server, finally! - try { - await startServer(); - } catch (e) { - vscode.window.showErrorMessage(e.message); - } - + activateStatusDisplay(ctx); activateHighlighting(ctx); - - if (ctx.config.displayInlayHints) { - activateInlayHints(ctx); - } + activateInlayHints(ctx); } -export function deactivate(): Thenable { - if (!Server.client) { - return Promise.resolve(); - } - return Server.client.stop(); -} - -async function reloadServer(startServer: () => Promise) { - if (Server.client != null) { - vscode.window.showInformationMessage('Reloading rust-analyzer...'); - await Server.client.stop(); - await startServer(); - } +export async function deactivate() { + await ctx?.client?.stop(); } diff --git a/editors/code/src/server.ts b/editors/code/src/server.ts deleted file mode 100644 index 2bb21da6bef..00000000000 --- a/editors/code/src/server.ts +++ /dev/null @@ -1,102 +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 { - public static config = new Config(); - public static client: lc.LanguageClient; - - public static async start( - notificationHandlers: Iterable<[string, lc.GenericNotificationHandler]>, - ) { - // '.' 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(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.onReady().then(() => { - for (const [type, handler] of notificationHandlers) { - Server.client.onNotification(type, handler); - } - }); - Server.client.start(); - } -} diff --git a/editors/code/src/source_change.ts b/editors/code/src/source_change.ts index a4f9068b274..887191d9e53 100644 --- a/editors/code/src/source_change.ts +++ b/editors/code/src/source_change.ts @@ -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; diff --git a/editors/code/src/status_display.ts b/editors/code/src/status_display.ts index 48cf0655be6..1454bf8b00c 100644 --- a/editors/code/src/status_display.ts +++ b/editors/code/src/status_display.ts @@ -1,8 +1,18 @@ import * as vscode from 'vscode'; +import { Ctx } from './ctx'; + const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']; -export class StatusDisplay implements vscode.Disposable { +export function activateStatusDisplay(ctx: Ctx) { + const statusDisplay = new StatusDisplay(ctx.config.cargoWatchOptions.command); + ctx.pushCleanup(statusDisplay); + ctx.onDidRestart(client => { + client.onNotification('$/progress', params => statusDisplay.handleProgressNotification(params)); + }) +} + +class StatusDisplay implements vscode.Disposable { packageName?: string; private i = 0;