From 7d4f8a73227beb6ed26031f9d426bd21e8c2da3f Mon Sep 17 00:00:00 2001 From: "Lyu, Wei-Da" <36730922+jasonlyu123@users.noreply.github.com> Date: Mon, 15 Jan 2024 17:24:51 +0800 Subject: [PATCH] fix: prevent completion using empty cache (#2258) The export info cache is created with an edit that doesn't trigger auto imports. And then The onFileChanged call marks the cache to be usable even though nothing is cached yet. #2260 --- .../src/plugins/typescript/service.ts | 11 +++-- .../features/CompletionProvider.test.ts | 49 +++++++++++++++++++ 2 files changed, 57 insertions(+), 3 deletions(-) diff --git a/packages/language-server/src/plugins/typescript/service.ts b/packages/language-server/src/plugins/typescript/service.ts index 5f2571e75..3bb33f880 100644 --- a/packages/language-server/src/plugins/typescript/service.ts +++ b/packages/language-server/src/plugins/typescript/service.ts @@ -719,11 +719,16 @@ async function createLanguageService( dirty = false; - if (!oldProgram) { + // https://github.com/microsoft/TypeScript/blob/23faef92703556567ddbcb9afb893f4ba638fc20/src/server/project.ts#L1624 + // host.getCachedExportInfoMap will create the cache if it doesn't exist + // so we need to check the property instead + const exportMapCache = project?.exportMapCache; + if (!oldProgram || !exportMapCache || exportMapCache.isEmpty()) { changedFilesForExportCache.clear(); return; } + exportMapCache.releaseSymbols(); for (const fileName of changedFilesForExportCache) { const oldFile = oldProgram.getSourceFile(fileName); const newFile = program?.getSourceFile(fileName); @@ -734,10 +739,10 @@ async function createLanguageService( } if (oldFile && newFile) { - host.getCachedExportInfoMap?.().onFileChanged?.(oldFile, newFile, false); + exportMapCache.onFileChanged?.(oldFile, newFile, false); } else { // new file or deleted file - host.getCachedExportInfoMap?.().clear(); + exportMapCache.clear(); } } changedFilesForExportCache.clear(); 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 c2dc0fcd8..ec7ddf7bc 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,55 @@ describe('CompletionProviderImpl', function () { assert.strictEqual(detail, 'Add import from "./Bar.svelte"\n\nclass Bar'); }); + it("doesn't use empty cache", async () => { + const virtualTestDir = getRandomVirtualDirPath(testFilesDir); + const { document, lsAndTsDocResolver, lsConfigManager, docManager } = + setupVirtualEnvironment({ + filename: 'index.svelte', + fileContent: '', + testDir: virtualTestDir + }); + + const completionProvider = new CompletionsProviderImpl(lsAndTsDocResolver, lsConfigManager); + + await lsAndTsDocResolver.getLSAndTSDoc(document); + + docManager.updateDocument(document, [ + { + range: Range.create( + Position.create(0, document.content.length), + Position.create(0, document.content.length) + ), + text: ' ' + } + ]); + + docManager.openClientDocument({ + text: '', + uri: pathToUrl(join(virtualTestDir, 'Bar.svelte')) + }); + + docManager.updateDocument(document, [ + { + range: Range.create( + Position.create(0, document.content.length), + Position.create(0, document.content.length) + ), + text: ' ' + } + ]); + + const completions = await completionProvider.getCompletions(document, { + line: 0, + character: 9 + }); + + const item2 = completions?.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('can auto import new export', async () => { const virtualTestDir = getRandomVirtualDirPath(testFilesDir); const { document, lsAndTsDocResolver, lsConfigManager, virtualSystem } =