From 6752119d804131f917d96a2afae2e22b0415b8ea Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Thu, 21 Dec 2023 17:47:12 +0800 Subject: [PATCH 01/15] wip auto-import-cache --- packages/language-server/package.json | 1 + .../plugins/typescript/LSAndTSDocResolver.ts | 11 ++++ .../src/plugins/typescript/service.ts | 53 +++++++++++++++++-- pnpm-lock.yaml | 9 ++++ 4 files changed, 71 insertions(+), 3 deletions(-) diff --git a/packages/language-server/package.json b/packages/language-server/package.json index fc66d7b54..612883700 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -58,6 +58,7 @@ "svelte-preprocess": "~5.1.0", "svelte2tsx": "workspace:~", "typescript": "^5.3.2", + "typescript-auto-import-cache": "^0.3.0", "vscode-css-languageservice": "~6.2.10", "vscode-html-languageservice": "~5.1.1", "vscode-languageserver": "8.0.2", diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index 2d438cedb..45cb586d7 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -71,6 +71,10 @@ export class LSAndTSDocResolver { this.getCanonicalFileName = createGetCanonicalFileName( (options?.tsSystem ?? ts.sys).useCaseSensitiveFileNames ); + + configManager.onChange(() => { + this.configChanged = true; + }); } /** @@ -96,6 +100,8 @@ export class LSAndTSDocResolver { private extendedConfigCache = new Map(); private getCanonicalFileName: GetCanonicalFileName; + private configChanged = true; + private get lsDocumentContext(): LanguageServiceDocumentContext { return { ambientTypesSource: this.options?.isSvelteCheck ? 'svelte-check' : 'svelte2tsx', @@ -121,6 +127,11 @@ export class LSAndTSDocResolver { }> { const { tsDoc, lsContainer, userPreferences } = await this.getLSAndTSDocWorker(document); + if (this.configChanged) { + this.configChanged = false; + lsContainer.setUserPreferences(userPreferences); + } + return { tsDoc, lang: lsContainer.getService(), userPreferences }; } diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 09cac7fba..e0f3da4ee 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -21,6 +21,7 @@ import { hasTsExtensions, isSvelteFilePath } from './utils'; +import { createLanguageService as createLanguageServiceWithCache } from 'typescript-auto-import-cache'; export interface LanguageServiceContainer { readonly tsconfigPath: string; @@ -47,6 +48,8 @@ export interface LanguageServiceContainer { */ fileBelongsToProject(filePath: string, isNew: boolean): boolean; + setUserPreferences(preferences: ts.UserPreferences): void; + dispose(): void; } @@ -302,7 +305,6 @@ async function createLanguageService( tsSystem.useCaseSensitiveFileNames ); - const languageService = ts.createLanguageService(host, documentRegistry); const transformationConfig: SvelteSnapshotOptions = { parse: svelteCompiler?.parse, version: svelteCompiler?.VERSION, @@ -310,6 +312,14 @@ async function createLanguageService( typingsNamespace: raw?.svelteOptions?.namespace || 'svelteHTML' }; + const serviceWithCache = createLanguageServiceWithCache( + ts as any, + tsSystemWithPackageJsonCache, + host, + () => ts.createLanguageService(host, documentRegistry) + ); + const { languageService } = serviceWithCache; + docContext.globalSnapshotsManager.onChange(scheduleUpdate); reduceLanguageServiceCapabilityIfFileSizeTooBig(); @@ -329,6 +339,7 @@ async function createLanguageService( fileBelongsToProject, snapshotManager, invalidateModuleCache, + setUserPreferences, dispose }; @@ -368,6 +379,8 @@ async function createLanguageService( if (!prevSnapshot) { svelteModuleLoader.deleteUnresolvedResolutionsFromCache(filePath); + // @ts-expect-error + host?.getCachedExportInfoMap()?.clear(); } const newSnapshot = DocumentSnapshot.fromDocument(document, transformationConfig); @@ -443,9 +456,20 @@ async function createLanguageService( function updateProjectFiles(): void { projectVersion++; dirty = true; - const projectFileCountBefore = snapshotManager.getProjectFileNames().length; + const projectFileBefore = snapshotManager.getProjectFileNames(); + const projectFileCountBefore = projectFileBefore.length; snapshotManager.updateProjectFiles(); - const projectFileCountAfter = snapshotManager.getProjectFileNames().length; + const projectFileAfter = snapshotManager.getProjectFileNames(); + const projectFileCountAfter = projectFileAfter.length; + + const hasAddedOrRemoved = + projectFileCountAfter !== projectFileCountBefore || + checkProjectFileUpdate(projectFileBefore, projectFileAfter); + + if (hasAddedOrRemoved) { + // @ts-expect-error + host?.getCachedExportInfoMap()?.clear(); + } if (projectFileCountAfter <= projectFileCountBefore) { return; @@ -454,6 +478,25 @@ async function createLanguageService( reduceLanguageServiceCapabilityIfFileSizeTooBig(); } + function checkProjectFileUpdate(oldFiles: string[], newFiles: string[]) { + const oldSet = new Set(oldFiles); + const newSet = new Set(newFiles); + + for (const file of oldSet) { + if (!newSet.has(file)) { + return true; + } + } + + for (const file of newSet) { + if (!oldSet.has(file)) { + return true; + } + } + + return false; + } + function getScriptFileNames() { const projectFiles = languageServiceReducedMode ? [] @@ -645,6 +688,10 @@ async function createLanguageService( } } + function setUserPreferences(userPreferences: ts.UserPreferences) { + serviceWithCache.setPreferences?.(userPreferences); + } + function dispose() { languageService.dispose(); snapshotManager.dispose(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f1fb51a95..cd08d4437 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,6 +60,9 @@ importers: typescript: specifier: ^5.3.2 version: 5.3.2 + typescript-auto-import-cache: + specifier: ^0.3.0 + version: 0.3.0 vscode-css-languageservice: specifier: ~6.2.10 version: 6.2.10 @@ -1924,6 +1927,12 @@ packages: engines: {node: '>=4'} dev: true + /typescript-auto-import-cache@0.3.0: + resolution: {integrity: sha512-Rq6/q4O9iyqUdjvOoyas7x/Qf9nWUMeqpP3YeTaLA+uECgfy5wOhfOS+SW/+fZ/uI/ZcKaf+2/ZhFzXh8xfofQ==} + dependencies: + semver: 7.5.1 + dev: false + /typescript@5.3.2: resolution: {integrity: sha512-6l+RyNy7oAHDfxC4FzSJcz9vnjTKxrLpDG5M2Vu4SHRVNg6xzqZp6LYSR9zjqQTu8DU/f5xwxUdADOkbrIX2gQ==} engines: {node: '>=14.17'} From 9fceacbf6912edd3bbfbaa93a08ac85d29064f20 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Fri, 22 Dec 2023 13:34:26 +0800 Subject: [PATCH 02/15] ts 5.3 compatibility --- packages/language-server/package.json | 2 +- pnpm-lock.yaml | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 612883700..49945a91e 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -58,7 +58,7 @@ "svelte-preprocess": "~5.1.0", "svelte2tsx": "workspace:~", "typescript": "^5.3.2", - "typescript-auto-import-cache": "^0.3.0", + "typescript-auto-import-cache": "^0.3.1", "vscode-css-languageservice": "~6.2.10", "vscode-html-languageservice": "~5.1.1", "vscode-languageserver": "8.0.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cd08d4437..aa110ee35 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,8 +61,8 @@ importers: specifier: ^5.3.2 version: 5.3.2 typescript-auto-import-cache: - specifier: ^0.3.0 - version: 0.3.0 + specifier: ^0.3.1 + version: 0.3.1 vscode-css-languageservice: specifier: ~6.2.10 version: 6.2.10 @@ -1927,8 +1927,8 @@ packages: engines: {node: '>=4'} dev: true - /typescript-auto-import-cache@0.3.0: - resolution: {integrity: sha512-Rq6/q4O9iyqUdjvOoyas7x/Qf9nWUMeqpP3YeTaLA+uECgfy5wOhfOS+SW/+fZ/uI/ZcKaf+2/ZhFzXh8xfofQ==} + /typescript-auto-import-cache@0.3.1: + resolution: {integrity: sha512-ujC5E2gT3Sf3Dzfg5QYgb8NkZNxFQI12W6rk5U/TbkDFXyvIb9YENic+hsNoVDmKEmlRTUjRRD8RCjLMIx1rxg==} dependencies: semver: 7.5.1 dev: false From 2857bd57c78d3203a883367cd4040d704f39c2af Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Thu, 28 Dec 2023 14:22:14 +0800 Subject: [PATCH 03/15] patching the cache methods ourself for more control fixing virtual fs test and memory leak problems --- .../plugins/typescript/LSAndTSDocResolver.ts | 136 ++++++++++++++---- .../src/plugins/typescript/SnapshotManager.ts | 94 +----------- .../src/plugins/typescript/service.ts | 100 ++++++++----- .../src/plugins/typescript/serviceCache.ts | 97 +++++++++++++ .../test/plugins/typescript/service.test.ts | 3 +- 5 files changed, 275 insertions(+), 155 deletions(-) create mode 100644 packages/language-server/src/plugins/typescript/serviceCache.ts diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index 45cb586d7..85762a31d 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -1,4 +1,4 @@ -import { dirname } from 'path'; +import { dirname, join } from 'path'; import ts from 'typescript'; import { TextDocumentContentChangeEvent } from 'vscode-languageserver'; import { Document, DocumentManager } from '../../lib/documents'; @@ -19,8 +19,10 @@ import { LanguageServiceContainer, LanguageServiceDocumentContext } from './service'; +import { createProjectService } from './serviceCache'; import { GlobalSnapshotsManager, SnapshotManager } from './SnapshotManager'; import { isSubPath } from './utils'; +import { FileMap } from '../../lib/documents/fileCollection'; interface LSAndTSDocResolverOptions { notifyExceedSizeLimit?: () => void; @@ -72,9 +74,42 @@ export class LSAndTSDocResolver { (options?.tsSystem ?? ts.sys).useCaseSensitiveFileNames ); + this.tsSystem = this.options?.tsSystem ?? ts.sys; + this.globalSnapshotsManager = new GlobalSnapshotsManager(this.tsSystem); + this.userPreferencesAccessor = { preferences: this.getTsUserPreferences() }; + const projectService = createProjectService( + this.wrapWithPackageJsonMonitoring(this.tsSystem), + this.userPreferencesAccessor + ); + configManager.onChange(() => { - this.configChanged = true; + const newPreferences = this.getTsUserPreferences(); + const autoImportConfigChanged = + newPreferences.includePackageJsonAutoImports !== + this.userPreferencesAccessor.preferences.includePackageJsonAutoImports; + + this.userPreferencesAccessor.preferences = newPreferences; + + if (autoImportConfigChanged) { + forAllServices((service) => { + service.onAutoImportProviderSettingsChanged(); + }); + } }); + + this.watchers = new FileMap(this.tsSystem.useCaseSensitiveFileNames); + this.lsDocumentContext = { + ambientTypesSource: this.options?.isSvelteCheck ? 'svelte-check' : 'svelte2tsx', + createDocument: this.createDocument, + transformOnTemplateError: !this.options?.isSvelteCheck, + globalSnapshotsManager: this.globalSnapshotsManager, + notifyExceedSizeLimit: this.options?.notifyExceedSizeLimit, + extendedConfigCache: this.extendedConfigCache, + onProjectReloaded: this.options?.onProjectReloaded, + watchTsConfig: !!this.options?.watch, + tsSystem: this.tsSystem, + projectService: projectService + } } /** @@ -93,28 +128,15 @@ export class LSAndTSDocResolver { return document; }; - private globalSnapshotsManager = new GlobalSnapshotsManager( - this.lsDocumentContext.tsSystem, - /* watchPackageJson */ !!this.options?.watch - ); + private tsSystem: ts.System; + private globalSnapshotsManager: GlobalSnapshotsManager; private extendedConfigCache = new Map(); private getCanonicalFileName: GetCanonicalFileName; - private configChanged = true; + private userPreferencesAccessor: { preferences: ts.UserPreferences }; + private readonly watchers: FileMap; - private get lsDocumentContext(): LanguageServiceDocumentContext { - return { - ambientTypesSource: this.options?.isSvelteCheck ? 'svelte-check' : 'svelte2tsx', - createDocument: this.createDocument, - transformOnTemplateError: !this.options?.isSvelteCheck, - globalSnapshotsManager: this.globalSnapshotsManager, - notifyExceedSizeLimit: this.options?.notifyExceedSizeLimit, - extendedConfigCache: this.extendedConfigCache, - onProjectReloaded: this.options?.onProjectReloaded, - watchTsConfig: !!this.options?.watch, - tsSystem: this.options?.tsSystem ?? ts.sys - }; - } + private lsDocumentContext: LanguageServiceDocumentContext; async getLSForPath(path: string) { return (await this.getTSService(path)).getService(); @@ -127,11 +149,6 @@ export class LSAndTSDocResolver { }> { const { tsDoc, lsContainer, userPreferences } = await this.getLSAndTSDocWorker(document); - if (this.configChanged) { - this.configChanged = false; - lsContainer.setUserPreferences(userPreferences); - } - return { tsDoc, lang: lsContainer.getService(), userPreferences }; } @@ -262,4 +279,73 @@ export class LSAndTSDocResolver { nearestWorkspaceUri ? urlToPath(nearestWorkspaceUri) : null ); } + + private getTsUserPreferences() { + return this.configManager.getTsUserPreferences('typescript', null); + } + + private wrapWithPackageJsonMonitoring(sys: ts.System): ts.System { + if (!sys.watchFile || !this.options?.watch) { + return sys; + } + + const watchFile = sys.watchFile; + return { + ...sys, + readFile: (path, encoding) => { + if (path.endsWith('package.json') && !this.watchers.has(path)) { + this.watchers.set( + path, + watchFile(path, this.onPackageJsonWatchChange.bind(this), 3_000) + ); + } + + return sys.readFile(path, encoding); + } + }; + } + + private onPackageJsonWatchChange(path: string, onWatchChange: ts.FileWatcherEventKind) { + const dir = dirname(path); + const projectService = this.lsDocumentContext.projectService; + const packageJsonCache = projectService?.packageJsonCache; + const normalizedPath = projectService?.toPath(path); + + if (onWatchChange === ts.FileWatcherEventKind.Deleted) { + this.watchers.get(path)?.close(); + this.watchers.delete(path); + packageJsonCache?.delete(normalizedPath); + } else { + packageJsonCache?.addOrUpdate(normalizedPath); + } + + forAllServices((service) => { + service.onPackageJsonChange(path); + }); + if (!path.includes('node_modules')) { + return; + } + + setTimeout(() => { + this.updateSnapshotsInDirectory(dir); + const realPath = + this.tsSystem.realpath && + this.getCanonicalFileName(normalizePath(this.tsSystem.realpath?.(dir))); + + // pnpm + if (realPath && realPath !== dir) { + this.updateSnapshotsInDirectory(realPath); + const realPkgPath = join(realPath, 'package.json'); + forAllServices((service) => { + service.onPackageJsonChange(realPkgPath); + }); + } + }, 500); + } + + private updateSnapshotsInDirectory(dir: string) { + this.globalSnapshotsManager.getByPrefix(dir).forEach((snapshot) => { + this.globalSnapshotsManager.updateTsOrJsFile(snapshot.filePath); + }); + } } diff --git a/packages/language-server/src/plugins/typescript/SnapshotManager.ts b/packages/language-server/src/plugins/typescript/SnapshotManager.ts index 8b035e65a..d7b8cd011 100644 --- a/packages/language-server/src/plugins/typescript/SnapshotManager.ts +++ b/packages/language-server/src/plugins/typescript/SnapshotManager.ts @@ -5,7 +5,6 @@ import { TextDocumentContentChangeEvent } from 'vscode-languageserver'; import { createGetCanonicalFileName, GetCanonicalFileName, normalizePath } from '../../utils'; import { EventEmitter } from 'events'; import { FileMap } from '../../lib/documents/fileCollection'; -import { dirname } from 'path'; type SnapshotChangeHandler = (fileName: string, newDocument: DocumentSnapshot | undefined) => void; @@ -18,20 +17,12 @@ export class GlobalSnapshotsManager { private emitter = new EventEmitter(); private documents: FileMap; private getCanonicalFileName: GetCanonicalFileName; - private packageJsonCache: PackageJsonCache; constructor( - private readonly tsSystem: ts.System, - watchPackageJson = false + private readonly tsSystem: ts.System ) { this.documents = new FileMap(tsSystem.useCaseSensitiveFileNames); this.getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames); - this.packageJsonCache = new PackageJsonCache( - tsSystem, - watchPackageJson, - this.getCanonicalFileName, - this.updateSnapshotsInDirectory.bind(this) - ); } get(fileName: string) { @@ -94,16 +85,6 @@ export class GlobalSnapshotsManager { removeChangeListener(listener: SnapshotChangeHandler) { this.emitter.off('change', listener); } - - getPackageJson(path: string) { - return this.packageJsonCache.getPackageJson(path); - } - - private updateSnapshotsInDirectory(dir: string) { - this.getByPrefix(dir).forEach((snapshot) => { - this.updateTsOrJsFile(snapshot.filePath); - }); - } } export interface TsFilesSpec { @@ -267,76 +248,3 @@ export class SnapshotManager { } export const ignoredBuildDirectories = ['__sapper__', '.svelte-kit']; - -class PackageJsonCache { - constructor( - private readonly tsSystem: ts.System, - private readonly watchPackageJson: boolean, - private readonly getCanonicalFileName: GetCanonicalFileName, - private readonly updateSnapshotsInDirectory: (directory: string) => void - ) { - this.watchers = new FileMap(tsSystem.useCaseSensitiveFileNames); - } - - private readonly watchers: FileMap; - - private packageJsonCache = new FileMap< - { text: string; modifiedTime: number | undefined } | undefined - >(); - - getPackageJson(path: string) { - if (!this.packageJsonCache.has(path)) { - this.packageJsonCache.set(path, this.initWatcherAndRead(path)); - } - - return this.packageJsonCache.get(path); - } - - private initWatcherAndRead(path: string) { - if (this.watchPackageJson) { - this.tsSystem.watchFile?.(path, this.onPackageJsonWatchChange.bind(this), 3_000); - } - const exist = this.tsSystem.fileExists(path); - - if (!exist) { - return undefined; - } - - return this.readPackageJson(path); - } - - private readPackageJson(path: string) { - return { - text: this.tsSystem.readFile(path) ?? '', - modifiedTime: this.tsSystem.getModifiedTime?.(path)?.valueOf() - }; - } - - private onPackageJsonWatchChange(path: string, onWatchChange: ts.FileWatcherEventKind) { - const dir = dirname(path); - - if (onWatchChange === ts.FileWatcherEventKind.Deleted) { - this.packageJsonCache.delete(path); - this.watchers.get(path)?.close(); - this.watchers.delete(path); - } else { - this.packageJsonCache.set(path, this.readPackageJson(path)); - } - - if (!path.includes('node_modules')) { - return; - } - - setTimeout(() => { - this.updateSnapshotsInDirectory(dir); - const realPath = - this.tsSystem.realpath && - this.getCanonicalFileName(normalizePath(this.tsSystem.realpath?.(dir))); - - // pnpm - if (realPath && realPath !== dir) { - this.updateSnapshotsInDirectory(realPath); - } - }, 500); - } -} diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index e0f3da4ee..6b7c1c201 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -21,7 +21,7 @@ import { hasTsExtensions, isSvelteFilePath } from './utils'; -import { createLanguageService as createLanguageServiceWithCache } from 'typescript-auto-import-cache'; +import { createProject, ProjectService } from './serviceCache'; export interface LanguageServiceContainer { readonly tsconfigPath: string; @@ -47,8 +47,8 @@ export interface LanguageServiceContainer { * Only works for TS versions that have ScriptKind.Deferred */ fileBelongsToProject(filePath: string, isNew: boolean): boolean; - - setUserPreferences(preferences: ts.UserPreferences): void; + onAutoImportProviderSettingsChanged(): void; + onPackageJsonChange(packageJsonPath: string): void; dispose(): void; } @@ -111,6 +111,7 @@ export interface LanguageServiceDocumentContext { onProjectReloaded: (() => void) | undefined; watchTsConfig: boolean; tsSystem: ts.System; + projectService: ProjectService | undefined; } export async function getService( @@ -217,26 +218,11 @@ async function createLanguageService( // Load all configs within the tsconfig scope and the one above so that they are all loaded // by the time they need to be accessed synchronously by DocumentSnapshots. await configLoader.loadConfigs(workspacePath); - const tsSystemWithPackageJsonCache = { - ...tsSystem, - /** - * While TypeScript doesn't cache package.json in the tsserver, they do cache the - * information they get from it within other internal APIs. We'll somewhat do the same - * by caching the text of the package.json file here. - */ - readFile: (path: string, encoding?: string | undefined) => { - if (basename(path) === 'package.json') { - return docContext.globalSnapshotsManager.getPackageJson(path)?.text; - } - - return tsSystem.readFile(path, encoding); - } - }; const svelteModuleLoader = createSvelteModuleLoader( getSnapshot, compilerOptions, - tsSystemWithPackageJsonCache, + tsSystem, ts ); @@ -312,13 +298,8 @@ async function createLanguageService( typingsNamespace: raw?.svelteOptions?.namespace || 'svelteHTML' }; - const serviceWithCache = createLanguageServiceWithCache( - ts as any, - tsSystemWithPackageJsonCache, - host, - () => ts.createLanguageService(host, documentRegistry) - ); - const { languageService } = serviceWithCache; + const project = initLsCacheProject(); + const languageService = ts.createLanguageService(host, documentRegistry); docContext.globalSnapshotsManager.onChange(scheduleUpdate); @@ -339,7 +320,8 @@ async function createLanguageService( fileBelongsToProject, snapshotManager, invalidateModuleCache, - setUserPreferences, + onAutoImportProviderSettingsChanged, + onPackageJsonChange, dispose }; @@ -379,8 +361,7 @@ async function createLanguageService( if (!prevSnapshot) { svelteModuleLoader.deleteUnresolvedResolutionsFromCache(filePath); - // @ts-expect-error - host?.getCachedExportInfoMap()?.clear(); + host.getCachedExportInfoMap?.()?.clear(); } const newSnapshot = DocumentSnapshot.fromDocument(document, transformationConfig); @@ -463,12 +444,12 @@ async function createLanguageService( const projectFileCountAfter = projectFileAfter.length; const hasAddedOrRemoved = + !!project && projectFileCountAfter !== projectFileCountBefore || checkProjectFileUpdate(projectFileBefore, projectFileAfter); if (hasAddedOrRemoved) { - // @ts-expect-error - host?.getCachedExportInfoMap()?.clear(); + host.getCachedExportInfoMap?.()?.clear(); } if (projectFileCountAfter <= projectFileCountBefore) { @@ -684,14 +665,13 @@ async function createLanguageService( ) { languageService.cleanupSemanticCache(); languageServiceReducedMode = true; + if (project) { + project.languageServiceEnabled = false; + } docContext.notifyExceedSizeLimit?.(); } } - function setUserPreferences(userPreferences: ts.UserPreferences) { - serviceWithCache.setPreferences?.(userPreferences); - } - function dispose() { languageService.dispose(); snapshotManager.dispose(); @@ -770,9 +750,13 @@ async function createLanguageService( return; } - languageService.getProgram(); + const program = languageService.getProgram(); svelteModuleLoader.clearPendingInvalidations(); + if (project) { + project.program = program; + } + dirty = false; } @@ -784,6 +768,50 @@ async function createLanguageService( projectVersion++; dirty = true; } + + function initLsCacheProject() { + const projectService = docContext.projectService; + if (!projectService) { + return; + } + + const createLanguageService = (host: ts.LanguageServiceHost) => + ts.createLanguageService(host, documentRegistry); + + return createProject(host, createLanguageService, { + compilerOptions: compilerOptions, + projectService: projectService, + currentDirectory: workspacePath + }); + } + + function onAutoImportProviderSettingsChanged() { + project?.onAutoImportProviderSettingsChanged(); + } + + function onPackageJsonChange(packageJsonPath: string) { + if (!project) { + return; + } + + if (project.packageJsonsForAutoImport?.has(packageJsonPath)) { + project.moduleSpecifierCache.clear(); + + if (project.autoImportProviderHost) { + project.autoImportProviderHost.markAsDirty(); + } + } + + if (packageJsonPath.includes('node_modules')) { + const dir = dirname(packageJsonPath); + const inProgram = project.getCurrentProgram()?.getSourceFiles() + .some((sf: ts.SourceFile) => sf.fileName.includes(dir)); + + if (inProgram) { + host.getModuleSpecifierCache?.().clear(); + } + } + } } /** diff --git a/packages/language-server/src/plugins/typescript/serviceCache.ts b/packages/language-server/src/plugins/typescript/serviceCache.ts new file mode 100644 index 000000000..d1f1c5753 --- /dev/null +++ b/packages/language-server/src/plugins/typescript/serviceCache.ts @@ -0,0 +1,97 @@ +// abstracting the typescript-auto-import-cache package to support our use case + +import { + ProjectService, + createProjectService as createProjectService50 +} from 'typescript-auto-import-cache/out/5_0/projectService'; +import { createProject as createProject50 } from 'typescript-auto-import-cache/out/5_0/project'; +import { createProject as createProject53 } from 'typescript-auto-import-cache/out/5_3/project'; +import ts from 'typescript'; +import { ExportInfoMap } from 'typescript-auto-import-cache/out/5_0/exportInfoMap'; +import { ModuleSpecifierCache } from 'typescript-auto-import-cache/out/5_0/moduleSpecifierCache'; +import { SymlinkCache } from 'typescript-auto-import-cache/out/5_0/symlinkCache'; +import { ProjectPackageJsonInfo } from 'typescript-auto-import-cache/out/5_0/packageJsonCache'; + +export { ProjectService }; + +declare module 'typescript' { + interface LanguageServiceHost { + /** @internal */ getCachedExportInfoMap?(): ExportInfoMap; + /** @internal */ getModuleSpecifierCache?(): ModuleSpecifierCache; + /** @internal */ getGlobalTypingsCacheLocation?(): string | undefined; + /** @internal */ getSymlinkCache?(files: readonly ts.SourceFile[]): SymlinkCache; + /** @internal */ getPackageJsonsVisibleToFile?( + fileName: string, + rootDir?: string + ): readonly ProjectPackageJsonInfo[]; + /** @internal */ getPackageJsonAutoImportProvider?(): ts.Program | undefined; + /** @internal */ useSourceOfProjectReferenceRedirect?(): boolean; + } +} + +export function createProjectService( + system: ts.System, + hostConfiguration: { + preferences: ts.UserPreferences; + } +) { + const version = ts.version.split('.'); + const major = parseInt(version[0]); + + if (major < 5) { + return undefined; + } + + const projectService = createProjectService50( + ts, + system, + system.getCurrentDirectory(), + hostConfiguration, + ts.LanguageServiceMode.Semantic + ); + + return projectService; +} + +export function createProject( + host: ts.LanguageServiceHost, + createLanguageService: (host: ts.LanguageServiceHost) => ts.LanguageService, + options: { + projectService: ProjectService; + compilerOptions: ts.CompilerOptions; + currentDirectory: string; + } +) { + const version = ts.version.split('.'); + const major = parseInt(version[0]); + const minor = parseInt(version[1]); + + if (major < 5) { + return undefined; + } + + const factory = minor < 3 ? createProject50 : createProject53; + const project = factory(ts, host, createLanguageService, options); + + const proxyMethods: (keyof typeof project)[] = [ + 'getCachedExportInfoMap', + 'getModuleSpecifierCache', + 'getGlobalTypingsCacheLocation', + 'getSymlinkCache', + 'getPackageJsonsVisibleToFile', + 'getPackageJsonAutoImportProvider', + 'includePackageJsonAutoImports', + 'useSourceOfProjectReferenceRedirect' + ]; + proxyMethods.forEach((key) => ((host as any)[key] = project[key].bind(project))); + + if (host.log) { + project.log = host.log.bind(host); + } + + return project; + + function ensureTrailDirSeparator(path: string) { + return path.endsWith('/') ? path : path + '/'; + } +} diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index 1b2801bc1..f8b36914a 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -27,7 +27,8 @@ describe('service', () => { tsSystem: virtualSystem, watchTsConfig: false, notifyExceedSizeLimit: undefined, - onProjectReloaded: undefined + onProjectReloaded: undefined, + projectService: undefined }; const rootUris = [pathToUrl(testDir)]; From 86e2d800460cc3e671bc01d995239ad5fff62792 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Thu, 28 Dec 2023 18:04:56 +0800 Subject: [PATCH 04/15] format --- .../src/plugins/typescript/LSAndTSDocResolver.ts | 2 +- .../src/plugins/typescript/SnapshotManager.ts | 4 +--- .../src/plugins/typescript/service.ts | 16 ++++++---------- .../src/plugins/typescript/serviceCache.ts | 2 +- 4 files changed, 9 insertions(+), 15 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index 85762a31d..bf6405f50 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -109,7 +109,7 @@ export class LSAndTSDocResolver { watchTsConfig: !!this.options?.watch, tsSystem: this.tsSystem, projectService: projectService - } + }; } /** diff --git a/packages/language-server/src/plugins/typescript/SnapshotManager.ts b/packages/language-server/src/plugins/typescript/SnapshotManager.ts index d7b8cd011..7ee160ffc 100644 --- a/packages/language-server/src/plugins/typescript/SnapshotManager.ts +++ b/packages/language-server/src/plugins/typescript/SnapshotManager.ts @@ -18,9 +18,7 @@ export class GlobalSnapshotsManager { private documents: FileMap; private getCanonicalFileName: GetCanonicalFileName; - constructor( - private readonly tsSystem: ts.System - ) { + constructor(private readonly tsSystem: ts.System) { this.documents = new FileMap(tsSystem.useCaseSensitiveFileNames); this.getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames); } diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 6b7c1c201..fdee6b8bf 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -219,12 +219,7 @@ async function createLanguageService( // by the time they need to be accessed synchronously by DocumentSnapshots. await configLoader.loadConfigs(workspacePath); - const svelteModuleLoader = createSvelteModuleLoader( - getSnapshot, - compilerOptions, - tsSystem, - ts - ); + const svelteModuleLoader = createSvelteModuleLoader(getSnapshot, compilerOptions, tsSystem, ts); let svelteTsPath: string; try { @@ -444,8 +439,7 @@ async function createLanguageService( const projectFileCountAfter = projectFileAfter.length; const hasAddedOrRemoved = - !!project && - projectFileCountAfter !== projectFileCountBefore || + (!!project && projectFileCountAfter !== projectFileCountBefore) || checkProjectFileUpdate(projectFileBefore, projectFileAfter); if (hasAddedOrRemoved) { @@ -796,7 +790,7 @@ async function createLanguageService( if (project.packageJsonsForAutoImport?.has(packageJsonPath)) { project.moduleSpecifierCache.clear(); - + if (project.autoImportProviderHost) { project.autoImportProviderHost.markAsDirty(); } @@ -804,7 +798,9 @@ async function createLanguageService( if (packageJsonPath.includes('node_modules')) { const dir = dirname(packageJsonPath); - const inProgram = project.getCurrentProgram()?.getSourceFiles() + const inProgram = project + .getCurrentProgram() + ?.getSourceFiles() .some((sf: ts.SourceFile) => sf.fileName.includes(dir)); if (inProgram) { diff --git a/packages/language-server/src/plugins/typescript/serviceCache.ts b/packages/language-server/src/plugins/typescript/serviceCache.ts index d1f1c5753..dd6d03654 100644 --- a/packages/language-server/src/plugins/typescript/serviceCache.ts +++ b/packages/language-server/src/plugins/typescript/serviceCache.ts @@ -84,7 +84,7 @@ export function createProject( 'useSourceOfProjectReferenceRedirect' ]; proxyMethods.forEach((key) => ((host as any)[key] = project[key].bind(project))); - + if (host.log) { project.log = host.log.bind(host); } From b6e5e45061255284826b8cda09d4b5ad5d3513c8 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Fri, 29 Dec 2023 11:48:19 +0800 Subject: [PATCH 05/15] fix package.json import --- packages/language-server/package.json | 2 +- .../features/CompletionProvider.test.ts | 82 +++++++++++++++++++ pnpm-lock.yaml | 8 +- 3 files changed, 87 insertions(+), 5 deletions(-) diff --git a/packages/language-server/package.json b/packages/language-server/package.json index 49945a91e..e15bc971d 100644 --- a/packages/language-server/package.json +++ b/packages/language-server/package.json @@ -58,7 +58,7 @@ "svelte-preprocess": "~5.1.0", "svelte2tsx": "workspace:~", "typescript": "^5.3.2", - "typescript-auto-import-cache": "^0.3.1", + "typescript-auto-import-cache": "^0.3.2", "vscode-css-languageservice": "~6.2.10", "vscode-html-languageservice": "~5.1.1", "vscode-languageserver": "8.0.2", diff --git a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts index 132539a1d..f6a4ae712 100644 --- a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts @@ -1378,6 +1378,88 @@ describe('CompletionProviderImpl', function () { assert.strictEqual(detail, 'Add import from "random-package2"\n\nfunction foo(): string'); }); + it('can auto import package not in the program', async () => { + const virtualTestDir = getRandomVirtualDirPath(testFilesDir); + const { document, lsAndTsDocResolver, lsConfigManager, virtualSystem } = + setupVirtualEnvironment({ + filename: 'index.svelte', + fileContent: '', + testDir: virtualTestDir + }); + + const mockPackageDir = join(virtualTestDir, 'node_modules', 'random-package'); + + virtualSystem.writeFile( + join(virtualTestDir, 'package.json'), + JSON.stringify({ + dependencies: { + 'random-package': '*' + } + }) + ); + + virtualSystem.writeFile( + join(mockPackageDir, 'package.json'), + JSON.stringify({ + name: 'random-package', + version: '1.0.0' + }) + ); + + virtualSystem.writeFile( + join(mockPackageDir, 'index.d.ts'), + 'export function bar(): string' + ); + + const completionProvider = new CompletionsProviderImpl(lsAndTsDocResolver, lsConfigManager); + + const completions = await completionProvider.getCompletions(document, { + line: 0, + character: 9 + }); + const item = completions?.items.find((item) => item.label === 'bar'); + + const { detail } = await completionProvider.resolveCompletion(document, item!); + + assert.strictEqual(detail, 'Add import from "random-package"\n\nfunction bar(): string'); + }); + + it('can auto import new file', async () => { + const virtualTestDir = getRandomVirtualDirPath(testFilesDir); + const { document, lsAndTsDocResolver, lsConfigManager, docManager } = + setupVirtualEnvironment({ + filename: 'index.svelte', + fileContent: '', + testDir: virtualTestDir + }); + + const completionProvider = new CompletionsProviderImpl(lsAndTsDocResolver, lsConfigManager); + + const completions = await completionProvider.getCompletions(document, { + line: 0, + character: 9 + }); + + const item = completions?.items.find((item) => item.label === 'Bar'); + + assert.equal(item, undefined); + + docManager.openClientDocument({ + text: '', + uri: pathToUrl(join(virtualTestDir, 'Bar.svelte')) + }); + + const completions2 = await completionProvider.getCompletions(document, { + line: 0, + character: 9 + }); + + const item2 = completions2?.items.find((item) => item.label === 'Bar'); + const { detail } = await completionProvider.resolveCompletion(document, item2!); + + assert.strictEqual(detail, 'Add import from "./Bar.svelte"\n\nclass Bar'); + }); + it('provides completions for object literal member', async () => { const { completionProvider, document } = setup('object-member.svelte'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa110ee35..828565e96 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,8 +61,8 @@ importers: specifier: ^5.3.2 version: 5.3.2 typescript-auto-import-cache: - specifier: ^0.3.1 - version: 0.3.1 + specifier: ^0.3.2 + version: 0.3.2 vscode-css-languageservice: specifier: ~6.2.10 version: 6.2.10 @@ -1927,8 +1927,8 @@ packages: engines: {node: '>=4'} dev: true - /typescript-auto-import-cache@0.3.1: - resolution: {integrity: sha512-ujC5E2gT3Sf3Dzfg5QYgb8NkZNxFQI12W6rk5U/TbkDFXyvIb9YENic+hsNoVDmKEmlRTUjRRD8RCjLMIx1rxg==} + /typescript-auto-import-cache@0.3.2: + resolution: {integrity: sha512-+laqe5SFL1vN62FPOOJSUDTZxtgsoOXjneYOXIpx5rQ4UMiN89NAtJLpqLqyebv9fgQ/IMeeTX+mQyRnwvJzvg==} dependencies: semver: 7.5.1 dev: false From e5791f1b6eb30e22febdfce3867190ee4c8677dc Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Fri, 29 Dec 2023 15:53:49 +0800 Subject: [PATCH 06/15] monitor package.json from module resolution as well, like before --- .../src/plugins/typescript/LSAndTSDocResolver.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index bf6405f50..c7c3078aa 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -74,13 +74,10 @@ export class LSAndTSDocResolver { (options?.tsSystem ?? ts.sys).useCaseSensitiveFileNames ); - this.tsSystem = this.options?.tsSystem ?? ts.sys; + this.tsSystem = this.wrapWithPackageJsonMonitoring(this.options?.tsSystem ?? ts.sys); this.globalSnapshotsManager = new GlobalSnapshotsManager(this.tsSystem); this.userPreferencesAccessor = { preferences: this.getTsUserPreferences() }; - const projectService = createProjectService( - this.wrapWithPackageJsonMonitoring(this.tsSystem), - this.userPreferencesAccessor - ); + const projectService = createProjectService(this.tsSystem, this.userPreferencesAccessor); configManager.onChange(() => { const newPreferences = this.getTsUserPreferences(); From bfa7eb44097158815d2d7882f2b3ec7903ee157d Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Fri, 29 Dec 2023 16:01:15 +0800 Subject: [PATCH 07/15] let ts cache IncompleteCompletions --- packages/language-server/src/ls-config.ts | 1 + .../src/plugins/typescript/features/CompletionProvider.ts | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/language-server/src/ls-config.ts b/packages/language-server/src/ls-config.ts index 1511488b6..75defe9d7 100644 --- a/packages/language-server/src/ls-config.ts +++ b/packages/language-server/src/ls-config.ts @@ -439,6 +439,7 @@ export class LSConfigManager { includeCompletionsWithObjectLiteralMethodSnippets: config.suggest?.objectLiteralMethodSnippets?.enabled ?? true, preferTypeOnlyAutoImports: config.preferences?.preferTypeOnlyAutoImports, + allowIncompleteCompletions: true, includeInlayEnumMemberValueHints: inlayHints?.enumMemberValues?.enabled, includeInlayFunctionLikeReturnTypeHints: inlayHints?.functionLikeReturnTypes?.enabled, diff --git a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts index 94284de22..4bb1d9f71 100644 --- a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts @@ -236,7 +236,9 @@ export class CompletionsProviderImpl implements CompletionsProvider Date: Fri, 29 Dec 2023 16:13:38 +0800 Subject: [PATCH 08/15] most module specifier is resolved in getCompletionsAtPosition now --- .../plugins/typescript/features/CompletionProvider.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts index f6a4ae712..d2e026e63 100644 --- a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts @@ -1545,7 +1545,7 @@ describe('CompletionProviderImpl', function () { const item = completions?.items.find((item) => item.label === '$store'); assert.ok(item); - assert.equal(item?.data?.source?.endsWith('completions/to-import'), true); + assert.equal(item?.data?.source?.endsWith('/to-import'), true); const { data, ...itemWithoutData } = item; @@ -1558,7 +1558,9 @@ describe('CompletionProviderImpl', function () { insertTextFormat: undefined, commitCharacters: ['.', ',', ';', '('], textEdit: undefined, - labelDetails: undefined + labelDetails: { + description: './to-import' + } }); const { detail } = await completionProvider.resolveCompletion(document, item); From 4539429c45caf17773a6da474b2ea31ac07cc3c8 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Thu, 4 Jan 2024 10:55:26 +0800 Subject: [PATCH 09/15] another cache --- .../language-server/src/plugins/typescript/module-loader.ts | 3 ++- packages/language-server/src/plugins/typescript/service.ts | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/module-loader.ts b/packages/language-server/src/plugins/typescript/module-loader.ts index 007415a1b..84da155d8 100644 --- a/packages/language-server/src/plugins/typescript/module-loader.ts +++ b/packages/language-server/src/plugins/typescript/module-loader.ts @@ -208,7 +208,8 @@ export function createSvelteModuleLoader( resolveModuleNames, resolveTypeReferenceDirectiveReferences, mightHaveInvalidatedResolutions, - clearPendingInvalidations + clearPendingInvalidations, + getModuleResolutionCache: () => tsModuleCache, }; function resolveModuleNames( diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index fdee6b8bf..0c63866f4 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -61,6 +61,8 @@ declare module 'typescript' { * that might change the module resolution results */ hasInvalidatedResolutions?: (sourceFile: string) => boolean; + + getModuleResolutionCache?(): ts.ModuleResolutionCache; } interface ResolvedModuleWithFailedLookupLocations { @@ -278,7 +280,8 @@ async function createLanguageService( getNewLine: () => tsSystem.newLine, resolveTypeReferenceDirectiveReferences: svelteModuleLoader.resolveTypeReferenceDirectiveReferences, - hasInvalidatedResolutions: svelteModuleLoader.mightHaveInvalidatedResolutions + hasInvalidatedResolutions: svelteModuleLoader.mightHaveInvalidatedResolutions, + getModuleResolutionCache: svelteModuleLoader.getModuleResolutionCache, }; const documentRegistry = getOrCreateDocumentRegistry( From 29d34db6b386e0d5670597295e5a92f0d41dbd4e Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Thu, 4 Jan 2024 10:57:34 +0800 Subject: [PATCH 10/15] format --- .../language-server/src/plugins/typescript/module-loader.ts | 2 +- packages/language-server/src/plugins/typescript/service.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/module-loader.ts b/packages/language-server/src/plugins/typescript/module-loader.ts index 84da155d8..4eefdc700 100644 --- a/packages/language-server/src/plugins/typescript/module-loader.ts +++ b/packages/language-server/src/plugins/typescript/module-loader.ts @@ -209,7 +209,7 @@ export function createSvelteModuleLoader( resolveTypeReferenceDirectiveReferences, mightHaveInvalidatedResolutions, clearPendingInvalidations, - getModuleResolutionCache: () => tsModuleCache, + getModuleResolutionCache: () => tsModuleCache }; function resolveModuleNames( diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 0c63866f4..434001bc9 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -281,7 +281,7 @@ async function createLanguageService( resolveTypeReferenceDirectiveReferences: svelteModuleLoader.resolveTypeReferenceDirectiveReferences, hasInvalidatedResolutions: svelteModuleLoader.mightHaveInvalidatedResolutions, - getModuleResolutionCache: svelteModuleLoader.getModuleResolutionCache, + getModuleResolutionCache: svelteModuleLoader.getModuleResolutionCache }; const documentRegistry = getOrCreateDocumentRegistry( From 032929733a2fcdd24fdc009618ef8f5f08f193a9 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Thu, 4 Jan 2024 11:20:44 +0800 Subject: [PATCH 11/15] cleanup --- packages/language-server/src/plugins/typescript/service.ts | 2 +- .../language-server/src/plugins/typescript/serviceCache.ts | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 434001bc9..f23957997 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -804,7 +804,7 @@ async function createLanguageService( const inProgram = project .getCurrentProgram() ?.getSourceFiles() - .some((sf: ts.SourceFile) => sf.fileName.includes(dir)); + .some((file) => file.fileName.includes(dir)); if (inProgram) { host.getModuleSpecifierCache?.().clear(); diff --git a/packages/language-server/src/plugins/typescript/serviceCache.ts b/packages/language-server/src/plugins/typescript/serviceCache.ts index dd6d03654..0dbb3bfe2 100644 --- a/packages/language-server/src/plugins/typescript/serviceCache.ts +++ b/packages/language-server/src/plugins/typescript/serviceCache.ts @@ -90,8 +90,4 @@ export function createProject( } return project; - - function ensureTrailDirSeparator(path: string) { - return path.endsWith('/') ? path : path + '/'; - } } From 9244da7ef1cda5f583f82da9c58908ba4ef74943 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Thu, 11 Jan 2024 10:50:04 +0800 Subject: [PATCH 12/15] revert incomplete completion cache seems like IncompleteCompletions only trigger very rarely and it require another internal API to work keeping allowIncompleteCompletions since it will also make completion label detail shows up in more cases --- packages/language-server/src/ls-config.ts | 4 ++++ .../src/plugins/typescript/features/CompletionProvider.ts | 2 -- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/language-server/src/ls-config.ts b/packages/language-server/src/ls-config.ts index 75defe9d7..83ecbfded 100644 --- a/packages/language-server/src/ls-config.ts +++ b/packages/language-server/src/ls-config.ts @@ -439,6 +439,10 @@ export class LSConfigManager { includeCompletionsWithObjectLiteralMethodSnippets: config.suggest?.objectLiteralMethodSnippets?.enabled ?? true, preferTypeOnlyAutoImports: config.preferences?.preferTypeOnlyAutoImports, + + // Although we don't support incompletion cache. + // But this will make ts resolve the module specifier more aggressively + // Which also make the completion label detail show up in more cases allowIncompleteCompletions: true, includeInlayEnumMemberValueHints: inlayHints?.enumMemberValues?.enabled, diff --git a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts index 4bb1d9f71..c342bf0cc 100644 --- a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts @@ -237,8 +237,6 @@ export class CompletionsProviderImpl implements CompletionsProvider Date: Thu, 11 Jan 2024 10:58:59 +0800 Subject: [PATCH 13/15] clarify createLanguageService passed into typescript-auto-import-cache --- packages/language-server/src/ls-config.ts | 4 ++-- .../src/plugins/typescript/features/CompletionProvider.ts | 2 +- packages/language-server/src/plugins/typescript/service.ts | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/language-server/src/ls-config.ts b/packages/language-server/src/ls-config.ts index 83ecbfded..42de751ea 100644 --- a/packages/language-server/src/ls-config.ts +++ b/packages/language-server/src/ls-config.ts @@ -440,9 +440,9 @@ export class LSConfigManager { config.suggest?.objectLiteralMethodSnippets?.enabled ?? true, preferTypeOnlyAutoImports: config.preferences?.preferTypeOnlyAutoImports, - // Although we don't support incompletion cache. + // Although we don't support incompletion cache. // But this will make ts resolve the module specifier more aggressively - // Which also make the completion label detail show up in more cases + // Which also makes the completion label detail show up in more cases allowIncompleteCompletions: true, includeInlayEnumMemberValueHints: inlayHints?.enumMemberValues?.enabled, diff --git a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts index c342bf0cc..94284de22 100644 --- a/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts +++ b/packages/language-server/src/plugins/typescript/features/CompletionProvider.ts @@ -236,7 +236,7 @@ export class CompletionsProviderImpl implements CompletionsProvider + // Used by typescript-auto-import-cache to create a lean language service for package.json auto-import. + const createLanguageServiceForAutoImportProvider = (host: ts.LanguageServiceHost) => ts.createLanguageService(host, documentRegistry); - return createProject(host, createLanguageService, { + return createProject(host, createLanguageServiceForAutoImportProvider, { compilerOptions: compilerOptions, projectService: projectService, currentDirectory: workspacePath From 9b012d3eec21fc0bc9b552a0c4899476bbbfcd41 Mon Sep 17 00:00:00 2001 From: "Lyu, Wei Da" Date: Thu, 11 Jan 2024 15:22:32 +0800 Subject: [PATCH 14/15] handle delete file and new export --- .../src/plugins/typescript/service.ts | 84 ++++++++----------- .../features/CompletionProvider.test.ts | 37 ++++++++ 2 files changed, 71 insertions(+), 50 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index f755d97ca..c581eadf0 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -248,6 +248,8 @@ async function createLanguageService( ? svelteHtmlDeclaration : './svelte-jsx-v4.d.ts'; + const changedFilesForExportCache = new Set(); + const svelteTsxFiles = ( isSvelte3 ? ['./svelte-shims.d.ts', './svelte-jsx.d.ts', './svelte-native-jsx.d.ts'] @@ -341,7 +343,7 @@ async function createLanguageService( svelteModuleLoader.deleteFromModuleCache(filePath); svelteModuleLoader.deleteUnresolvedResolutionsFromCache(filePath); - scheduleUpdate(); + scheduleUpdate(filePath); } function updateSnapshot(documentOrFilePath: Document | string): DocumentSnapshot { @@ -359,7 +361,6 @@ async function createLanguageService( if (!prevSnapshot) { svelteModuleLoader.deleteUnresolvedResolutionsFromCache(filePath); - host.getCachedExportInfoMap?.()?.clear(); } const newSnapshot = DocumentSnapshot.fromDocument(document, transformationConfig); @@ -375,15 +376,7 @@ async function createLanguageService( return prevSnapshot; } - svelteModuleLoader.deleteUnresolvedResolutionsFromCache(filePath); - const newSnapshot = DocumentSnapshot.fromFilePath( - filePath, - docContext.createDocument, - transformationConfig, - tsSystem - ); - snapshotManager.set(filePath, newSnapshot); - return newSnapshot; + return createSnapshot(filePath); } /** @@ -404,8 +397,7 @@ async function createLanguageService( } return createSnapshot( - svelteModuleLoader.svelteFileExists(fileName) ? svelteFileName : fileName, - doc + svelteModuleLoader.svelteFileExists(fileName) ? svelteFileName : fileName ); } @@ -417,12 +409,12 @@ async function createLanguageService( return doc; } - return createSnapshot(fileName, doc); + return createSnapshot(fileName); } - function createSnapshot(fileName: string, doc: DocumentSnapshot | undefined) { + function createSnapshot(fileName: string) { svelteModuleLoader.deleteUnresolvedResolutionsFromCache(fileName); - doc = DocumentSnapshot.fromFilePath( + const doc = DocumentSnapshot.fromFilePath( fileName, docContext.createDocument, transformationConfig, @@ -433,21 +425,10 @@ async function createLanguageService( } function updateProjectFiles(): void { - projectVersion++; - dirty = true; - const projectFileBefore = snapshotManager.getProjectFileNames(); - const projectFileCountBefore = projectFileBefore.length; + scheduleUpdate(); + const projectFileCountBefore = snapshotManager.getProjectFileNames().length; snapshotManager.updateProjectFiles(); - const projectFileAfter = snapshotManager.getProjectFileNames(); - const projectFileCountAfter = projectFileAfter.length; - - const hasAddedOrRemoved = - (!!project && projectFileCountAfter !== projectFileCountBefore) || - checkProjectFileUpdate(projectFileBefore, projectFileAfter); - - if (hasAddedOrRemoved) { - host.getCachedExportInfoMap?.()?.clear(); - } + const projectFileCountAfter = snapshotManager.getProjectFileNames().length; if (projectFileCountAfter <= projectFileCountBefore) { return; @@ -456,25 +437,6 @@ async function createLanguageService( reduceLanguageServiceCapabilityIfFileSizeTooBig(); } - function checkProjectFileUpdate(oldFiles: string[], newFiles: string[]) { - const oldSet = new Set(oldFiles); - const newSet = new Set(newFiles); - - for (const file of oldSet) { - if (!newSet.has(file)) { - return true; - } - } - - for (const file of newSet) { - if (!oldSet.has(file)) { - return true; - } - } - - return false; - } - function getScriptFileNames() { const projectFiles = languageServiceReducedMode ? [] @@ -747,6 +709,7 @@ async function createLanguageService( return; } + const oldProgram = project?.program; const program = languageService.getProgram(); svelteModuleLoader.clearPendingInvalidations(); @@ -755,9 +718,30 @@ async function createLanguageService( } dirty = false; + + for (const fileName of changedFilesForExportCache) { + const oldFile = oldProgram?.getSourceFile(fileName); + const newFile = program?.getSourceFile(fileName); + + // file for another tsconfig + if (!oldFile && !newFile) { + continue; + } + + if (oldFile && newFile) { + host.getCachedExportInfoMap?.().onFileChanged?.(oldFile, newFile, false); + } else { + // new file or deleted file + host.getCachedExportInfoMap?.().clear(); + } + } + changedFilesForExportCache.clear(); } - function scheduleUpdate() { + function scheduleUpdate(triggeredFile?: string) { + if (triggeredFile) { + changedFilesForExportCache.add(triggeredFile); + } if (dirty) { return; } diff --git a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts index d2e026e63..c2dc0fcd8 100644 --- a/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/CompletionProvider.test.ts @@ -1460,6 +1460,43 @@ describe('CompletionProviderImpl', function () { assert.strictEqual(detail, 'Add import from "./Bar.svelte"\n\nclass Bar'); }); + it('can auto import new export', async () => { + const virtualTestDir = getRandomVirtualDirPath(testFilesDir); + const { document, lsAndTsDocResolver, lsConfigManager, virtualSystem } = + setupVirtualEnvironment({ + filename: 'index.svelte', + fileContent: '', + testDir: virtualTestDir + }); + + const completionProvider = new CompletionsProviderImpl(lsAndTsDocResolver, lsConfigManager); + const tsFile = join(virtualTestDir, 'foo.ts'); + + virtualSystem.writeFile(tsFile, 'export {}'); + + const completions = await completionProvider.getCompletions(document, { + line: 0, + character: 31 + }); + + const item = completions?.items.find((item) => item.label === 'foo'); + + assert.equal(item, undefined); + + virtualSystem.writeFile(tsFile, 'export function foo() {}'); + lsAndTsDocResolver.updateExistingTsOrJsFile(tsFile); + + const completions2 = await completionProvider.getCompletions(document, { + line: 0, + character: 31 + }); + + const item2 = completions2?.items.find((item) => item.label === 'foo'); + const { detail } = await completionProvider.resolveCompletion(document, item2!); + + assert.strictEqual(detail, 'Update import from "./foo"\n\nfunction foo(): void'); + }); + it('provides completions for object literal member', async () => { const { completionProvider, document } = setup('object-member.svelte'); From 4620a2843f6800f88cebb9a136cc3694fd5b6228 Mon Sep 17 00:00:00 2001 From: Lyu Jason Date: Thu, 11 Jan 2024 21:08:58 +0800 Subject: [PATCH 15/15] slight optimization. no cache to clear for the first run --- packages/language-server/src/plugins/typescript/service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index c581eadf0..5f2571e75 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -719,8 +719,13 @@ async function createLanguageService( dirty = false; + if (!oldProgram) { + changedFilesForExportCache.clear(); + return; + } + for (const fileName of changedFilesForExportCache) { - const oldFile = oldProgram?.getSourceFile(fileName); + const oldFile = oldProgram.getSourceFile(fileName); const newFile = program?.getSourceFile(fileName); // file for another tsconfig