diff --git a/src/features/inlay-hints.ts b/src/features/inlay-hints.ts index 4a273aa4..d1d3f350 100644 --- a/src/features/inlay-hints.ts +++ b/src/features/inlay-hints.ts @@ -34,21 +34,21 @@ export class TypeScriptInlayHintsProvider { token?: lsp.CancellationToken, ): Promise { if (tspClient.apiVersion.lt(TypeScriptInlayHintsProvider.minVersion)) { - lspClient.showErrorMessage('Inlay Hints request failed. Requires TypeScript 4.4+.'); + lspClient.showWarningMessage('Inlay Hints request failed. Requires TypeScript 4.4+.'); return []; } const file = uriToPath(uri); if (!file) { - lspClient.showErrorMessage('Inlay Hints request failed. No resource provided.'); + lspClient.showWarningMessage('Inlay Hints request failed. No resource provided.'); return []; } const document = documents.get(file); if (!document) { - lspClient.showErrorMessage('Inlay Hints request failed. File not opened in the editor.'); + lspClient.showWarningMessage('Inlay Hints request failed. File not opened in the editor.'); return []; } diff --git a/src/features/source-definition.ts b/src/features/source-definition.ts index 5b492a63..3711e83e 100644 --- a/src/features/source-definition.ts +++ b/src/features/source-definition.ts @@ -32,26 +32,26 @@ export class SourceDefinitionCommand { token?: lsp.CancellationToken, ): Promise { if (tspClient.apiVersion.lt(SourceDefinitionCommand.minVersion)) { - lspClient.showErrorMessage('Go to Source Definition failed. Requires TypeScript 4.7+.'); + lspClient.showWarningMessage('Go to Source Definition failed. Requires TypeScript 4.7+.'); return; } if (!position || typeof position.character !== 'number' || typeof position.line !== 'number') { - lspClient.showErrorMessage('Go to Source Definition failed. Invalid position.'); + lspClient.showWarningMessage('Go to Source Definition failed. Invalid position.'); return; } let file: string | undefined; if (!uri || typeof uri !== 'string' || !(file = uriToPath(uri))) { - lspClient.showErrorMessage('Go to Source Definition failed. No resource provided.'); + lspClient.showWarningMessage('Go to Source Definition failed. No resource provided.'); return; } const document = documents.get(file); if (!document) { - lspClient.showErrorMessage('Go to Source Definition failed. File not opened in the editor.'); + lspClient.showWarningMessage('Go to Source Definition failed. File not opened in the editor.'); return; } @@ -62,7 +62,7 @@ export class SourceDefinitionCommand { }, async () => { const response = await tspClient.request(CommandTypes.FindSourceDefinition, args, token); if (response.type !== 'response' || !response.body) { - lspClient.showErrorMessage('No source definitions found.'); + lspClient.showWarningMessage('No source definitions found.'); return; } return response.body.map(reference => toLocation(reference, documents)); diff --git a/src/lsp-client.ts b/src/lsp-client.ts index bf154a7f..ea2f9076 100644 --- a/src/lsp-client.ts +++ b/src/lsp-client.ts @@ -19,7 +19,7 @@ export interface LspClient { createProgressReporter(token?: lsp.CancellationToken, workDoneProgress?: lsp.WorkDoneProgressReporter): Promise; withProgress(options: WithProgressOptions, task: (progress: lsp.WorkDoneProgressReporter) => Promise): Promise; publishDiagnostics(args: lsp.PublishDiagnosticsParams): void; - showErrorMessage(message: string): void; + showWarningMessage(message: string): void; logMessage(args: lsp.LogMessageParams): void; applyWorkspaceEdit(args: lsp.ApplyWorkspaceEditParams): Promise; rename(args: lsp.TextDocumentPositionParams): Promise; @@ -55,8 +55,8 @@ export class LspClientImpl implements LspClient { this.connection.sendDiagnostics(params); } - showErrorMessage(message: string): void { - this.connection.sendNotification(lsp.ShowMessageNotification.type, { type: MessageType.Error, message }); + showWarningMessage(message: string): void { + this.connection.sendRequest(lsp.ShowMessageRequest.type, { type: MessageType.Warning, message }); } logMessage(args: lsp.LogMessageParams): void { diff --git a/src/lsp-server.ts b/src/lsp-server.ts index cc7c9e00..b41ab355 100644 --- a/src/lsp-server.ts +++ b/src/lsp-server.ts @@ -40,6 +40,8 @@ import { Position, Range } from './utils/typeConverters.js'; import { CodeActionKind } from './utils/types.js'; import { ConfigurationManager } from './configuration-manager.js'; +const LARGE_NUMBER_OF_COMPLETIONS_WARNING_TRIGGER = 10000; + export class LspServer { private _tspClient: TspClient | null = null; private hasShutDown = false; @@ -666,9 +668,36 @@ export class LspServer { optionalReplacementRange: optionalReplacementSpan ? Range.fromTextSpan(optionalReplacementSpan) : undefined, }; const completions = asCompletionItems(entries, this.completionDataCache, file, params.position, document, this.documents, completionOptions, this.features, completionContext); + + if (entries.length > LARGE_NUMBER_OF_COMPLETIONS_WARNING_TRIGGER) { + setImmediate(() => this.triggerLargeNumberOfCompletionsWarning(entries)); + } + return lsp.CompletionList.create(completions, isIncomplete); } + private triggerLargeNumberOfCompletionsWarning(entries: readonly ts.server.protocol.CompletionEntry[]): void { + const largeCompletionsMap = new Map(); + for (const entry of entries) { + if (!entry.source) { + continue; + } + + const { source } = entry; + const count = largeCompletionsMap.get(source) || 0; + largeCompletionsMap.set(source, count + 1); + } + + const largeCompletionsList: [string, number][] = []; + for (const [key, count] of largeCompletionsMap.entries()) { + largeCompletionsList.push([key, count]); + } + + largeCompletionsList.sort((a, b) => b[1] - a[1]).splice(25); + const table = largeCompletionsList.map(([key, count]) => ` ${key}: ${count}`).join('\n'); + this.options.lspClient.showWarningMessage(`Large number (${entries.length}) of completions received.\n\nModules contributing most completions:\nConsider ignoring the biggest offenders with the \`autoImportFileExcludePatterns\` option. ${table}`); + } + async completionResolve(item: lsp.CompletionItem, token?: lsp.CancellationToken): Promise { this.logger.log('completion/resolve', item); item.data = item.data?.cacheId !== undefined ? this.completionDataCache.get(item.data.cacheId) : item.data; diff --git a/src/test-utils.ts b/src/test-utils.ts index 158850e1..9d7b9da2 100644 --- a/src/test-utils.ts +++ b/src/test-utils.ts @@ -143,7 +143,7 @@ export class TestLspClient implements LspClient { return this.options.publishDiagnostics(args); } - showErrorMessage(message: string): void { + showWarningMessage(message: string): void { this.logger.error(`[showErrorMessage] ${message}`); }