diff --git a/src/compiler/program.ts b/src/compiler/program.ts index c77fa8371185e..1b7710bc0e3c7 100644 --- a/src/compiler/program.ts +++ b/src/compiler/program.ts @@ -560,7 +560,7 @@ namespace ts { program: Program | undefined, rootFileNames: string[], newOptions: CompilerOptions, - getSourceVersion: (path: Path) => string | undefined, + getSourceVersion: (path: Path, fileName: string) => string | undefined, fileExists: (fileName: string) => boolean, hasInvalidatedResolution: HasInvalidatedResolution, hasChangedAutomaticTypeDirectiveNames: boolean, @@ -613,7 +613,7 @@ namespace ts { } function sourceFileVersionUptoDate(sourceFile: SourceFile) { - return sourceFile.version === getSourceVersion(sourceFile.resolvedPath); + return sourceFile.version === getSourceVersion(sourceFile.resolvedPath, sourceFile.fileName); } function projectReferenceUptoDate(oldRef: ProjectReference, newRef: ProjectReference, index: number) { diff --git a/src/harness/client.ts b/src/harness/client.ts index 63ad8b90da83c..7a9f5d610a24a 100644 --- a/src/harness/client.ts +++ b/src/harness/client.ts @@ -808,6 +808,10 @@ namespace ts.server { return notImplemented(); } + clearSourceMapperCache(): never { + return notImplemented(); + } + dispose(): void { throw new Error("dispose is not available through the server layer."); } diff --git a/src/harness/harnessLanguageService.ts b/src/harness/harnessLanguageService.ts index 759384aff3875..23093406e2d53 100644 --- a/src/harness/harnessLanguageService.ts +++ b/src/harness/harnessLanguageService.ts @@ -598,6 +598,9 @@ namespace Harness.LanguageService { getSourceMapper(): never { return ts.notImplemented(); } + clearSourceMapperCache(): never { + return ts.notImplemented(); + } dispose(): void { this.shim.dispose({}); } } diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 74108fa9bf558..ebea941f5c554 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -873,9 +873,11 @@ namespace ts.server { this.delayEnsureProjectForOpenFiles(); } - private delayUpdateProjectGraphs(projects: readonly Project[]) { + private delayUpdateProjectGraphs(projects: readonly Project[], clearSourceMapperCache?: boolean) { if (projects.length) { for (const project of projects) { + // Even if program doesnt change, clear the source mapper cache + if (clearSourceMapperCache) project.clearSourceMapperCache(); this.delayUpdateProjectGraph(project); } this.delayEnsureProjectForOpenFiles(); @@ -1066,7 +1068,7 @@ namespace ts.server { private delayUpdateProjectsOfScriptInfoPath(path: Path) { const info = this.getScriptInfoForPath(path); if (info) { - this.delayUpdateProjectGraphs(info.containingProjects); + this.delayUpdateProjectGraphs(info.containingProjects, /*clearSourceMapperCache*/ true); } } @@ -2537,7 +2539,7 @@ namespace ts.server { const declarationInfo = this.getScriptInfoForPath(declarationInfoPath); if (declarationInfo && declarationInfo.sourceMapFilePath && !isString(declarationInfo.sourceMapFilePath)) { // Update declaration and source projects - this.delayUpdateProjectGraphs(declarationInfo.containingProjects); + this.delayUpdateProjectGraphs(declarationInfo.containingProjects, /*clearSourceMapperCache*/ true); this.delayUpdateSourceInfoProjects(declarationInfo.sourceMapFilePath.sourceInfos); declarationInfo.closeSourceMapFileWatcher(); } diff --git a/src/server/project.ts b/src/server/project.ts index 8994f296eb88b..5e0c912390088 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -384,7 +384,8 @@ namespace ts.server { } getScriptVersion(filename: string) { - const info = this.getOrCreateScriptInfoAndAttachToProject(filename); + // Dont attache to the project if version is asked + const info = this.projectService.getOrCreateScriptInfoNotOpenedByClient(filename, this.currentDirectory, this.directoryStructureHost); return (info && info.getLatestVersion())!; // TODO: GH#18217 } @@ -558,6 +559,11 @@ namespace ts.server { return this.getLanguageService().getSourceMapper(); } + /** @internal */ + clearSourceMapperCache() { + this.languageService.clearSourceMapperCache(); + } + /*@internal*/ getDocumentPositionMapper(generatedFileName: string, sourceFileName?: string): DocumentPositionMapper | undefined { return this.projectService.getDocumentPositionMapper(this, generatedFileName, sourceFileName); @@ -1224,7 +1230,10 @@ namespace ts.server { watcher: this.projectService.watchFactory.watchFile( this.projectService.host, generatedFile, - () => this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this), + () => { + this.clearSourceMapperCache(); + this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this); + }, PollingInterval.High, this.projectService.getWatchOptions(this), WatchType.MissingGeneratedFile, diff --git a/src/server/scriptInfo.ts b/src/server/scriptInfo.ts index 79a46c9077563..0a7a339e5f436 100644 --- a/src/server/scriptInfo.ts +++ b/src/server/scriptInfo.ts @@ -557,6 +557,8 @@ namespace ts.server { } getLatestVersion() { + // Ensure we have updated snapshot to give back latest version + this.textStorage.getSnapshot(); return this.textStorage.getVersion(); } diff --git a/src/services/services.ts b/src/services/services.ts index 0c45e3f2d6ba5..5af1f95e461eb 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -980,11 +980,6 @@ namespace ts { return names; } - public getVersion(path: Path): string { - const file = this.getHostFileInformation(path); - return (file && file.version)!; // TODO: GH#18217 - } - public getScriptSnapshot(path: Path): IScriptSnapshot { const file = this.getHostFileInformation(path); return (file && file.scriptSnapshot)!; // TODO: GH#18217 @@ -1228,7 +1223,7 @@ namespace ts { const projectReferences = hostCache.getProjectReferences(); // If the program is already up-to-date, we can reuse it - if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), path => hostCache!.getVersion(path), fileExists, hasInvalidatedResolution, !!host.hasChangedAutomaticTypeDirectiveNames, projectReferences)) { + if (isProgramUptoDate(program, rootFileNames, hostCache.compilationSettings(), (_path, fileName) => host.getScriptVersion(fileName), fileExists, hasInvalidatedResolution, !!host.hasChangedAutomaticTypeDirectiveNames, projectReferences)) { return; } @@ -2227,6 +2222,7 @@ namespace ts { getEditsForRefactor, toLineColumnOffset: sourceMapper.toLineColumnOffset, getSourceMapper: () => sourceMapper, + clearSourceMapperCache: () => sourceMapper.clearCache(), prepareCallHierarchy, provideCallHierarchyIncomingCalls, provideCallHierarchyOutgoingCalls diff --git a/src/services/types.ts b/src/services/types.ts index 44f73626a8fb5..1df82f8a7e9d9 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -382,6 +382,8 @@ namespace ts { toLineColumnOffset?(fileName: string, position: number): LineAndCharacter; /** @internal */ getSourceMapper(): SourceMapper; + /** @internal */ + clearSourceMapperCache(): void; getCodeFixesAtPosition(fileName: string, start: number, end: number, errorCodes: readonly number[], formatOptions: FormatCodeSettings, preferences: UserPreferences): readonly CodeFixAction[]; getCombinedCodeFix(scope: CombinedCodeFixScope, fixId: {}, formatOptions: FormatCodeSettings, preferences: UserPreferences): CombinedCodeActions; diff --git a/src/testRunner/unittests/services/languageService.ts b/src/testRunner/unittests/services/languageService.ts index 48e35916de48a..01b1a73e9d8c3 100644 --- a/src/testRunner/unittests/services/languageService.ts +++ b/src/testRunner/unittests/services/languageService.ts @@ -80,5 +80,61 @@ export function Component(x: Config): any;` } ); }); + + describe("detects program upto date correctly", () => { + function verifyProgramUptoDate(useProjectVersion: boolean) { + let projectVersion = "1"; + const files = createMap<{ version: string, text: string; }>(); + files.set("/project/root.ts", { version: "1", text: `import { foo } from "./other"` }); + files.set("/project/other.ts", { version: "1", text: `export function foo() { }` }); + files.set("/lib/lib.d.ts", { version: "1", text: projectSystem.libFile.content }); + const host: LanguageServiceHost = { + useCaseSensitiveFileNames: returnTrue, + getCompilationSettings: getDefaultCompilerOptions, + fileExists: path => files.has(path), + getProjectVersion: !useProjectVersion ? undefined : () => projectVersion, + getScriptFileNames: () => ["/project/root.ts"], + getScriptVersion: path => files.get(path)?.version || "", + getScriptSnapshot: path => { + const text = files.get(path)?.text; + return text ? ScriptSnapshot.fromString(text) : undefined; + }, + getCurrentDirectory: () => "/project", + getDefaultLibFileName: () => "/lib/lib.d.ts" + }; + const ls = ts.createLanguageService(host); + const program1 = ls.getProgram()!; + const program2 = ls.getProgram()!; + assert.strictEqual(program1, program2); + verifyProgramFiles(program1); + + // Change other + projectVersion = "2"; + files.set("/project/other.ts", { version: "2", text: `export function foo() { } export function bar() { }` }); + const program3 = ls.getProgram()!; + assert.notStrictEqual(program2, program3); + verifyProgramFiles(program3); + + // change root + projectVersion = "3"; + files.set("/project/root.ts", { version: "2", text: `import { foo, bar } from "./other"` }); + const program4 = ls.getProgram()!; + assert.notStrictEqual(program3, program4); + verifyProgramFiles(program4); + + function verifyProgramFiles(program: Program) { + assert.deepEqual( + program.getSourceFiles().map(f => f.fileName), + ["/lib/lib.d.ts", "/project/other.ts", "/project/root.ts"] + ); + } + } + it("when host implements getProjectVersion", () => { + verifyProgramUptoDate(/*useProjectVersion*/ true); + }); + it("when host does not implement getProjectVersion", () => { + verifyProgramUptoDate(/*useProjectVersion*/ false); + }); + }); }); }