diff --git a/packages/language-server/src/plugins/PluginHost.ts b/packages/language-server/src/plugins/PluginHost.ts index 54fbb29fd..6f15f630f 100644 --- a/packages/language-server/src/plugins/PluginHost.ts +++ b/packages/language-server/src/plugins/PluginHost.ts @@ -53,6 +53,7 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { filterIncompleteCompletions: true, definitionLinkSupport: false }; + private deferredRequests: Record]> = {}; constructor(private documentsManager: DocumentManager) {} @@ -64,6 +65,10 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { this.plugins.push(plugin); } + didUpdateDocument() { + this.deferredRequests = {}; + } + async getDiagnostics(textDocument: TextDocumentIdentifier): Promise { const document = this.getDocument(textDocument.uri); if (!document) { @@ -71,7 +76,12 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { } return flatten( - await this.execute('getDiagnostics', [document], ExecuteMode.Collect) + await this.execute( + 'getDiagnostics', + [document], + ExecuteMode.Collect, + 'high' + ) ); } @@ -81,7 +91,12 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { throw new Error('Cannot call methods on an unopened document'); } - return this.execute('doHover', [document, position], ExecuteMode.FirstNonNull); + return this.execute( + 'doHover', + [document, position], + ExecuteMode.FirstNonNull, + 'high' + ); } async getCompletions( @@ -99,7 +114,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { await this.execute( 'getCompletions', [document, position, completionContext, cancellationToken], - ExecuteMode.Collect + ExecuteMode.Collect, + 'high' ) ).filter((completion) => completion != null); @@ -141,7 +157,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { const result = await this.execute( 'resolveCompletion', [document, completionItem, cancellationToken], - ExecuteMode.FirstNonNull + ExecuteMode.FirstNonNull, + 'high' ); return result ?? completionItem; @@ -160,7 +177,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { await this.execute( 'formatDocument', [document, options], - ExecuteMode.Collect + ExecuteMode.Collect, + 'high' ) ); } @@ -177,7 +195,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { return this.execute( 'doTagComplete', [document, position], - ExecuteMode.FirstNonNull + ExecuteMode.FirstNonNull, + 'high' ); } @@ -191,7 +210,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { await this.execute( 'getDocumentColors', [document], - ExecuteMode.Collect + ExecuteMode.Collect, + 'low' ) ); } @@ -210,7 +230,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { await this.execute( 'getColorPresentations', [document, range, color], - ExecuteMode.Collect + ExecuteMode.Collect, + 'low' ) ); } @@ -228,7 +249,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { await this.execute( 'getDocumentSymbols', [document, cancellationToken], - ExecuteMode.Collect + ExecuteMode.Collect, + 'low' ) ); } @@ -246,7 +268,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { await this.execute( 'getDefinitions', [document, position], - ExecuteMode.Collect + ExecuteMode.Collect, + 'high' ) ); @@ -274,7 +297,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { await this.execute( 'getCodeActions', [document, range, context, cancellationToken], - ExecuteMode.Collect + ExecuteMode.Collect, + 'high' ) ); } @@ -292,7 +316,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { return await this.execute( 'executeCommand', [document, command, args], - ExecuteMode.FirstNonNull + ExecuteMode.FirstNonNull, + 'high' ); } @@ -300,7 +325,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { return await this.execute( 'updateImports', [fileRename], - ExecuteMode.FirstNonNull + ExecuteMode.FirstNonNull, + 'high' ); } @@ -316,7 +342,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { return await this.execute( 'prepareRename', [document, position], - ExecuteMode.FirstNonNull + ExecuteMode.FirstNonNull, + 'high' ); } @@ -333,7 +360,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { return await this.execute( 'rename', [document, position, newName], - ExecuteMode.FirstNonNull + ExecuteMode.FirstNonNull, + 'high' ); } @@ -350,7 +378,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { return await this.execute( 'findReferences', [document, position, context], - ExecuteMode.FirstNonNull + ExecuteMode.FirstNonNull, + 'high' ); } @@ -368,7 +397,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { return await this.execute( 'getSignatureHelp', [document, position, context, cancellationToken], - ExecuteMode.FirstNonNull + ExecuteMode.FirstNonNull, + 'high' ); } @@ -424,7 +454,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { return await this.execute( 'getSemanticTokens', [document, range, cancellationToken], - ExecuteMode.FirstNonNull + ExecuteMode.FirstNonNull, + 'low' ); } @@ -440,7 +471,8 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { return await this.execute( 'getLinkedEditingRanges', [document, position], - ExecuteMode.FirstNonNull + ExecuteMode.FirstNonNull, + 'high' ); } @@ -463,21 +495,71 @@ export class PluginHost implements LSProvider, OnWatchFileChanges { private execute( name: keyof LSProvider, args: any[], - mode: ExecuteMode.FirstNonNull + mode: ExecuteMode.FirstNonNull, + priority: 'low' | 'high' ): Promise; private execute( name: keyof LSProvider, args: any[], - mode: ExecuteMode.Collect + mode: ExecuteMode.Collect, + priority: 'low' | 'high' ): Promise; - private execute(name: keyof LSProvider, args: any[], mode: ExecuteMode.None): Promise; + private execute( + name: keyof LSProvider, + args: any[], + mode: ExecuteMode.None, + priority: 'low' | 'high' + ): Promise; private async execute( name: keyof LSProvider, args: any[], - mode: ExecuteMode + mode: ExecuteMode, + priority: 'low' | 'high' ): Promise<(T | null) | T[] | void> { const plugins = this.plugins.filter((plugin) => typeof plugin[name] === 'function'); + if (priority === 'low') { + // If a request doesn't have priority, we first wait 1 second to + // 1. let higher priority requests get through first + // 2. wait for possible document changes, which make the request wait again + // Due to waiting, low priority items should preferrably be those who do not + // rely on positions or ranges and rather on the whole document only. + const debounce = async (): Promise => { + const id = Math.random(); + this.deferredRequests[name] = [ + id, + new Promise((resolve, reject) => { + setTimeout(() => { + if ( + !this.deferredRequests[name] || + this.deferredRequests[name][0] === id + ) { + resolve(); + } else { + // We should not get into this case. According to the spec, + // the language client // does not send another request + // of the same type until the previous one is answered. + reject(); + } + }, 1000); + }) + ]; + try { + await this.deferredRequests[name][1]; + if (!this.deferredRequests[name]) { + return debounce(); + } + return true; + } catch (e) { + return false; + } + }; + const shouldContinue = await debounce(); + if (!shouldContinue) { + return; + } + } + switch (mode) { case ExecuteMode.FirstNonNull: for (const plugin of plugins) { diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index d549d485f..ddf070a14 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -262,9 +262,10 @@ export function startServer(options?: LSOptions) { }); connection.onDidCloseTextDocument((evt) => docManager.closeDocument(evt.textDocument.uri)); - connection.onDidChangeTextDocument((evt) => - docManager.updateDocument(evt.textDocument, evt.contentChanges) - ); + connection.onDidChangeTextDocument((evt) => { + docManager.updateDocument(evt.textDocument, evt.contentChanges); + pluginHost.didUpdateDocument(); + }); connection.onHover((evt) => pluginHost.doHover(evt.textDocument, evt.position)); connection.onCompletion((evt, cancellationToken) => pluginHost.getCompletions(evt.textDocument, evt.position, evt.context, cancellationToken)