diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index d839fda21..09cac7fba 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -18,7 +18,8 @@ import { ensureRealSvelteFilePath, findTsConfigPath, getNearestWorkspaceUri, - hasTsExtensions + hasTsExtensions, + isSvelteFilePath } from './utils'; export interface LanguageServiceContainer { @@ -84,6 +85,7 @@ const extendedConfigToTsConfigPath = new FileMap(); const configFileModifiedTime = new FileMap(); const configFileForOpenFiles = new FileMap(); const pendingReloads = new FileSet(); +const documentRegistries = new Map(); /** * For testing only: Reset the cache for services. @@ -295,7 +297,12 @@ async function createLanguageService( hasInvalidatedResolutions: svelteModuleLoader.mightHaveInvalidatedResolutions }; - let languageService = ts.createLanguageService(host); + const documentRegistry = getOrCreateDocumentRegistry( + host.getCurrentDirectory(), + tsSystem.useCaseSensitiveFileNames + ); + + const languageService = ts.createLanguageService(host, documentRegistry); const transformationConfig: SvelteSnapshotOptions = { parse: svelteCompiler?.parse, version: svelteCompiler?.VERSION, @@ -366,11 +373,6 @@ async function createLanguageService( const newSnapshot = DocumentSnapshot.fromDocument(document, transformationConfig); snapshotManager.set(filePath, newSnapshot); - if (prevSnapshot && prevSnapshot.scriptKind !== newSnapshot.scriptKind) { - // Restart language service as it doesn't handle script kind changes. - languageService.dispose(); - languageService = ts.createLanguageService(host); - } return newSnapshot; } @@ -853,3 +855,53 @@ function scheduleReload(fileName: string) { // where a file update is received before the service is reloaded, swallowing the update pendingReloads.add(fileName); } + +function getOrCreateDocumentRegistry( + currentDirectory: string, + useCaseSensitiveFileNames: boolean +): ts.DocumentRegistry { + // unless it's a multi root workspace, there's only one registry + const key = [currentDirectory, useCaseSensitiveFileNames].join('|'); + + let registry = documentRegistries.get(key); + if (registry) { + return registry; + } + + registry = ts.createDocumentRegistry(useCaseSensitiveFileNames, currentDirectory); + + // impliedNodeFormat is always undefined when the svelte source file is created + // We might patched it later but the registry doesn't know about it + const releaseDocumentWithKey = registry.releaseDocumentWithKey; + registry.releaseDocumentWithKey = ( + path: ts.Path, + key: ts.DocumentRegistryBucketKey, + scriptKind: ts.ScriptKind, + impliedNodeFormat?: ts.ResolutionMode + ) => { + if (isSvelteFilePath(path)) { + releaseDocumentWithKey(path, key, scriptKind, undefined); + return; + } + + releaseDocumentWithKey(path, key, scriptKind, impliedNodeFormat); + }; + + registry.releaseDocument = ( + fileName: string, + compilationSettings: ts.CompilerOptions, + scriptKind: ts.ScriptKind, + impliedNodeFormat?: ts.ResolutionMode + ) => { + if (isSvelteFilePath(fileName)) { + registry?.releaseDocument(fileName, compilationSettings, scriptKind, undefined); + return; + } + + registry?.releaseDocument(fileName, compilationSettings, scriptKind, impliedNodeFormat); + }; + + documentRegistries.set(key, registry); + + return registry; +} diff --git a/packages/language-server/test/plugins/typescript/service.test.ts b/packages/language-server/test/plugins/typescript/service.test.ts index ad56a35bd..1b2801bc1 100644 --- a/packages/language-server/test/plugins/typescript/service.test.ts +++ b/packages/language-server/test/plugins/typescript/service.test.ts @@ -72,6 +72,53 @@ describe('service', () => { }); }); + it('patch release document so dispose do not throw', async () => { + // testing this because the patch rely on ts implementation details + // and we want to be aware of the changes + + const dirPath = getRandomVirtualDirPath(testDir); + const { virtualSystem, lsDocumentContext, rootUris } = setup(); + + virtualSystem.writeFile( + path.join(dirPath, 'tsconfig.json'), + JSON.stringify({ + compilerOptions: { + module: 'NodeNext', + moduleResolution: 'NodeNext' + } + }) + ); + + const ls = await getService( + path.join(dirPath, 'random.svelte'), + rootUris, + lsDocumentContext + ); + + const document = new Document(pathToUrl(path.join(dirPath, 'random.svelte')), ''); + document.openedByClient = true; + ls.updateSnapshot(document); + + const document2 = new Document( + pathToUrl(path.join(dirPath, 'random2.svelte')), + '' + ); + document.openedByClient = true; + ls.updateSnapshot(document2); + + const lang = ls.getService(); + + lang.getProgram(); + + // ensure updated document also works + document2.update(' ', 0, 0); + lang.getProgram(); + + assert.doesNotThrow(() => { + lang.dispose(); + }); + }); + function createReloadTester( docContext: LanguageServiceDocumentContext, testAfterReload: () => Promise