From dbfb47acde50e2b2c9bbcd864c3ffeadb06ceb5d Mon Sep 17 00:00:00 2001 From: "Lyu, Wei-Da" <36730922+jasonlyu123@users.noreply.github.com> Date: Mon, 24 Jun 2024 17:52:54 +0800 Subject: [PATCH] fix: watch svelte files and project files outside workspace (#2299) #2233 #2393 update project files(tsconfig.include) when a new client file is opened. So files included in both tsocnfig.json will be loaded into the respecting language service. --- .../src/lib/FallbackWatcher.ts | 24 ++++- .../src/lib/documents/DocumentManager.ts | 3 +- .../plugins/typescript/LSAndTSDocResolver.ts | 76 +++++++++++++--- .../src/plugins/typescript/SnapshotManager.ts | 59 ++++++++++-- .../plugins/typescript/TypeScriptPlugin.ts | 51 ++++++----- .../src/plugins/typescript/service.ts | 91 +++++++++++++++---- packages/language-server/src/server.ts | 67 ++++++++++---- packages/language-server/src/svelte-check.ts | 7 +- .../typescript/TypescriptPlugin.test.ts | 28 ++++-- .../features/DiagnosticsProvider.test.ts | 6 +- .../features/RenameProvider.test.ts | 2 - .../test/plugins/typescript/service.test.ts | 13 +-- .../test/plugins/typescript/test-utils.ts | 13 +++ .../typescript/typescript-performance.test.ts | 3 +- 14 files changed, 342 insertions(+), 101 deletions(-) diff --git a/packages/language-server/src/lib/FallbackWatcher.ts b/packages/language-server/src/lib/FallbackWatcher.ts index eac647051..81ec47349 100644 --- a/packages/language-server/src/lib/FallbackWatcher.ts +++ b/packages/language-server/src/lib/FallbackWatcher.ts @@ -1,8 +1,14 @@ import { FSWatcher, watch } from 'chokidar'; import { debounce } from 'lodash'; import { join } from 'path'; -import { DidChangeWatchedFilesParams, FileChangeType, FileEvent } from 'vscode-languageserver'; +import { + DidChangeWatchedFilesParams, + FileChangeType, + FileEvent, + RelativePattern +} from 'vscode-languageserver'; import { pathToUrl } from '../utils'; +import { fileURLToPath } from 'url'; type DidChangeHandler = (para: DidChangeWatchedFilesParams) => void; @@ -14,10 +20,10 @@ export class FallbackWatcher { private undeliveredFileEvents: FileEvent[] = []; - constructor(glob: string, workspacePaths: string[]) { + constructor(recursivePatterns: string, workspacePaths: string[]) { const gitOrNodeModules = /\.git|node_modules/; this.watcher = watch( - workspacePaths.map((workspacePath) => join(workspacePath, glob)), + workspacePaths.map((workspacePath) => join(workspacePath, recursivePatterns)), { ignored: (path: string) => gitOrNodeModules.test(path) && @@ -65,6 +71,18 @@ export class FallbackWatcher { this.callbacks.push(callback); } + watchDirectory(patterns: RelativePattern[]) { + for (const pattern of patterns) { + const basePath = fileURLToPath( + typeof pattern.baseUri === 'string' ? pattern.baseUri : pattern.baseUri.uri + ); + if (!basePath) { + continue; + } + this.watcher.add(join(basePath, pattern.pattern)); + } + } + dispose() { this.watcher.close(); } diff --git a/packages/language-server/src/lib/documents/DocumentManager.ts b/packages/language-server/src/lib/documents/DocumentManager.ts index bbf86303b..048219717 100644 --- a/packages/language-server/src/lib/documents/DocumentManager.ts +++ b/packages/language-server/src/lib/documents/DocumentManager.ts @@ -47,15 +47,16 @@ export class DocumentManager { let document: Document; if (this.documents.has(textDocument.uri)) { document = this.documents.get(textDocument.uri)!; + document.openedByClient = openedByClient; document.setText(textDocument.text); } else { document = this.createDocument(textDocument); + document.openedByClient = openedByClient; this.documents.set(textDocument.uri, document); this.notify('documentOpen', document); } this.notify('documentChange', document); - document.openedByClient = openedByClient; return document; } diff --git a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts index c7c3078aa..f3b8d6f48 100644 --- a/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts +++ b/packages/language-server/src/plugins/typescript/LSAndTSDocResolver.ts @@ -1,6 +1,6 @@ import { dirname, join } from 'path'; import ts from 'typescript'; -import { TextDocumentContentChangeEvent } from 'vscode-languageserver'; +import { RelativePattern, TextDocumentContentChangeEvent } from 'vscode-languageserver'; import { Document, DocumentManager } from '../../lib/documents'; import { LSConfigManager } from '../../ls-config'; import { @@ -22,7 +22,7 @@ import { import { createProjectService } from './serviceCache'; import { GlobalSnapshotsManager, SnapshotManager } from './SnapshotManager'; import { isSubPath } from './utils'; -import { FileMap } from '../../lib/documents/fileCollection'; +import { FileMap, FileSet } from '../../lib/documents/fileCollection'; interface LSAndTSDocResolverOptions { notifyExceedSizeLimit?: () => void; @@ -39,6 +39,8 @@ interface LSAndTSDocResolverOptions { onProjectReloaded?: () => void; watch?: boolean; tsSystem?: ts.System; + watchDirectory?: (patterns: RelativePattern[]) => void; + nonRecursiveWatchPattern?: string; } export class LSAndTSDocResolver { @@ -94,7 +96,17 @@ export class LSAndTSDocResolver { } }); - this.watchers = new FileMap(this.tsSystem.useCaseSensitiveFileNames); + this.packageJsonWatchers = new FileMap(this.tsSystem.useCaseSensitiveFileNames); + this.watchedDirectories = new FileSet(this.tsSystem.useCaseSensitiveFileNames); + + // workspaceUris are already watched during initialization + for (const root of this.workspaceUris) { + const rootPath = urlToPath(root); + if (rootPath) { + this.watchedDirectories.add(rootPath); + } + } + this.lsDocumentContext = { ambientTypesSource: this.options?.isSvelteCheck ? 'svelte-check' : 'svelte2tsx', createDocument: this.createDocument, @@ -105,7 +117,11 @@ export class LSAndTSDocResolver { onProjectReloaded: this.options?.onProjectReloaded, watchTsConfig: !!this.options?.watch, tsSystem: this.tsSystem, - projectService: projectService + projectService, + watchDirectory: this.options?.watchDirectory + ? this.watchDirectory.bind(this) + : undefined, + nonRecursiveWatchPattern: this.options?.nonRecursiveWatchPattern }; } @@ -131,9 +147,9 @@ export class LSAndTSDocResolver { private getCanonicalFileName: GetCanonicalFileName; private userPreferencesAccessor: { preferences: ts.UserPreferences }; - private readonly watchers: FileMap; - + private readonly packageJsonWatchers: FileMap; private lsDocumentContext: LanguageServiceDocumentContext; + private readonly watchedDirectories: FileSet; async getLSForPath(path: string) { return (await this.getTSService(path)).getService(); @@ -209,15 +225,15 @@ export class LSAndTSDocResolver { this.docManager.releaseDocument(uri); } - async invalidateModuleCache(filePath: string) { - await forAllServices((service) => service.invalidateModuleCache(filePath)); + async invalidateModuleCache(filePaths: string[]) { + await forAllServices((service) => service.invalidateModuleCache(filePaths)); } /** * Updates project files in all existing ts services */ - async updateProjectFiles() { - await forAllServices((service) => service.updateProjectFiles()); + async updateProjectFiles(watcherNewFiles: string[]) { + await forAllServices((service) => service.scheduleProjectFileUpdate(watcherNewFiles)); } /** @@ -227,6 +243,20 @@ export class LSAndTSDocResolver { path: string, changes?: TextDocumentContentChangeEvent[] ): Promise { + await this.updateExistingFile(path, (service) => service.updateTsOrJsFile(path, changes)); + } + + async updateExistingSvelteFile(path: string): Promise { + const newDocument = this.createDocument(path, this.tsSystem.readFile(path) ?? ''); + await this.updateExistingFile(path, (service) => { + service.updateSnapshot(newDocument); + }); + } + + private async updateExistingFile( + path: string, + cb: (service: LanguageServiceContainer) => void + ) { path = normalizePath(path); // Only update once because all snapshots are shared between // services. Since we don't have a current version of TS/JS @@ -235,7 +265,7 @@ export class LSAndTSDocResolver { await forAllServices((service) => { if (service.hasFile(path) && !didUpdate) { didUpdate = true; - service.updateTsOrJsFile(path, changes); + cb(service); } }); } @@ -290,8 +320,8 @@ export class LSAndTSDocResolver { return { ...sys, readFile: (path, encoding) => { - if (path.endsWith('package.json') && !this.watchers.has(path)) { - this.watchers.set( + if (path.endsWith('package.json') && !this.packageJsonWatchers.has(path)) { + this.packageJsonWatchers.set( path, watchFile(path, this.onPackageJsonWatchChange.bind(this), 3_000) ); @@ -309,8 +339,8 @@ export class LSAndTSDocResolver { const normalizedPath = projectService?.toPath(path); if (onWatchChange === ts.FileWatcherEventKind.Deleted) { - this.watchers.get(path)?.close(); - this.watchers.delete(path); + this.packageJsonWatchers.get(path)?.close(); + this.packageJsonWatchers.delete(path); packageJsonCache?.delete(normalizedPath); } else { packageJsonCache?.addOrUpdate(normalizedPath); @@ -345,4 +375,20 @@ export class LSAndTSDocResolver { this.globalSnapshotsManager.updateTsOrJsFile(snapshot.filePath); }); } + + private watchDirectory(patterns: RelativePattern[]) { + if (!this.options?.watchDirectory || patterns.length === 0) { + return; + } + + for (const pattern of patterns) { + const uri = typeof pattern.baseUri === 'string' ? pattern.baseUri : pattern.baseUri.uri; + for (const watched of this.watchedDirectories) { + if (isSubPath(watched, uri, this.getCanonicalFileName)) { + return; + } + } + } + this.options.watchDirectory(patterns); + } } diff --git a/packages/language-server/src/plugins/typescript/SnapshotManager.ts b/packages/language-server/src/plugins/typescript/SnapshotManager.ts index 7ee160ffc..6bf64f0b6 100644 --- a/packages/language-server/src/plugins/typescript/SnapshotManager.ts +++ b/packages/language-server/src/plugins/typescript/SnapshotManager.ts @@ -99,28 +99,37 @@ export class SnapshotManager { private readonly projectFileToOriginalCasing: Map; private getCanonicalFileName: GetCanonicalFileName; + private watchingCanonicalDirectories: Map | undefined; private readonly watchExtensions = [ ts.Extension.Dts, + ts.Extension.Dcts, + ts.Extension.Dmts, ts.Extension.Js, + ts.Extension.Cjs, + ts.Extension.Mjs, ts.Extension.Jsx, ts.Extension.Ts, + ts.Extension.Mts, + ts.Extension.Cts, ts.Extension.Tsx, - ts.Extension.Json + ts.Extension.Json, + '.svelte' ]; constructor( private globalSnapshotsManager: GlobalSnapshotsManager, private fileSpec: TsFilesSpec, private workspaceRoot: string, + private tsSystem: ts.System, projectFiles: string[], - useCaseSensitiveFileNames = ts.sys.useCaseSensitiveFileNames + wildcardDirectories: ts.MapLike | undefined ) { this.onSnapshotChange = this.onSnapshotChange.bind(this); this.globalSnapshotsManager.onChange(this.onSnapshotChange); - this.documents = new FileMap(useCaseSensitiveFileNames); + this.documents = new FileMap(tsSystem.useCaseSensitiveFileNames); this.projectFileToOriginalCasing = new Map(); - this.getCanonicalFileName = createGetCanonicalFileName(useCaseSensitiveFileNames); + this.getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames); projectFiles.forEach((originalCasing) => this.projectFileToOriginalCasing.set( @@ -128,6 +137,13 @@ export class SnapshotManager { originalCasing ) ); + + this.watchingCanonicalDirectories = new Map( + Object.entries(wildcardDirectories ?? {}).map(([dir, flags]) => [ + this.getCanonicalFileName(dir), + flags + ]) + ); } private onSnapshotChange(fileName: string, document: DocumentSnapshot | undefined) { @@ -144,16 +160,45 @@ export class SnapshotManager { } } - updateProjectFiles(): void { - const { include, exclude } = this.fileSpec; + areIgnoredFromNewFileWatch(watcherNewFiles: string[]): boolean { + const { include } = this.fileSpec; // Since we default to not include anything, // just don't waste time on this + if (include?.length === 0 || !this.watchingCanonicalDirectories) { + return true; + } + + for (const newFile of watcherNewFiles) { + const path = this.getCanonicalFileName(normalizePath(newFile)); + if (this.projectFileToOriginalCasing.has(path)) { + continue; + } + + for (const [dir, flags] of this.watchingCanonicalDirectories) { + if (path.startsWith(dir)) { + if (!(flags & ts.WatchDirectoryFlags.Recursive)) { + const relative = path.slice(dir.length); + if (relative.includes('/')) { + continue; + } + } + return false; + } + } + } + + return true; + } + + updateProjectFiles(): void { + const { include, exclude } = this.fileSpec; + if (include?.length === 0) { return; } - const projectFiles = ts.sys + const projectFiles = this.tsSystem .readDirectory(this.workspaceRoot, this.watchExtensions, exclude, include) .map(normalizePath); diff --git a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts index e729fd736..5ce566ade 100644 --- a/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts +++ b/packages/language-server/src/plugins/typescript/TypeScriptPlugin.ts @@ -28,7 +28,12 @@ import { TextDocumentContentChangeEvent, WorkspaceEdit } from 'vscode-languageserver'; -import { Document, getTextInRange, mapSymbolInformationToOriginal } from '../../lib/documents'; +import { + Document, + DocumentManager, + getTextInRange, + mapSymbolInformationToOriginal +} from '../../lib/documents'; import { LSConfigManager, LSTypescriptConfig } from '../../ls-config'; import { isNotNullOrUndefined, isZeroLengthRange } from '../../utils'; import { @@ -87,8 +92,8 @@ import { isAttributeName, isAttributeShorthand, isEventHandler } from './svelte- import { convertToLocationForReferenceOrDefinition, convertToLocationRange, - getScriptKindFromFileName, isInScript, + isSvelteFilePath, symbolKindFromString } from './utils'; @@ -118,6 +123,7 @@ export class TypeScriptPlugin { __name = 'ts'; private readonly configManager: LSConfigManager; + private readonly documentManager: DocumentManager; private readonly lsAndTsDocResolver: LSAndTSDocResolver; private readonly completionProvider: CompletionsProviderImpl; private readonly codeActionsProvider: CodeActionsProviderImpl; @@ -141,9 +147,11 @@ export class TypeScriptPlugin constructor( configManager: LSConfigManager, lsAndTsDocResolver: LSAndTSDocResolver, - workspaceUris: string[] + workspaceUris: string[], + documentManager: DocumentManager ) { this.configManager = configManager; + this.documentManager = documentManager; this.lsAndTsDocResolver = lsAndTsDocResolver; this.completionProvider = new CompletionsProviderImpl( this.lsAndTsDocResolver, @@ -493,7 +501,7 @@ export class TypeScriptPlugin } async onWatchFileChanges(onWatchFileChangesParas: OnWatchFileChangesPara[]): Promise { - let doneUpdateProjectFiles = false; + const newFiles: string[] = []; for (const { fileName, changeType } of onWatchFileChangesParas) { const pathParts = fileName.split(/\/|\\/); @@ -513,28 +521,36 @@ export class TypeScriptPlugin continue; } - const scriptKind = getScriptKindFromFileName(fileName); - if (scriptKind === ts.ScriptKind.Unknown) { - // We don't deal with svelte files here - continue; - } + const isSvelteFile = isSvelteFilePath(fileName); + const isClientSvelteFile = + isSvelteFile && this.documentManager.get(fileName)?.openedByClient; if (changeType === FileChangeType.Deleted) { - await this.lsAndTsDocResolver.deleteSnapshot(fileName); + if (!isClientSvelteFile) { + await this.lsAndTsDocResolver.deleteSnapshot(fileName); + } continue; } if (changeType === FileChangeType.Created) { - if (!doneUpdateProjectFiles) { - doneUpdateProjectFiles = true; - await this.lsAndTsDocResolver.updateProjectFiles(); + newFiles.push(fileName); + continue; + } + + if (isSvelteFile) { + if (!isClientSvelteFile) { + await this.lsAndTsDocResolver.updateExistingSvelteFile(fileName); } - await this.lsAndTsDocResolver.invalidateModuleCache(fileName); continue; } await this.lsAndTsDocResolver.updateExistingTsOrJsFile(fileName); } + + if (newFiles.length) { + await this.lsAndTsDocResolver.updateProjectFiles(newFiles); + await this.lsAndTsDocResolver.invalidateModuleCache(newFiles); + } } async updateTsOrJsFile( @@ -641,13 +657,6 @@ export class TypeScriptPlugin return this.foldingRangeProvider.getFoldingRanges(document); } - /** - * @internal Public for tests only - */ - public getSnapshotManager(fileName: string) { - return this.lsAndTsDocResolver.getSnapshotManager(fileName); - } - private featureEnabled(feature: keyof LSTypescriptConfig) { return ( this.configManager.enabled('typescript.enable') && diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index fd3c9a15f..f0471e78c 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -1,12 +1,12 @@ import { basename, dirname, join, resolve } from 'path'; import ts from 'typescript'; -import { TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; +import { RelativePattern, TextDocumentContentChangeEvent } from 'vscode-languageserver-protocol'; import { getPackageInfo, importSvelte } from '../../importPackage'; import { Document } from '../../lib/documents'; import { configLoader } from '../../lib/documents/configLoader'; import { FileMap, FileSet } from '../../lib/documents/fileCollection'; import { Logger } from '../../logger'; -import { createGetCanonicalFileName, normalizePath, urlToPath } from '../../utils'; +import { createGetCanonicalFileName, normalizePath, pathToUrl, urlToPath } from '../../utils'; import { DocumentSnapshot, SvelteSnapshotOptions } from './DocumentSnapshot'; import { createSvelteModuleLoader } from './module-loader'; import { @@ -34,8 +34,8 @@ export interface LanguageServiceContainer { getService(skipSynchronize?: boolean): ts.LanguageService; updateSnapshot(documentOrFilePath: Document | string): DocumentSnapshot; deleteSnapshot(filePath: string): void; - invalidateModuleCache(filePath: string): void; - updateProjectFiles(): void; + invalidateModuleCache(filePath: string[]): void; + scheduleProjectFileUpdate(watcherNewFiles: string[]): void; updateTsOrJsFile(fileName: string, changes?: TextDocumentContentChangeEvent[]): void; /** * Checks if a file is present in the project. @@ -92,6 +92,7 @@ const configFileModifiedTime = new FileMap(); const configFileForOpenFiles = new FileMap(); const pendingReloads = new FileSet(); const documentRegistries = new Map(); +const pendingForAllServices = new Set>(); /** * For testing only: Reset the cache for services. @@ -115,6 +116,8 @@ export interface LanguageServiceDocumentContext { watchTsConfig: boolean; tsSystem: ts.System; projectService: ProjectService | undefined; + watchDirectory: ((patterns: RelativePattern[]) => void) | undefined; + nonRecursiveWatchPattern: string | undefined; } export async function getService( @@ -158,6 +161,13 @@ export async function getService( export async function forAllServices( cb: (service: LanguageServiceContainer) => any ): Promise { + const promise = forAllServicesWorker(cb); + pendingForAllServices.add(promise); + await promise; + pendingForAllServices.delete(promise); +} + +async function forAllServicesWorker(cb: (service: LanguageServiceContainer) => any): Promise { for (const service of services.values()) { cb(await service); } @@ -192,6 +202,10 @@ export async function getServiceForTsconfig( service = await services.get(tsconfigPathOrWorkspacePath)!; } + if (pendingForAllServices.size > 0) { + await Promise.all(pendingForAllServices); + } + return service; } @@ -207,15 +221,22 @@ async function createLanguageService( errors: configErrors, fileNames: files, raw, - extendedConfigPaths + extendedConfigPaths, + wildcardDirectories } = getParsedConfig(); + + const getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames); + watchWildCardDirectories(); + // raw is the tsconfig merged with extending config // see: https://github.com/microsoft/TypeScript/blob/08e4f369fbb2a5f0c30dee973618d65e6f7f09f8/src/compiler/commandLineParser.ts#L2537 const snapshotManager = new SnapshotManager( docContext.globalSnapshotsManager, raw, workspacePath, - files + tsSystem, + files, + wildcardDirectories ); // Load all configs within the tsconfig scope and the one above so that they are all loaded @@ -261,8 +282,7 @@ async function createLanguageService( let languageServiceReducedMode = false; let projectVersion = 0; let dirty = false; - - const getCanonicalFileName = createGetCanonicalFileName(tsSystem.useCaseSensitiveFileNames); + let pendingProjectFileUpdate = false; const host: ts.LanguageServiceHost = { log: (message) => Logger.debug(`[ts] ${message}`), @@ -316,7 +336,7 @@ async function createLanguageService( getService, updateSnapshot, deleteSnapshot, - updateProjectFiles, + scheduleProjectFileUpdate, updateTsOrJsFile, hasFile, fileBelongsToProject, @@ -328,7 +348,34 @@ async function createLanguageService( dispose }; + function watchWildCardDirectories() { + if (!wildcardDirectories || !docContext.watchDirectory) { + return; + } + + const patterns: RelativePattern[] = []; + + Object.entries(wildcardDirectories).forEach(([dir, flags]) => { + // already watched + if (getCanonicalFileName(dir).startsWith(workspacePath)) { + return; + } + patterns.push({ + baseUri: pathToUrl(dir), + pattern: + (flags & ts.WatchDirectoryFlags.Recursive ? `**/` : '') + + docContext.nonRecursiveWatchPattern + }); + }); + + docContext.watchDirectory?.(patterns); + } + function getService(skipSynchronize?: boolean) { + if (pendingProjectFileUpdate) { + updateProjectFiles(); + pendingProjectFileUpdate = false; + } if (!skipSynchronize) { updateIfDirty(); } @@ -342,11 +389,13 @@ async function createLanguageService( configFileForOpenFiles.delete(filePath); } - function invalidateModuleCache(filePath: string) { - svelteModuleLoader.deleteFromModuleCache(filePath); - svelteModuleLoader.deleteUnresolvedResolutionsFromCache(filePath); + function invalidateModuleCache(filePaths: string[]) { + for (const filePath of filePaths) { + svelteModuleLoader.deleteFromModuleCache(filePath); + svelteModuleLoader.deleteUnresolvedResolutionsFromCache(filePath); - scheduleUpdate(filePath); + scheduleUpdate(filePath); + } } function updateSnapshot(documentOrFilePath: Document | string): DocumentSnapshot { @@ -427,17 +476,23 @@ async function createLanguageService( return doc; } - function updateProjectFiles(): void { + function scheduleProjectFileUpdate(watcherNewFiles: string[]): void { + if (snapshotManager.areIgnoredFromNewFileWatch(watcherNewFiles)) { + return; + } + scheduleUpdate(); + pendingProjectFileUpdate = true; + } + + function updateProjectFiles(): void { const projectFileCountBefore = snapshotManager.getProjectFileNames().length; snapshotManager.updateProjectFiles(); const projectFileCountAfter = snapshotManager.getProjectFileNames().length; - if (projectFileCountAfter <= projectFileCountBefore) { - return; + if (projectFileCountAfter > projectFileCountBefore) { + reduceLanguageServiceCapabilityIfFileSizeTooBig(); } - - reduceLanguageServiceCapabilityIfFileSizeTooBig(); } function getScriptFileNames() { diff --git a/packages/language-server/src/server.ts b/packages/language-server/src/server.ts index fcc031466..632681f43 100644 --- a/packages/language-server/src/server.ts +++ b/packages/language-server/src/server.ts @@ -22,7 +22,8 @@ import { InlayHintRequest, SemanticTokensRefreshRequest, InlayHintRefreshRequest, - DidChangeWatchedFilesNotification + DidChangeWatchedFilesNotification, + RelativePattern } from 'vscode-languageserver'; import { IPCMessageReader, IPCMessageWriter, createConnection } from 'vscode-languageserver/node'; import { DiagnosticsManager } from './lib/DiagnosticsManager'; @@ -98,6 +99,13 @@ export function startServer(options?: LSOptions) { const pluginHost = new PluginHost(docManager); let sveltePlugin: SveltePlugin = undefined as any; let watcher: FallbackWatcher | undefined; + let pendingWatchPatterns: RelativePattern[] = []; + let watchDirectory: (patterns: RelativePattern[]) => void = (patterns) => { + pendingWatchPatterns = patterns; + }; + + const nonRecursiveWatchPattern = '*.{ts,js,mts,mjs,cjs,cts,json,svelte}'; + const recursiveWatchPattern = '**/' + nonRecursiveWatchPattern; connection.onInitialize((evt) => { const workspaceUris = evt.workspaceFolders?.map((folder) => folder.uri.toString()) ?? [ @@ -110,8 +118,12 @@ export function startServer(options?: LSOptions) { if (!evt.capabilities.workspace?.didChangeWatchedFiles) { const workspacePaths = workspaceUris.map(urlToPath).filter(isNotNullOrUndefined); - watcher = new FallbackWatcher('**/*.{ts,js}', workspacePaths); + watcher = new FallbackWatcher(recursiveWatchPattern, workspacePaths); watcher.onDidChangeWatchedFiles(onDidChangeWatchedFiles); + + watchDirectory = (patterns) => { + watcher?.watchDirectory(patterns); + }; } const isTrusted: boolean = evt.initializationOptions?.isTrusted ?? true; @@ -183,9 +195,12 @@ export function startServer(options?: LSOptions) { new LSAndTSDocResolver(docManager, normalizedWorkspaceUris, configManager, { notifyExceedSizeLimit: notifyTsServiceExceedSizeLimit, onProjectReloaded: refreshCrossFilesSemanticFeatures, - watch: true + watch: true, + nonRecursiveWatchPattern, + watchDirectory: (patterns) => watchDirectory(patterns) }), - normalizedWorkspaceUris + normalizedWorkspaceUris, + docManager ) ); @@ -299,18 +314,38 @@ export function startServer(options?: LSOptions) { }); connection.onInitialized(() => { - if ( - !watcher && - configManager.getClientCapabilities()?.workspace?.didChangeWatchedFiles - ?.dynamicRegistration - ) { - connection?.client.register(DidChangeWatchedFilesNotification.type, { - watchers: [ - { - globPattern: '**/*.{ts,js,mts,mjs,cjs,cts,json}' - } - ] - }); + if (watcher) { + return; + } + + const didChangeWatchedFiles = + configManager.getClientCapabilities()?.workspace?.didChangeWatchedFiles; + + if (!didChangeWatchedFiles?.dynamicRegistration) { + return; + } + + // still watch the roots since some files might be referenced but not included in the project + connection?.client.register(DidChangeWatchedFilesNotification.type, { + watchers: [ + { + globPattern: recursiveWatchPattern + } + ] + }); + + if (didChangeWatchedFiles.relativePatternSupport) { + watchDirectory = (patterns) => { + connection?.client.register(DidChangeWatchedFilesNotification.type, { + watchers: patterns.map((pattern) => ({ + globPattern: pattern + })) + }); + }; + if (pendingWatchPatterns.length) { + watchDirectory(pendingWatchPatterns); + pendingWatchPatterns = []; + } } }); diff --git a/packages/language-server/src/svelte-check.ts b/packages/language-server/src/svelte-check.ts index 4d863a882..b6100e60c 100644 --- a/packages/language-server/src/svelte-check.ts +++ b/packages/language-server/src/svelte-check.ts @@ -94,7 +94,12 @@ export class SvelteCheck { } ); this.pluginHost.register( - new TypeScriptPlugin(this.configManager, this.lsAndTSDocResolver, workspaceUris) + new TypeScriptPlugin( + this.configManager, + this.lsAndTSDocResolver, + workspaceUris, + this.docManager + ) ); } diff --git a/packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts b/packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts index 9e65beb1c..286c15b44 100644 --- a/packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts +++ b/packages/language-server/test/plugins/typescript/TypescriptPlugin.test.ts @@ -42,13 +42,22 @@ describe('TypescriptPlugin', function () { const document = new Document(pathToUrl(filePath), ts.sys.readFile(filePath) || ''); const lsConfigManager = new LSConfigManager(); const workspaceUris = [pathToUrl(testDir)]; + const lsAndTsDocResolver = new LSAndTSDocResolver( + docManager, + workspaceUris, + lsConfigManager, + { + nonRecursiveWatchPattern: '**/*.{ts,js}' + } + ); const plugin = new TypeScriptPlugin( lsConfigManager, - new LSAndTSDocResolver(docManager, [pathToUrl(testDir)], lsConfigManager), - workspaceUris + lsAndTsDocResolver, + workspaceUris, + docManager ); docManager.openClientDocument('some doc'); - return { plugin, document }; + return { plugin, document, lsAndTsDocResolver }; } it('provides document symbols', async () => { @@ -609,14 +618,16 @@ describe('TypescriptPlugin', function () { }); const setupForOnWatchedFileChanges = async () => { - const { plugin, document } = setup('empty.svelte'); + const { plugin, document, lsAndTsDocResolver } = setup('empty.svelte'); const targetSvelteFile = document.getFilePath()!; - const snapshotManager = await plugin.getSnapshotManager(targetSvelteFile); + const snapshotManager = (await lsAndTsDocResolver.getTSService(targetSvelteFile)) + .snapshotManager; return { snapshotManager, plugin, - targetSvelteFile + targetSvelteFile, + lsAndTsDocResolver }; }; @@ -677,7 +688,8 @@ describe('TypescriptPlugin', function () { }); const testForOnWatchedFileAdd = async (filePath: string, shouldExist: boolean) => { - const { snapshotManager, plugin, targetSvelteFile } = await setupForOnWatchedFileChanges(); + const { snapshotManager, plugin, targetSvelteFile, lsAndTsDocResolver } = + await setupForOnWatchedFileChanges(); const addFile = path.join(path.dirname(targetSvelteFile), filePath); const dir = path.dirname(addFile); @@ -697,6 +709,8 @@ describe('TypescriptPlugin', function () { } ]); + (await lsAndTsDocResolver.getTSService(targetSvelteFile)).getService(); + assert.equal(snapshotManager.has(addFile), shouldExist); await plugin.onWatchFileChanges([ diff --git a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts index 6aa9918bf..92601459f 100644 --- a/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/DiagnosticsProvider.test.ts @@ -43,7 +43,7 @@ describe('DiagnosticsProvider', function () { const newFilePath = normalizePath(path.join(testDir, 'doesntexistyet.js')) || ''; writeFileSync(newFilePath, 'export default function foo() {}'); assert.ok(existsSync(newFilePath)); - await lsAndTsDocResolver.invalidateModuleCache(newFilePath); + await lsAndTsDocResolver.invalidateModuleCache([newFilePath]); try { const diagnostics2 = await plugin.getDiagnostics(document); @@ -68,7 +68,7 @@ describe('DiagnosticsProvider', function () { const newTsFilePath = normalizePath(path.join(testDir, 'doesntexistyet.ts')) || ''; writeFileSync(newFilePath, 'export function foo() {}'); assert.ok(existsSync(newFilePath)); - await lsAndTsDocResolver.invalidateModuleCache(newFilePath); + await lsAndTsDocResolver.invalidateModuleCache([newFilePath]); try { const diagnostics2 = await plugin.getDiagnostics(document); @@ -80,7 +80,7 @@ describe('DiagnosticsProvider', function () { writeFileSync(newTsFilePath, 'export default function foo() {}'); assert.ok(existsSync(newTsFilePath)); - await lsAndTsDocResolver.invalidateModuleCache(newTsFilePath); + await lsAndTsDocResolver.invalidateModuleCache([newTsFilePath]); try { const diagnostics3 = await plugin.getDiagnostics(document); diff --git a/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts b/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts index d3ce11c92..abef92110 100644 --- a/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts +++ b/packages/language-server/test/plugins/typescript/features/RenameProvider.test.ts @@ -953,8 +953,6 @@ describe('RenameProvider', function () { const result = await provider.rename(renameRunes, Position.create(1, 54), 'newName'); - console.log(JSON.stringify(result, null, 2)); - assert.deepStrictEqual(result, { changes: { [getUri('rename-runes.svelte')]: [ diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index f8b36914a..8d3ee0efe 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -2,11 +2,11 @@ import assert from 'assert'; import path from 'path'; import ts from 'typescript'; import { Document } from '../../../src/lib/documents'; +import { GlobalSnapshotsManager } from '../../../src/plugins/typescript/SnapshotManager'; import { - getService, - LanguageServiceDocumentContext + LanguageServiceDocumentContext, + getService } from '../../../src/plugins/typescript/service'; -import { GlobalSnapshotsManager } from '../../../src/plugins/typescript/SnapshotManager'; import { pathToUrl } from '../../../src/utils'; import { createVirtualTsSystem, getRandomVirtualDirPath } from './test-utils'; @@ -16,6 +16,7 @@ describe('service', () => { function setup() { const virtualSystem = createVirtualTsSystem(testDir); + const rootUris = [pathToUrl(testDir)]; const lsDocumentContext: LanguageServiceDocumentContext = { ambientTypesSource: 'svelte2tsx', createDocument(fileName, content) { @@ -28,11 +29,11 @@ describe('service', () => { watchTsConfig: false, notifyExceedSizeLimit: undefined, onProjectReloaded: undefined, - projectService: undefined + projectService: undefined, + nonRecursiveWatchPattern: undefined, + watchDirectory: undefined }; - const rootUris = [pathToUrl(testDir)]; - return { virtualSystem, lsDocumentContext, rootUris }; } diff --git a/packages/language-server/test/plugins/typescript/test-utils.ts b/packages/language-server/test/plugins/typescript/test-utils.ts index 522471464..4c398c12c 100644 --- a/packages/language-server/test/plugins/typescript/test-utils.ts +++ b/packages/language-server/test/plugins/typescript/test-utils.ts @@ -88,6 +88,19 @@ export function createVirtualTsSystem(currentDirectory: string): ts.System { }, getModifiedTime(path) { return modifiedTime.get(normalizePath(toAbsolute(path))); + }, + readDirectory(path, _extensions, _exclude, include, _depth) { + if (include && (include.length != 1 || include[0] !== '**/*')) { + throw new Error( + 'include pattern matching not implemented. Mock it if the test needs it. Pattern: ' + + include + ); + } + + const normalizedPath = normalizePath(toAbsolute(path)); + return Array.from(virtualFs.keys()).filter((fileName) => + fileName.startsWith(normalizedPath) + ); } }; diff --git a/packages/language-server/test/plugins/typescript/typescript-performance.test.ts b/packages/language-server/test/plugins/typescript/typescript-performance.test.ts index 305c8fc02..5a1a7523e 100644 --- a/packages/language-server/test/plugins/typescript/typescript-performance.test.ts +++ b/packages/language-server/test/plugins/typescript/typescript-performance.test.ts @@ -19,7 +19,8 @@ describe('TypeScript Plugin Performance Tests', () => { const plugin = new TypeScriptPlugin( pluginManager, new LSAndTSDocResolver(docManager, workspaceUris, pluginManager), - workspaceUris + workspaceUris, + docManager ); docManager.openClientDocument({ uri, text: document.getText() }); const append = (newText: string) =>