From 5c604f92d7bf058cf5228ebdcb82099ecc95c1d6 Mon Sep 17 00:00:00 2001 From: Vladimir Matveev Date: Thu, 15 Sep 2016 15:39:16 -0700 Subject: [PATCH] correctly update external project if config file is added or removed --- .../unittests/tsserverProjectSystem.ts | 134 ++++++++++++++++++ src/server/editorServices.ts | 83 +++++++++-- 2 files changed, 204 insertions(+), 13 deletions(-) diff --git a/src/harness/unittests/tsserverProjectSystem.ts b/src/harness/unittests/tsserverProjectSystem.ts index e22f1752ce..b455c0d216 100644 --- a/src/harness/unittests/tsserverProjectSystem.ts +++ b/src/harness/unittests/tsserverProjectSystem.ts @@ -1747,4 +1747,138 @@ namespace ts.projectSystem { assert.isTrue(containsNavToItem(items2, "foo", "function"), `Cannot find function symbol "foo".`); }); }); + + describe("external projects", () => { + it("correctly handling add/remove tsconfig - 1", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1;" + }; + const f2 = { + path: "/a/b/lib.ts", + content: "" + }; + const tsconfig = { + path: "/a/b/tsconfig.json", + content: "" + }; + const host = createServerHost([f1, f2]); + const projectService = createProjectService(host); + + // open external project + const projectName = "/a/b/proj1"; + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, f2.path]), + options: {} + }); + projectService.openClientFile(f1.path); + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); + + // rename lib.ts to tsconfig.json + host.reloadFS([f1, tsconfig]); + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, tsconfig.path]), + options: {} + }); + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(projectService.configuredProjects[0], [f1.path]); + + // rename tsconfig.json back to lib.ts + host.reloadFS([f1, f2]); + host.triggerFileWatcherCallback(tsconfig.path, /*removed*/ true); + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, f2.path]), + options: {} + }); + + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [f1.path, f2.path]); + }); + + + it("correctly handling add/remove tsconfig - 2", () => { + const f1 = { + path: "/a/b/app.ts", + content: "let x = 1;" + }; + const cLib = { + path: "/a/b/c/lib.ts", + content: "" + }; + const cTsconfig = { + path: "/a/b/c/tsconfig.json", + content: "{}" + }; + const dLib = { + path: "/a/b/d/lib.ts", + content: "" + }; + const dTsconfig = { + path: "/a/b/d/tsconfig.json", + content: "{}" + }; + const host = createServerHost([f1, cLib, cTsconfig, dLib, dTsconfig]); + const projectService = createProjectService(host); + + // open external project + const projectName = "/a/b/proj1"; + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path]), + options: {} + }); + + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); + + // add two config file as root files + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), + options: {} + }); + projectService.checkNumberOfProjects({ configuredProjects: 2 }); + checkProjectActualFiles(projectService.configuredProjects[0], [cLib.path]); + checkProjectActualFiles(projectService.configuredProjects[1], [dLib.path]); + + // remove one config file + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, dTsconfig.path]), + options: {} + }); + + projectService.checkNumberOfProjects({ configuredProjects: 1 }); + checkProjectActualFiles(projectService.configuredProjects[0], [dLib.path]); + + // remove second config file + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path]), + options: {} + }); + + projectService.checkNumberOfProjects({ externalProjects: 1 }); + checkProjectActualFiles(projectService.externalProjects[0], [f1.path]); + + // open two config files + // add two config file as root files + projectService.openExternalProject({ + projectFileName: projectName, + rootFiles: toExternalFiles([f1.path, cTsconfig.path, dTsconfig.path]), + options: {} + }); + projectService.checkNumberOfProjects({ configuredProjects: 2 }); + checkProjectActualFiles(projectService.configuredProjects[0], [cLib.path]); + checkProjectActualFiles(projectService.configuredProjects[1], [dLib.path]); + + // close all projects - no projects should be opened + projectService.closeExternalProject(projectName); + projectService.checkNumberOfProjects({}); + }); + }); } \ No newline at end of file diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 32aa40591a..099409ca4a 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -1156,19 +1156,25 @@ namespace ts.server { } } - closeExternalProject(uncheckedFileName: string): void { + private closeConfiguredProject(configFile: NormalizedPath): void { + const configuredProject = this.findConfiguredProjectByProjectName(configFile); + if (configuredProject && configuredProject.deleteOpenRef() === 0) { + this.removeProject(configuredProject); + } + } + + closeExternalProject(uncheckedFileName: string, suppressRefresh = false): void { const fileName = toNormalizedPath(uncheckedFileName); const configFiles = this.externalProjectToConfiguredProjectMap[fileName]; if (configFiles) { let shouldRefreshInferredProjects = false; for (const configFile of configFiles) { - const configuredProject = this.findConfiguredProjectByProjectName(configFile); - if (configuredProject && configuredProject.deleteOpenRef() === 0) { - this.removeProject(configuredProject); + if (this.closeConfiguredProject(configFile)) { shouldRefreshInferredProjects = true; } } - if (shouldRefreshInferredProjects) { + delete this.externalProjectToConfiguredProjectMap[fileName]; + if (shouldRefreshInferredProjects && !suppressRefresh) { this.refreshInferredProjects(); } } @@ -1177,18 +1183,14 @@ namespace ts.server { const externalProject = this.findExternalProjectByProjectName(uncheckedFileName); if (externalProject) { this.removeProject(externalProject); - this.refreshInferredProjects(); + if (!suppressRefresh) { + this.refreshInferredProjects(); + } } } } openExternalProject(proj: protocol.ExternalProject): void { - const externalProject = this.findExternalProjectByProjectName(proj.projectFileName); - if (externalProject) { - this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, proj.options, proj.typingOptions, proj.options.compileOnSave, /*configFileErrors*/ undefined); - return; - } - let tsConfigFiles: NormalizedPath[]; const rootFiles: protocol.ExternalFile[] = []; for (const file of proj.rootFiles) { @@ -1200,6 +1202,58 @@ namespace ts.server { rootFiles.push(file); } } + + // sort config files to simplify comparison later + if (tsConfigFiles) { + tsConfigFiles.sort(); + } + + const externalProject = this.findExternalProjectByProjectName(proj.projectFileName); + let exisingConfigFiles: string[]; + if (externalProject) { + if (!tsConfigFiles) { + // external project already exists and not config files were added - update the project and return; + this.updateNonInferredProject(externalProject, proj.rootFiles, externalFilePropertyReader, proj.options, proj.typingOptions, proj.options.compileOnSave, /*configFileErrors*/ undefined); + return; + } + // some config files were added to external project (that previously were not there) + // close existing project and later we'll open a set of configured projects for these files + this.closeExternalProject(proj.projectFileName, /*suppressRefresh*/ true); + } + else if (this.externalProjectToConfiguredProjectMap[proj.projectFileName]) { + // this project used to include config files + if (!tsConfigFiles) { + // config files were removed from the project - close existing external project which in turn will close configured projects + this.closeExternalProject(proj.projectFileName, /*suppressRefresh*/ true); + } + else { + // project previously had some config files - compare them with new set of files and close all configured projects that correspond to unused files + const oldConfigFiles = this.externalProjectToConfiguredProjectMap[proj.projectFileName]; + let iNew = 0; + let iOld = 0; + while (iNew < tsConfigFiles.length && iOld < oldConfigFiles.length) { + const newConfig = tsConfigFiles[iNew]; + const oldConfig = oldConfigFiles[iOld]; + if (oldConfig < newConfig) { + this.closeConfiguredProject(oldConfig); + iOld++; + } + else if (oldConfig > newConfig) { + iNew++; + } + else { + // record existing config files so avoid extra add-refs + (exisingConfigFiles || (exisingConfigFiles = [])).push(oldConfig); + iOld++; + iNew++; + } + } + for (let i = iOld; i < oldConfigFiles.length; i++) { + // projects for all remaining old config files should be closed + this.closeConfiguredProject(oldConfigFiles[i]); + } + } + } if (tsConfigFiles) { // store the list of tsconfig files that belong to the external project this.externalProjectToConfiguredProjectMap[proj.projectFileName] = tsConfigFiles; @@ -1210,15 +1264,18 @@ namespace ts.server { // TODO: save errors project = result.success && result.project; } - if (project) { + if (project && !contains(exisingConfigFiles, tsconfigFile)) { // keep project alive even if no documents are opened - its lifetime is bound to the lifetime of containing external project project.addOpenRef(); } } } else { + // no config files - remove the item from the collection + delete this.externalProjectToConfiguredProjectMap[proj.projectFileName]; this.createAndAddExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typingOptions); } + this.refreshInferredProjects(); } } }