diff --git a/crates/ra_lsp_server/src/main_loop.rs b/crates/ra_lsp_server/src/main_loop.rs index af1a487ded10..4336583fee8e 100644 --- a/crates/ra_lsp_server/src/main_loop.rs +++ b/crates/ra_lsp_server/src/main_loop.rs @@ -709,16 +709,11 @@ where Ok(lsp_error) => Response::new_err(id, lsp_error.code, lsp_error.message), Err(e) => { if is_canceled(&e) { - // FIXME: When https://github.com/Microsoft/vscode-languageserver-node/issues/457 - // gets fixed, we can return the proper response. - // This works around the issue where "content modified" error would continuously - // show an message pop-up in VsCode - // Response::err( - // id, - // ErrorCode::ContentModified as i32, - // "content modified".to_string(), - // ) - Response::new_ok(id, ()) + Response::new_err( + id, + ErrorCode::ContentModified as i32, + "content modified".to_string(), + ) } else { Response::new_err(id, ErrorCode::InternalError as i32, e.to_string()) } diff --git a/editors/code/rollup.config.js b/editors/code/rollup.config.js index 4c001f8993c5..14fb9e085c51 100644 --- a/editors/code/rollup.config.js +++ b/editors/code/rollup.config.js @@ -13,7 +13,7 @@ export default { commonjs({ namedExports: { // squelch missing import warnings - 'vscode-languageclient': ['CreateFile', 'RenameFile'] + 'vscode-languageclient': ['CreateFile', 'RenameFile', 'ErrorCodes'] } }) ], diff --git a/editors/code/src/ctx.ts b/editors/code/src/ctx.ts index c3a3583b52d9..d3ef27e430fd 100644 --- a/editors/code/src/ctx.ts +++ b/editors/code/src/ctx.ts @@ -1,6 +1,7 @@ import * as vscode from 'vscode'; import * as lc from 'vscode-languageclient'; import { Server } from './server'; +import { Config } from './config'; export class Ctx { private extCtx: vscode.ExtensionContext; @@ -13,6 +14,10 @@ export class Ctx { return Server.client; } + get config(): Config { + return Server.config; + } + get activeRustEditor(): vscode.TextEditor | undefined { const editor = vscode.window.activeTextEditor; return editor && editor.document.languageId === 'rust' @@ -56,6 +61,24 @@ export class Ctx { pushCleanup(d: { dispose(): any }) { 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 this.client.sendRequest(method, param, token); + } catch (e) { + if (e.code === lc.ErrorCodes.ContentModified && delay !== null) { + await sleep(10 * (1 << delay)) + continue; + } + throw e; + } + } + throw 'unreachable' + } } export type Cmd = (...args: any[]) => any; + +const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) diff --git a/editors/code/src/events/change_active_text_editor.ts b/editors/code/src/events/change_active_text_editor.ts deleted file mode 100644 index 4384ee56768c..000000000000 --- a/editors/code/src/events/change_active_text_editor.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { TextEditor } from 'vscode'; -import { TextDocumentIdentifier } from 'vscode-languageclient'; -import { Decoration } from '../highlighting'; -import { Server } from '../server'; - -export function makeHandler() { - return async function handle(editor: TextEditor | undefined) { - if (!editor || editor.document.languageId !== 'rust') { - return; - } - - if (!Server.config.highlightingOn) { - return; - } - - const params: TextDocumentIdentifier = { - uri: editor.document.uri.toString(), - }; - const decorations = await Server.client.sendRequest( - 'rust-analyzer/decorationsRequest', - params, - ); - Server.highlighter.setHighlights(editor, decorations); - }; -} diff --git a/editors/code/src/events/index.ts b/editors/code/src/events/index.ts deleted file mode 100644 index be135474de62..000000000000 --- a/editors/code/src/events/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import * as changeActiveTextEditor from './change_active_text_editor'; - -export { changeActiveTextEditor }; diff --git a/editors/code/src/highlighting.ts b/editors/code/src/highlighting.ts index 4e224a54c116..333319b850c8 100644 --- a/editors/code/src/highlighting.ts +++ b/editors/code/src/highlighting.ts @@ -1,10 +1,31 @@ -import seedrandom = require('seedrandom'); import * as vscode from 'vscode'; import * as lc from 'vscode-languageclient'; +import * as seedrandom_ from 'seedrandom'; +const seedrandom = seedrandom_; // https://github.com/jvandemo/generator-angular2-library/issues/221#issuecomment-355945207 + import * as scopes from './scopes'; import * as scopesMapper from './scopes_mapper'; import { Server } from './server'; +import { Ctx } from './ctx'; + +export function activateHighlighting(ctx: Ctx) { + vscode.window.onDidChangeActiveTextEditor( + async (editor: vscode.TextEditor | undefined) => { + if (!editor || editor.document.languageId !== 'rust') return; + if (!ctx.config.highlightingOn) return; + + const params: lc.TextDocumentIdentifier = { + uri: editor.document.uri.toString(), + }; + const decorations = await ctx.client.sendRequest( + 'rust-analyzer/decorationsRequest', + params, + ); + Server.highlighter.setHighlights(editor, decorations); + }, + ); +} export interface Decoration { range: lc.Range; diff --git a/editors/code/src/inlay_hints.ts b/editors/code/src/inlay_hints.ts index 4581e22782f0..d41297407e07 100644 --- a/editors/code/src/inlay_hints.ts +++ b/editors/code/src/inlay_hints.ts @@ -1,41 +1,27 @@ import * as vscode from 'vscode'; import * as lc from 'vscode-languageclient'; -import { Server } from './server'; + import { Ctx } from './ctx'; export function activateInlayHints(ctx: Ctx) { - const hintsUpdater = new HintsUpdater(); - hintsUpdater.refreshHintsForVisibleEditors().then(() => { - // vscode may ignore top level hintsUpdater.refreshHintsForVisibleEditors() - // so update the hints once when the focus changes to guarantee their presence - let editorChangeDisposable: vscode.Disposable | null = null; - editorChangeDisposable = vscode.window.onDidChangeActiveTextEditor( - _ => { - if (editorChangeDisposable !== null) { - editorChangeDisposable.dispose(); - } - return hintsUpdater.refreshHintsForVisibleEditors(); - }, - ); + const hintsUpdater = new HintsUpdater(ctx); + vscode.window.onDidChangeVisibleTextEditors(async _ => { + await hintsUpdater.refresh(); + }, ctx.subscriptions); - ctx.pushCleanup( - vscode.window.onDidChangeVisibleTextEditors(_ => - hintsUpdater.refreshHintsForVisibleEditors(), - ), - ); - ctx.pushCleanup( - vscode.workspace.onDidChangeTextDocument(e => - hintsUpdater.refreshHintsForVisibleEditors(e), - ), - ); - ctx.pushCleanup( - vscode.workspace.onDidChangeConfiguration(_ => - hintsUpdater.toggleHintsDisplay( - Server.config.displayInlayHints, - ), - ), - ); - }); + vscode.workspace.onDidChangeTextDocument(async e => { + if (e.contentChanges.length === 0) return; + if (e.document.languageId !== 'rust') return; + await hintsUpdater.refresh(); + }, ctx.subscriptions); + + vscode.workspace.onDidChangeConfiguration(_ => { + hintsUpdater.setEnabled(ctx.config.displayInlayHints); + }, ctx.subscriptions); + + // XXX: don't await here; + // Who knows what happens if an exception is thrown here... + hintsUpdater.refresh(); } interface InlayHintsParams { @@ -55,95 +41,79 @@ const typeHintDecorationType = vscode.window.createTextEditorDecorationType({ }); class HintsUpdater { - private displayHints = true; + private pending: Map = new Map(); + private ctx: Ctx; + private enabled = true; - public async toggleHintsDisplay(displayHints: boolean): Promise { - if (this.displayHints !== displayHints) { - this.displayHints = displayHints; - return this.refreshVisibleEditorsHints( - displayHints ? undefined : [], - ); + constructor(ctx: Ctx) { + this.ctx = ctx; + } + + async setEnabled(enabled: boolean) { + if (this.enabled == enabled) return; + this.enabled = enabled; + + if (this.enabled) { + await this.refresh(); + } else { + this.allEditors.forEach(it => this.setDecorations(it, [])); } } - public async refreshHintsForVisibleEditors( - cause?: vscode.TextDocumentChangeEvent, - ): Promise { - if (!this.displayHints) return; - - if ( - cause !== undefined && - (cause.contentChanges.length === 0 || - !this.isRustDocument(cause.document)) - ) { - return; - } - return this.refreshVisibleEditorsHints(); + async refresh() { + if (!this.enabled) return; + const promises = this.allEditors.map(it => this.refreshEditor(it)); + await Promise.all(promises); } - private async refreshVisibleEditorsHints( - newDecorations?: vscode.DecorationOptions[], - ) { - const promises: Array> = []; - - for (const rustEditor of vscode.window.visibleTextEditors.filter( - editor => this.isRustDocument(editor.document), - )) { - if (newDecorations !== undefined) { - promises.push( - Promise.resolve( - rustEditor.setDecorations( - typeHintDecorationType, - newDecorations, - ), - ), - ); - } else { - promises.push(this.updateDecorationsFromServer(rustEditor)); - } - } - - for (const promise of promises) { - await promise; - } - } - - private isRustDocument(document: vscode.TextDocument): boolean { - return document && document.languageId === 'rust'; - } - - private async updateDecorationsFromServer( - editor: vscode.TextEditor, - ): Promise { + private async refreshEditor(editor: vscode.TextEditor): Promise { const newHints = await this.queryHints(editor.document.uri.toString()); - if (newHints !== null) { - const newDecorations = newHints.map(hint => ({ - range: hint.range, - renderOptions: { - after: { - contentText: `: ${hint.label}`, - }, + if (newHints == null) return; + const newDecorations = newHints.map(hint => ({ + range: hint.range, + renderOptions: { + after: { + contentText: `: ${hint.label}`, }, - })); - return editor.setDecorations( - typeHintDecorationType, - newDecorations, - ); - } + }, + })); + this.setDecorations(editor, newDecorations); + } + + private get allEditors(): vscode.TextEditor[] { + return vscode.window.visibleTextEditors.filter( + editor => editor.document.languageId === 'rust', + ); + } + + private setDecorations( + editor: vscode.TextEditor, + decorations: vscode.DecorationOptions[], + ) { + editor.setDecorations( + typeHintDecorationType, + this.enabled ? decorations : [], + ); } private async queryHints(documentUri: string): Promise { const request: InlayHintsParams = { textDocument: { uri: documentUri }, }; - const client = Server.client; - return client - .onReady() - .then(() => - client.sendRequest( - 'rust-analyzer/inlayHints', - request, - ), + let tokenSource = new vscode.CancellationTokenSource(); + let prev = this.pending.get(documentUri); + if (prev) prev.cancel() + this.pending.set(documentUri, tokenSource); + try { + return await this.ctx.sendRequestWithRetry( + 'rust-analyzer/inlayHints', + request, + tokenSource.token, ); + } finally { + if (!tokenSource.token.isCancellationRequested) { + this.pending.delete(documentUri) + } + } } } diff --git a/editors/code/src/main.ts b/editors/code/src/main.ts index 7e63a9cac23e..345ae068566e 100644 --- a/editors/code/src/main.ts +++ b/editors/code/src/main.ts @@ -4,10 +4,10 @@ import * as lc from 'vscode-languageclient'; import * as commands from './commands'; import { activateInlayHints } from './inlay_hints'; import { StatusDisplay } from './status_display'; -import * as events from './events'; import * as notifications from './notifications'; import { Server } from './server'; import { Ctx } from './ctx'; +import { activateHighlighting } from './highlighting'; let ctx!: Ctx; @@ -28,15 +28,15 @@ export async function activate(context: vscode.ExtensionContext) { ctx.registerCommand('runSingle', commands.runSingle); ctx.registerCommand('showReferences', commands.showReferences); - if (Server.config.enableEnhancedTyping) { + if (ctx.config.enableEnhancedTyping) { ctx.overrideCommand('type', commands.onEnter); } - const watchStatus = new StatusDisplay( - Server.config.cargoWatchOptions.command, - ); + const watchStatus = new StatusDisplay(ctx.config.cargoWatchOptions.command); ctx.pushCleanup(watchStatus); + activateHighlighting(ctx); + // Notifications are events triggered by the language server const allNotifications: [string, lc.GenericNotificationHandler][] = [ [ @@ -49,11 +49,6 @@ export async function activate(context: vscode.ExtensionContext) { ], ]; - // The events below are plain old javascript events, triggered and handled by vscode - vscode.window.onDidChangeActiveTextEditor( - events.changeActiveTextEditor.makeHandler(), - ); - const startServer = () => Server.start(allNotifications); const reloadCommand = () => reloadServer(startServer); @@ -66,7 +61,7 @@ export async function activate(context: vscode.ExtensionContext) { vscode.window.showErrorMessage(e.message); } - if (Server.config.displayInlayHints) { + if (ctx.config.displayInlayHints) { activateInlayHints(ctx); } }