Skip to content

Commit 81f61e5

Browse files
authored
(perf) improvements around diagnostics and completions (#1022)
- Better debounce of diagnostics to not call them more often than needed - Try to reuse old completions and handle the special case of import completions #1017
1 parent 268ae46 commit 81f61e5

File tree

5 files changed

+92
-8
lines changed

5 files changed

+92
-8
lines changed

packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class LSAndTSDocResolver {
2828
) {
2929
const handleDocumentChange = (document: Document) => {
3030
// This refreshes the document in the ts language service
31-
this.getLSAndTSDoc(document);
31+
this.getSnapshot(document);
3232
};
3333
docManager.on(
3434
'documentChange',

packages/language-server/src/plugins/typescript/features/CompletionProvider.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,20 @@ import {
3131
scriptElementKindToCompletionItemKind
3232
} from '../utils';
3333
import { getJsDocTemplateCompletion } from './getJsDocTemplateCompletion';
34-
import { getComponentAtPosition } from './utils';
34+
import { getComponentAtPosition, isPartOfImportStatement } from './utils';
3535

3636
export interface CompletionEntryWithIdentifer extends ts.CompletionEntry, TextDocumentIdentifier {
3737
position: Position;
3838
}
3939

4040
type validTriggerCharacter = '.' | '"' | "'" | '`' | '/' | '@' | '<' | '#';
4141

42+
type LastCompletion = {
43+
key: string;
44+
position: Position;
45+
completionList: AppCompletionList<CompletionEntryWithIdentifer> | null;
46+
};
47+
4248
export class CompletionsProviderImpl implements CompletionsProvider<CompletionEntryWithIdentifer> {
4349
constructor(private readonly lsAndTsDocResolver: LSAndTSDocResolver) {}
4450

@@ -48,6 +54,10 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
4854
* Therefore, only use the characters the typescript compiler treats as valid.
4955
*/
5056
private readonly validTriggerCharacters = ['.', '"', "'", '`', '/', '@', '<', '#'] as const;
57+
/**
58+
* For performance reasons, try to reuse the last completion if possible.
59+
*/
60+
private lastCompletion?: LastCompletion;
5161

5262
private isValidTriggerCharacter(
5363
character: string | undefined
@@ -94,6 +104,21 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
94104
return null;
95105
}
96106

107+
if (
108+
this.canReuseLastCompletion(
109+
this.lastCompletion,
110+
triggerKind,
111+
triggerCharacter,
112+
document,
113+
position
114+
)
115+
) {
116+
this.lastCompletion.position = position;
117+
return this.lastCompletion.completionList;
118+
} else {
119+
this.lastCompletion = undefined;
120+
}
121+
97122
const fragment = await tsDoc.getFragment();
98123
if (!fragment.isInGenerated(position)) {
99124
return null;
@@ -143,7 +168,28 @@ export class CompletionsProviderImpl implements CompletionsProvider<CompletionEn
143168
.map((comp) => mapCompletionItemToOriginal(fragment, comp))
144169
.concat(eventCompletions);
145170

146-
return CompletionList.create(completionItems, !!tsDoc.parserError);
171+
const completionList = CompletionList.create(completionItems, !!tsDoc.parserError);
172+
this.lastCompletion = { key: document.getFilePath() || '', position, completionList };
173+
return completionList;
174+
}
175+
176+
private canReuseLastCompletion(
177+
lastCompletion: LastCompletion | undefined,
178+
triggerKind: number | undefined,
179+
triggerCharacter: string | undefined,
180+
document: Document,
181+
position: Position
182+
): lastCompletion is LastCompletion {
183+
return (
184+
!!lastCompletion &&
185+
lastCompletion.key === document.getFilePath() &&
186+
lastCompletion.position.line === position.line &&
187+
Math.abs(lastCompletion.position.character - position.character) < 2 &&
188+
(triggerKind === CompletionTriggerKind.TriggerForIncompleteCompletions ||
189+
// Special case: `.` is a trigger character, but inside import path completions
190+
// it shouldn't trigger another completion because we can reuse the old one
191+
(triggerCharacter === '.' && isPartOfImportStatement(document.getText(), position)))
192+
);
147193
}
148194

149195
private getExistingImports(document: Document) {

packages/language-server/src/plugins/typescript/features/utils.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
import ts from 'typescript';
22
import { Position } from 'vscode-languageserver';
3-
import { Document, getNodeIfIsInComponentStartTag, isInTag } from '../../../lib/documents';
3+
import {
4+
Document,
5+
getLineAtPosition,
6+
getNodeIfIsInComponentStartTag,
7+
isInTag
8+
} from '../../../lib/documents';
49
import {
510
DocumentSnapshot,
611
SnapshotFragment,
@@ -74,6 +79,11 @@ export function isNoTextSpanInGeneratedCode(text: string, span: ts.TextSpan) {
7479
return !isInGeneratedCode(text, span.start, span.start + span.length);
7580
}
7681

82+
export function isPartOfImportStatement(text: string, position: Position): boolean {
83+
const line = getLineAtPosition(position, text);
84+
return /\s*from\s+["'][^"']*/.test(line.substr(0, position.character));
85+
}
86+
7787
export class SnapshotFragmentMap {
7888
private map = new Map<string, { fragment: SnapshotFragment; snapshot: DocumentSnapshot }>();
7989
constructor(private resolver: LSAndTSDocResolver) {}

packages/language-server/src/server.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import _ from 'lodash';
21
import {
32
ApplyWorkspaceEditParams,
43
ApplyWorkspaceEditRequest,
@@ -34,7 +33,7 @@ import {
3433
OnWatchFileChangesPara,
3534
LSAndTSDocResolver
3635
} from './plugins';
37-
import { isNotNullOrUndefined, urlToPath } from './utils';
36+
import { debounceThrottle, isNotNullOrUndefined, urlToPath } from './utils';
3837
import { FallbackWatcher } from './lib/FallbackWatcher';
3938

4039
namespace TagCloseRequest {
@@ -319,7 +318,7 @@ export function startServer(options?: LSOptions) {
319318
pluginHost.getDiagnostics.bind(pluginHost)
320319
);
321320

322-
const updateAllDiagnostics = _.debounce(() => diagnosticsManager.updateAll(), 1000);
321+
const updateAllDiagnostics = debounceThrottle(() => diagnosticsManager.updateAll(), 1000);
323322

324323
connection.onDidChangeWatchedFiles(onDidChangeWatchedFiles);
325324
function onDidChangeWatchedFiles(para: DidChangeWatchedFilesParams) {
@@ -358,7 +357,7 @@ export function startServer(options?: LSOptions) {
358357

359358
docManager.on(
360359
'documentChange',
361-
_.debounce(async (document: Document) => diagnosticsManager.update(document), 500)
360+
debounceThrottle(async (document: Document) => diagnosticsManager.update(document), 1000)
362361
);
363362
docManager.on('documentClose', (document: Document) =>
364363
diagnosticsManager.removeDiagnostics(document)

packages/language-server/src/utils.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,35 @@ export function debounceSameArg<T>(
8585
};
8686
}
8787

88+
/**
89+
* Debounces a function but also waits at minimum the specified number of miliseconds until
90+
* the next invocation. This avoids needless calls when a synchronous call (like diagnostics)
91+
* took too long and the whole timeout of the next call was eaten up already.
92+
*
93+
* @param fn The function with it's argument
94+
* @param miliseconds Number of miliseconds to debounce/throttle
95+
*/
96+
export function debounceThrottle<T extends (...args: any) => void>(fn: T, miliseconds: number): T {
97+
let timeout: any;
98+
let lastInvocation = Date.now() - miliseconds;
99+
100+
function maybeCall(...args: any) {
101+
clearTimeout(timeout);
102+
103+
timeout = setTimeout(() => {
104+
if (Date.now() - lastInvocation < miliseconds) {
105+
maybeCall(...args);
106+
return;
107+
}
108+
109+
fn(...args);
110+
lastInvocation = Date.now();
111+
}, miliseconds);
112+
}
113+
114+
return maybeCall as any;
115+
}
116+
88117
/**
89118
* Like str.lastIndexOf, but for regular expressions. Note that you need to provide the g-flag to your RegExp!
90119
*/

0 commit comments

Comments
 (0)