Skip to content

Commit e1eacce

Browse files
authored
perf: auto import cache (#2237)
Add `typescript-auto-import-cache` to more cache auto imports more reliably #2232 #2193
1 parent 5c08ff6 commit e1eacce

File tree

10 files changed

+454
-159
lines changed

10 files changed

+454
-159
lines changed

packages/language-server/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
"svelte-preprocess": "~5.1.0",
5959
"svelte2tsx": "workspace:~",
6060
"typescript": "^5.3.2",
61+
"typescript-auto-import-cache": "^0.3.2",
6162
"vscode-css-languageservice": "~6.2.10",
6263
"vscode-html-languageservice": "~5.1.1",
6364
"vscode-languageserver": "8.0.2",

packages/language-server/src/ls-config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,11 @@ export class LSConfigManager {
440440
config.suggest?.objectLiteralMethodSnippets?.enabled ?? true,
441441
preferTypeOnlyAutoImports: config.preferences?.preferTypeOnlyAutoImports,
442442

443+
// Although we don't support incompletion cache.
444+
// But this will make ts resolve the module specifier more aggressively
445+
// Which also makes the completion label detail show up in more cases
446+
allowIncompleteCompletions: true,
447+
443448
includeInlayEnumMemberValueHints: inlayHints?.enumMemberValues?.enabled,
444449
includeInlayFunctionLikeReturnTypeHints: inlayHints?.functionLikeReturnTypes?.enabled,
445450
includeInlayParameterNameHints: inlayHints?.parameterNames?.enabled,

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

Lines changed: 112 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { dirname } from 'path';
1+
import { dirname, join } from 'path';
22
import ts from 'typescript';
33
import { TextDocumentContentChangeEvent } from 'vscode-languageserver';
44
import { Document, DocumentManager } from '../../lib/documents';
@@ -19,8 +19,10 @@ import {
1919
LanguageServiceContainer,
2020
LanguageServiceDocumentContext
2121
} from './service';
22+
import { createProjectService } from './serviceCache';
2223
import { GlobalSnapshotsManager, SnapshotManager } from './SnapshotManager';
2324
import { isSubPath } from './utils';
25+
import { FileMap } from '../../lib/documents/fileCollection';
2426

2527
interface LSAndTSDocResolverOptions {
2628
notifyExceedSizeLimit?: () => void;
@@ -71,6 +73,40 @@ export class LSAndTSDocResolver {
7173
this.getCanonicalFileName = createGetCanonicalFileName(
7274
(options?.tsSystem ?? ts.sys).useCaseSensitiveFileNames
7375
);
76+
77+
this.tsSystem = this.wrapWithPackageJsonMonitoring(this.options?.tsSystem ?? ts.sys);
78+
this.globalSnapshotsManager = new GlobalSnapshotsManager(this.tsSystem);
79+
this.userPreferencesAccessor = { preferences: this.getTsUserPreferences() };
80+
const projectService = createProjectService(this.tsSystem, this.userPreferencesAccessor);
81+
82+
configManager.onChange(() => {
83+
const newPreferences = this.getTsUserPreferences();
84+
const autoImportConfigChanged =
85+
newPreferences.includePackageJsonAutoImports !==
86+
this.userPreferencesAccessor.preferences.includePackageJsonAutoImports;
87+
88+
this.userPreferencesAccessor.preferences = newPreferences;
89+
90+
if (autoImportConfigChanged) {
91+
forAllServices((service) => {
92+
service.onAutoImportProviderSettingsChanged();
93+
});
94+
}
95+
});
96+
97+
this.watchers = new FileMap(this.tsSystem.useCaseSensitiveFileNames);
98+
this.lsDocumentContext = {
99+
ambientTypesSource: this.options?.isSvelteCheck ? 'svelte-check' : 'svelte2tsx',
100+
createDocument: this.createDocument,
101+
transformOnTemplateError: !this.options?.isSvelteCheck,
102+
globalSnapshotsManager: this.globalSnapshotsManager,
103+
notifyExceedSizeLimit: this.options?.notifyExceedSizeLimit,
104+
extendedConfigCache: this.extendedConfigCache,
105+
onProjectReloaded: this.options?.onProjectReloaded,
106+
watchTsConfig: !!this.options?.watch,
107+
tsSystem: this.tsSystem,
108+
projectService: projectService
109+
};
74110
}
75111

76112
/**
@@ -89,26 +125,15 @@ export class LSAndTSDocResolver {
89125
return document;
90126
};
91127

92-
private globalSnapshotsManager = new GlobalSnapshotsManager(
93-
this.lsDocumentContext.tsSystem,
94-
/* watchPackageJson */ !!this.options?.watch
95-
);
128+
private tsSystem: ts.System;
129+
private globalSnapshotsManager: GlobalSnapshotsManager;
96130
private extendedConfigCache = new Map<string, ts.ExtendedConfigCacheEntry>();
97131
private getCanonicalFileName: GetCanonicalFileName;
98132

99-
private get lsDocumentContext(): LanguageServiceDocumentContext {
100-
return {
101-
ambientTypesSource: this.options?.isSvelteCheck ? 'svelte-check' : 'svelte2tsx',
102-
createDocument: this.createDocument,
103-
transformOnTemplateError: !this.options?.isSvelteCheck,
104-
globalSnapshotsManager: this.globalSnapshotsManager,
105-
notifyExceedSizeLimit: this.options?.notifyExceedSizeLimit,
106-
extendedConfigCache: this.extendedConfigCache,
107-
onProjectReloaded: this.options?.onProjectReloaded,
108-
watchTsConfig: !!this.options?.watch,
109-
tsSystem: this.options?.tsSystem ?? ts.sys
110-
};
111-
}
133+
private userPreferencesAccessor: { preferences: ts.UserPreferences };
134+
private readonly watchers: FileMap<ts.FileWatcher>;
135+
136+
private lsDocumentContext: LanguageServiceDocumentContext;
112137

113138
async getLSForPath(path: string) {
114139
return (await this.getTSService(path)).getService();
@@ -251,4 +276,73 @@ export class LSAndTSDocResolver {
251276
nearestWorkspaceUri ? urlToPath(nearestWorkspaceUri) : null
252277
);
253278
}
279+
280+
private getTsUserPreferences() {
281+
return this.configManager.getTsUserPreferences('typescript', null);
282+
}
283+
284+
private wrapWithPackageJsonMonitoring(sys: ts.System): ts.System {
285+
if (!sys.watchFile || !this.options?.watch) {
286+
return sys;
287+
}
288+
289+
const watchFile = sys.watchFile;
290+
return {
291+
...sys,
292+
readFile: (path, encoding) => {
293+
if (path.endsWith('package.json') && !this.watchers.has(path)) {
294+
this.watchers.set(
295+
path,
296+
watchFile(path, this.onPackageJsonWatchChange.bind(this), 3_000)
297+
);
298+
}
299+
300+
return sys.readFile(path, encoding);
301+
}
302+
};
303+
}
304+
305+
private onPackageJsonWatchChange(path: string, onWatchChange: ts.FileWatcherEventKind) {
306+
const dir = dirname(path);
307+
const projectService = this.lsDocumentContext.projectService;
308+
const packageJsonCache = projectService?.packageJsonCache;
309+
const normalizedPath = projectService?.toPath(path);
310+
311+
if (onWatchChange === ts.FileWatcherEventKind.Deleted) {
312+
this.watchers.get(path)?.close();
313+
this.watchers.delete(path);
314+
packageJsonCache?.delete(normalizedPath);
315+
} else {
316+
packageJsonCache?.addOrUpdate(normalizedPath);
317+
}
318+
319+
forAllServices((service) => {
320+
service.onPackageJsonChange(path);
321+
});
322+
if (!path.includes('node_modules')) {
323+
return;
324+
}
325+
326+
setTimeout(() => {
327+
this.updateSnapshotsInDirectory(dir);
328+
const realPath =
329+
this.tsSystem.realpath &&
330+
this.getCanonicalFileName(normalizePath(this.tsSystem.realpath?.(dir)));
331+
332+
// pnpm
333+
if (realPath && realPath !== dir) {
334+
this.updateSnapshotsInDirectory(realPath);
335+
const realPkgPath = join(realPath, 'package.json');
336+
forAllServices((service) => {
337+
service.onPackageJsonChange(realPkgPath);
338+
});
339+
}
340+
}, 500);
341+
}
342+
343+
private updateSnapshotsInDirectory(dir: string) {
344+
this.globalSnapshotsManager.getByPrefix(dir).forEach((snapshot) => {
345+
this.globalSnapshotsManager.updateTsOrJsFile(snapshot.filePath);
346+
});
347+
}
254348
}

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

Lines changed: 1 addition & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import { TextDocumentContentChangeEvent } from 'vscode-languageserver';
55
import { createGetCanonicalFileName, GetCanonicalFileName, normalizePath } from '../../utils';
66
import { EventEmitter } from 'events';
77
import { FileMap } from '../../lib/documents/fileCollection';
8-
import { dirname } from 'path';
98

109
type SnapshotChangeHandler = (fileName: string, newDocument: DocumentSnapshot | undefined) => void;
1110

@@ -18,20 +17,10 @@ export class GlobalSnapshotsManager {
1817
private emitter = new EventEmitter();
1918
private documents: FileMap<DocumentSnapshot>;
2019
private getCanonicalFileName: GetCanonicalFileName;
21-
private packageJsonCache: PackageJsonCache;
2220

23-
constructor(
24-
private readonly tsSystem: ts.System,
25-
watchPackageJson = false
26-
) {
21+
constructor(private readonly tsSystem: ts.System) {
2722
this.documents = new FileMap(tsSystem.useCaseSensitiveFileNames);
2823
this.getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames);
29-
this.packageJsonCache = new PackageJsonCache(
30-
tsSystem,
31-
watchPackageJson,
32-
this.getCanonicalFileName,
33-
this.updateSnapshotsInDirectory.bind(this)
34-
);
3524
}
3625

3726
get(fileName: string) {
@@ -94,16 +83,6 @@ export class GlobalSnapshotsManager {
9483
removeChangeListener(listener: SnapshotChangeHandler) {
9584
this.emitter.off('change', listener);
9685
}
97-
98-
getPackageJson(path: string) {
99-
return this.packageJsonCache.getPackageJson(path);
100-
}
101-
102-
private updateSnapshotsInDirectory(dir: string) {
103-
this.getByPrefix(dir).forEach((snapshot) => {
104-
this.updateTsOrJsFile(snapshot.filePath);
105-
});
106-
}
10786
}
10887

10988
export interface TsFilesSpec {
@@ -267,76 +246,3 @@ export class SnapshotManager {
267246
}
268247

269248
export const ignoredBuildDirectories = ['__sapper__', '.svelte-kit'];
270-
271-
class PackageJsonCache {
272-
constructor(
273-
private readonly tsSystem: ts.System,
274-
private readonly watchPackageJson: boolean,
275-
private readonly getCanonicalFileName: GetCanonicalFileName,
276-
private readonly updateSnapshotsInDirectory: (directory: string) => void
277-
) {
278-
this.watchers = new FileMap(tsSystem.useCaseSensitiveFileNames);
279-
}
280-
281-
private readonly watchers: FileMap<ts.FileWatcher>;
282-
283-
private packageJsonCache = new FileMap<
284-
{ text: string; modifiedTime: number | undefined } | undefined
285-
>();
286-
287-
getPackageJson(path: string) {
288-
if (!this.packageJsonCache.has(path)) {
289-
this.packageJsonCache.set(path, this.initWatcherAndRead(path));
290-
}
291-
292-
return this.packageJsonCache.get(path);
293-
}
294-
295-
private initWatcherAndRead(path: string) {
296-
if (this.watchPackageJson) {
297-
this.tsSystem.watchFile?.(path, this.onPackageJsonWatchChange.bind(this), 3_000);
298-
}
299-
const exist = this.tsSystem.fileExists(path);
300-
301-
if (!exist) {
302-
return undefined;
303-
}
304-
305-
return this.readPackageJson(path);
306-
}
307-
308-
private readPackageJson(path: string) {
309-
return {
310-
text: this.tsSystem.readFile(path) ?? '',
311-
modifiedTime: this.tsSystem.getModifiedTime?.(path)?.valueOf()
312-
};
313-
}
314-
315-
private onPackageJsonWatchChange(path: string, onWatchChange: ts.FileWatcherEventKind) {
316-
const dir = dirname(path);
317-
318-
if (onWatchChange === ts.FileWatcherEventKind.Deleted) {
319-
this.packageJsonCache.delete(path);
320-
this.watchers.get(path)?.close();
321-
this.watchers.delete(path);
322-
} else {
323-
this.packageJsonCache.set(path, this.readPackageJson(path));
324-
}
325-
326-
if (!path.includes('node_modules')) {
327-
return;
328-
}
329-
330-
setTimeout(() => {
331-
this.updateSnapshotsInDirectory(dir);
332-
const realPath =
333-
this.tsSystem.realpath &&
334-
this.getCanonicalFileName(normalizePath(this.tsSystem.realpath?.(dir)));
335-
336-
// pnpm
337-
if (realPath && realPath !== dir) {
338-
this.updateSnapshotsInDirectory(realPath);
339-
}
340-
}, 500);
341-
}
342-
}

packages/language-server/src/plugins/typescript/module-loader.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,8 @@ export function createSvelteModuleLoader(
208208
resolveModuleNames,
209209
resolveTypeReferenceDirectiveReferences,
210210
mightHaveInvalidatedResolutions,
211-
clearPendingInvalidations
211+
clearPendingInvalidations,
212+
getModuleResolutionCache: () => tsModuleCache
212213
};
213214

214215
function resolveModuleNames(

0 commit comments

Comments
 (0)