From 6fe1c7935ab7fc258b1bd5b1dab3d7d04cc0e66f Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Fri, 19 Oct 2018 10:13:32 -0700 Subject: [PATCH 1/7] Create ancestor projects when opening file --- src/server/editorServices.ts | 87 +++++++++++++++++++++++++++--------- src/server/project.ts | 35 +++++++++++++-- 2 files changed, 97 insertions(+), 25 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index ac80f43c84c26..bb5ecd79dfe1f 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -385,7 +385,7 @@ namespace ts.server { syntaxOnly?: boolean; } - interface OriginalFileInfo { fileName: NormalizedPath; path: Path; } + interface OriginalFileInfo { fileName: NormalizedPath; path: Path; openInfoPathForConfigFile?: Path; } type OpenScriptInfoOrClosedFileInfo = ScriptInfo | OriginalFileInfo; function isOpenScriptInfo(infoOrFileName: OpenScriptInfoOrClosedFileInfo): infoOrFileName is ScriptInfo { @@ -1009,6 +1009,10 @@ namespace ts.server { } else { this.logConfigFileWatchUpdate(project.getConfigFilePath(), project.canonicalConfigFilePath, configFileExistenceInfo, ConfigFileWatcherStatus.ReloadingInferredRootFiles); + if (project.isInitialLoadPending()) { + return; + } + project.pendingReload = ConfigFileProgramReloadLevel.Full; project.pendingReloadReason = "Change in config file detected"; this.delayUpdateProjectGraph(project); @@ -1416,25 +1420,30 @@ namespace ts.server { } Debug.assert(!isOpenScriptInfo(info) || this.openFiles.has(info.path)); - const projectRootPath = this.openFiles.get(info.path); + const openInfoPathForConfigFile = !isOpenScriptInfo(info) ? info.openInfoPathForConfigFile : undefined; + const projectRootPath = this.openFiles.get(openInfoPathForConfigFile || info.path); let searchPath = asNormalizedPath(getDirectoryPath(info.fileName)); const isSearchPathInProjectRoot = () => containsPath(projectRootPath!, searchPath, this.currentDirectory, !this.host.useCaseSensitiveFileNames); // If projectRootPath doesn't contain info.path, then do normal search for config file const anySearchPathOk = !projectRootPath || !isSearchPathInProjectRoot(); + // For config files always ignore its directory since that would just result to same config file + let ignoreDirectory = !!openInfoPathForConfigFile; do { - const canonicalSearchPath = normalizedPathToPath(searchPath, this.currentDirectory, this.toCanonicalFileName); - const tsconfigFileName = asNormalizedPath(combinePaths(searchPath, "tsconfig.json")); - let result = action(tsconfigFileName, combinePaths(canonicalSearchPath, "tsconfig.json")); - if (result) { - return tsconfigFileName; - } + if (!ignoreDirectory) { + const canonicalSearchPath = normalizedPathToPath(searchPath, this.currentDirectory, this.toCanonicalFileName); + const tsconfigFileName = asNormalizedPath(combinePaths(searchPath, "tsconfig.json")); + let result = action(tsconfigFileName, combinePaths(canonicalSearchPath, "tsconfig.json")); + if (result) { + return tsconfigFileName; + } - const jsconfigFileName = asNormalizedPath(combinePaths(searchPath, "jsconfig.json")); - result = action(jsconfigFileName, combinePaths(canonicalSearchPath, "jsconfig.json")); - if (result) { - return jsconfigFileName; + const jsconfigFileName = asNormalizedPath(combinePaths(searchPath, "jsconfig.json")); + result = action(jsconfigFileName, combinePaths(canonicalSearchPath, "jsconfig.json")); + if (result) { + return jsconfigFileName; + } } const parentPath = asNormalizedPath(getDirectoryPath(searchPath)); @@ -1442,6 +1451,7 @@ namespace ts.server { break; } searchPath = parentPath; + ignoreDirectory = false; } while (anySearchPathOk || isSearchPathInProjectRoot()); return undefined; @@ -2431,7 +2441,7 @@ namespace ts.server { if (!project) { project = this.createLoadAndUpdateConfiguredProject(configFileName, `Creating possible configured project for ${fileName} to open`); // Send the event only if the project got created as part of this open request and info is part of the project - if (info.isOrphan()) { + if (!project.containsScriptInfo(info)) { // Since the file isnt part of configured project, do not send config file info configFileName = undefined; } @@ -2444,6 +2454,8 @@ namespace ts.server { // Ensure project is ready to check if it contains opened script info updateProjectIfDirty(project); } + // Traverse till project Root and create those configured projects + this.createAncestorConfiguredProjects(info, project); } } @@ -2493,6 +2505,34 @@ namespace ts.server { return { configFileName, configFileErrors }; } + /** + * Traverse till project Root and create those configured projects + */ + private createAncestorConfiguredProjects(info: ScriptInfo, project: ConfiguredProject) { + if (!project.containsScriptInfo(info) || !project.getCompilerOptions().composite) return; + const configPath = this.toPath(project.canonicalConfigFilePath); + const configInfo: OriginalFileInfo = { + fileName: project.getConfigFilePath(), + path: configPath, + openInfoPathForConfigFile: info.path + }; + + // Go create all configured projects till project root + while (true) { + const configFileName = this.getConfigFileNameForFile(configInfo); + if (!configFileName) return; + + // TODO: may be we should create only first project and then once its loaded, + // do pending search if this is composite ? + const ancestor = this.findConfiguredProjectByProjectName(configFileName) || + this.createConfiguredProjectWithDelayLoad(configFileName, `Project possibly referencing default composite project ${project.getProjectName()} of open file ${info.fileName}`); + ancestor.setPotentialProjectRefence(configPath); + + configInfo.fileName = configFileName; + configInfo.path = this.toPath(configFileName); + } + } + private removeOrphanConfiguredProjects() { const toRemoveConfiguredProjects = cloneMap(this.configuredProjects); @@ -2506,15 +2546,11 @@ namespace ts.server { markOriginalProjectsAsUsed(project); } else { - // If the configured project for project reference has more than zero references, keep it alive - project.forEachResolvedProjectReference(ref => { - if (ref) { - const refProject = this.configuredProjects.get(ref.sourceFile.path); - if (refProject && refProject.hasOpenRef()) { - toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath); - } - } - }); + project.forEachResolvedProjectReference( + resolvedRef => markProjectAsUsedIfReferencedConfigWithOpenRef(project, resolvedRef && this.configuredProjects.get(resolvedRef.sourceFile.path)), + projectRef => markProjectAsUsedIfReferencedConfigWithOpenRef(project, this.configuredProjects.get(this.toPath(projectRef.path))), + potentialProjectRef => markProjectAsUsedIfReferencedConfigWithOpenRef(project, this.configuredProjects.get(potentialProjectRef)) + ); } }); @@ -2526,6 +2562,13 @@ namespace ts.server { project.originalConfiguredProjects.forEach((_value, configuredProjectPath) => toRemoveConfiguredProjects.delete(configuredProjectPath)); } } + + function markProjectAsUsedIfReferencedConfigWithOpenRef(project: ConfiguredProject, refProject: ConfiguredProject | undefined) { + if (refProject && refProject.hasOpenRef()) { + toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath); + return true; + } + } } private telemetryOnOpenFile(scriptInfo: ScriptInfo): void { diff --git a/src/server/project.ts b/src/server/project.ts index 7b11f48fbd623..4e30cafa8f520 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1351,10 +1351,14 @@ namespace ts.server { private projectReferences: ReadonlyArray | undefined; + /** Portentual project references before the project is actually loaded (read config file) */ + private potentialProjectReferences: Map | undefined; + /*@internal*/ projectOptions?: ProjectOptions | true; - protected isInitialLoadPending: () => boolean = returnTrue; + /*@internal*/ + isInitialLoadPending: () => boolean = returnTrue; /*@internal*/ sendLoadingProjectFinish = false; @@ -1377,6 +1381,13 @@ namespace ts.server { this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName)); } + filesToString(writeProjectFileNames: boolean) { + if (this.isInitialLoadPending()) { + return "\tFiles (0) InitialLoadPending\n"; + } + return super.filesToString(writeProjectFileNames); + } + /** * If the project has reload from disk pending, it reloads (and then updates graph as part of that) instead of just updating the graph * @returns: true if set of files in the project stays the same and false - otherwise. @@ -1420,12 +1431,30 @@ namespace ts.server { updateReferences(refs: ReadonlyArray | undefined) { this.projectReferences = refs; + this.potentialProjectReferences = undefined; + } + + setPotentialProjectRefence(path: Path) { + // We know the composites if we have read the config file + if (this.isInitialLoadPending()) { + (this.potentialProjectReferences || (this.potentialProjectReferences = createMap())).set(path, true); + } } /*@internal*/ - forEachResolvedProjectReference(cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined): T | undefined { + forEachResolvedProjectReference( + cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined, + cbProjectRef: (projectReference: ProjectReference) => T | undefined, + cbPotentialProjectRef: (path: Path) => T | undefined + ): T | undefined { const program = this.getCurrentProgram(); - return program && program.forEachResolvedProjectReference(cb); + if (program) { + return program.forEachResolvedProjectReference(cb); + } + if (this.isInitialLoadPending()) { + return this.potentialProjectReferences && forEachKey(this.potentialProjectReferences, cbPotentialProjectRef); + } + return forEach(this.projectReferences, cbProjectRef); } /*@internal*/ From 4b88a67f6de1aec15f6cc6f3308263e3c79333fb Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 22 Oct 2018 18:06:19 -0700 Subject: [PATCH 2/7] Create ancestor projects for composite projects till projectRoot but load them and their references when doing all project wide operation like rename --- src/server/editorServices.ts | 24 +++++++++++++++++++++++- src/server/project.ts | 2 +- src/server/session.ts | 22 ++++++++++++++++------ 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index bb5ecd79dfe1f..f6c6d2d78aae5 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -2533,6 +2533,28 @@ namespace ts.server { } } + /*@internal*/ + loadAncestorAndReferenceConfiguredProjects(forProjects: ReadonlyMap) { + // Load all the projects ancestor projects for seen projects + this.configuredProjects.forEach(project => { + if (project.isInitialLoadPending() && + project.forEachProjectReference(returnFalse, returnFalse, path => forProjects.has(path))) { + // Load the project + project.updateGraph(); + // We want to also load the referenced projects + // TODO:: Save them when project stays alive but at lower priority + project.forEachProjectReference(ref => { + if (ref) { + const configFileName = toNormalizedPath(ref.sourceFile.fileName); + const configuredProject = this.findConfiguredProjectByProjectName(configFileName) || + this.createAndLoadConfiguredProject(toNormalizedPath(configFileName), `Creating project for transitive reference of ancestor project: ${project.projectName}`); + updateProjectIfDirty(configuredProject); + } + }, noop, noop); + } + }); + } + private removeOrphanConfiguredProjects() { const toRemoveConfiguredProjects = cloneMap(this.configuredProjects); @@ -2546,7 +2568,7 @@ namespace ts.server { markOriginalProjectsAsUsed(project); } else { - project.forEachResolvedProjectReference( + project.forEachProjectReference( resolvedRef => markProjectAsUsedIfReferencedConfigWithOpenRef(project, resolvedRef && this.configuredProjects.get(resolvedRef.sourceFile.path)), projectRef => markProjectAsUsedIfReferencedConfigWithOpenRef(project, this.configuredProjects.get(this.toPath(projectRef.path))), potentialProjectRef => markProjectAsUsedIfReferencedConfigWithOpenRef(project, this.configuredProjects.get(potentialProjectRef)) diff --git a/src/server/project.ts b/src/server/project.ts index 4e30cafa8f520..24e788a4d2d56 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1442,7 +1442,7 @@ namespace ts.server { } /*@internal*/ - forEachResolvedProjectReference( + forEachProjectReference( cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined, cbProjectRef: (projectReference: ProjectReference) => T | undefined, cbPotentialProjectRef: (path: Path) => T | undefined diff --git a/src/server/session.ts b/src/server/session.ts index 7ff1364c4a095..9c011b9920c0a 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -412,7 +412,7 @@ namespace ts.server { ): void { const projectService = defaultProject.projectService; let toDo: ProjectAndLocation[] | undefined; - const seenProjects = createMap(); + const seenProjects = createMap(); forEachProjectInProjects(projects, initialLocation && initialLocation.fileName, (project, path) => { // TLocation shoud be either `DocumentPosition` or `undefined`. Since `initialLocation` is `TLocation` this cast should be valid. const location = (initialLocation ? { fileName: path, pos: initialLocation.pos } : undefined) as TLocation; @@ -421,9 +421,11 @@ namespace ts.server { // After initial references are collected, go over every other project and see if it has a reference for the symbol definition. if (getDefinition) { + projectService.loadAncestorAndReferenceConfiguredProjects(seenProjects); + const memGetDefinition = memoize(getDefinition); projectService.forEachEnabledProject(project => { - if (!addToSeen(seenProjects, project.projectName)) return; + if (!addToSeen(seenProjects, project)) return; const definition = getDefinitionInProject(memGetDefinition(), defaultProject, project); if (definition) { toDo = callbackProjectAndLocation({ project, location: definition as TLocation }, projectService, toDo, seenProjects, cb); @@ -442,16 +444,24 @@ namespace ts.server { return mappedDefinition && project.containsFile(toNormalizedPath(mappedDefinition.fileName)) ? mappedDefinition : undefined; } + function addToSeen(seenProjects: Map, project: Project) { + return ts.addToSeen(seenProjects, getProjectKey(project), project); + } + + function getProjectKey(project: Project) { + return project.projectKind === ProjectKind.Configured ? (project as ConfiguredProject).canonicalConfigFilePath : project.projectName; + } + function callbackProjectAndLocation( projectAndLocation: ProjectAndLocation, projectService: ProjectService, toDo: ProjectAndLocation[] | undefined, - seenProjects: Map, + seenProjects: Map, cb: CombineProjectOutputCallback, ): ProjectAndLocation[] | undefined { if (projectAndLocation.project.getCancellationToken().isCancellationRequested()) return undefined; // Skip rest of toDo if cancelled cb(projectAndLocation, (project, location) => { - seenProjects.set(projectAndLocation.project.projectName, true); + seenProjects.set(getProjectKey(projectAndLocation.project), projectAndLocation.project); const originalLocation = projectService.getOriginalLocationEnsuringConfiguredProject(project, location); if (!originalLocation) return undefined; @@ -472,8 +482,8 @@ namespace ts.server { return toDo; } - function addToTodo(projectAndLocation: ProjectAndLocation, toDo: Push>, seenProjects: Map): void { - if (addToSeen(seenProjects, projectAndLocation.project.projectName)) toDo.push(projectAndLocation); + function addToTodo(projectAndLocation: ProjectAndLocation, toDo: Push>, seenProjects: Map): void { + if (addToSeen(seenProjects, projectAndLocation.project)) toDo.push(projectAndLocation); } function documentSpanLocation({ fileName, textSpan }: DocumentSpan): DocumentPosition { From 88540378bcb7cfca087759c3268e27af495cb4d3 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 29 Oct 2018 16:13:28 -0700 Subject: [PATCH 3/7] When iterating to load configured project, create copy. --- src/server/editorServices.ts | 14 ++++++++------ src/server/project.ts | 4 ++++ src/server/session.ts | 5 ++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index f6c6d2d78aae5..58a29f134f788 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -410,8 +410,8 @@ namespace ts.server { } function setProjectOptionsUsed(project: ConfiguredProject | ExternalProject) { - if (project.projectKind === ProjectKind.Configured) { - (project as ConfiguredProject).projectOptions = true; + if (isConfiguredProject(project)) { + project.projectOptions = true; } } @@ -1606,7 +1606,7 @@ namespace ts.server { return; } - const projectOptions = project.projectKind === ProjectKind.Configured ? (project as ConfiguredProject).projectOptions as ProjectOptions : undefined; + const projectOptions = isConfiguredProject(project) ? project.projectOptions as ProjectOptions : undefined; setProjectOptionsUsed(project); const data: ProjectInfoTelemetryEventData = { projectId: this.host.createSHA256Hash(project.projectName), @@ -2399,8 +2399,8 @@ namespace ts.server { // Add configured projects as referenced originalScriptInfo.containingProjects.forEach(project => { - if (project.projectKind === ProjectKind.Configured) { - addOriginalConfiguredProject(project as ConfiguredProject); + if (isConfiguredProject(project)) { + addOriginalConfiguredProject(project); } }); return originalLocation; @@ -2536,7 +2536,9 @@ namespace ts.server { /*@internal*/ loadAncestorAndReferenceConfiguredProjects(forProjects: ReadonlyMap) { // Load all the projects ancestor projects for seen projects - this.configuredProjects.forEach(project => { + // Because the configured projects can update in the callback, get the copy to iterate + const currentConfigProjects = arrayFrom(this.configuredProjects.values()); + currentConfigProjects.forEach(project => { if (project.isInitialLoadPending() && project.forEachProjectReference(returnFalse, returnFalse, path => forProjects.has(path))) { // Load the project diff --git a/src/server/project.ts b/src/server/project.ts index 24e788a4d2d56..bf60acbebfa4d 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -92,6 +92,10 @@ namespace ts.server { return value instanceof ScriptInfo; } + export function isConfiguredProject(p: Project): p is ConfiguredProject { + return p.projectKind === ProjectKind.Configured; + } + export abstract class Project implements LanguageServiceHost, ModuleResolutionHost { private rootFiles: ScriptInfo[] = []; private rootFilesMap: Map = createMap(); diff --git a/src/server/session.ts b/src/server/session.ts index 9c011b9920c0a..27a2658a87f89 100644 --- a/src/server/session.ts +++ b/src/server/session.ts @@ -421,9 +421,8 @@ namespace ts.server { // After initial references are collected, go over every other project and see if it has a reference for the symbol definition. if (getDefinition) { - projectService.loadAncestorAndReferenceConfiguredProjects(seenProjects); - const memGetDefinition = memoize(getDefinition); + projectService.loadAncestorAndReferenceConfiguredProjects(seenProjects); projectService.forEachEnabledProject(project => { if (!addToSeen(seenProjects, project)) return; const definition = getDefinitionInProject(memGetDefinition(), defaultProject, project); @@ -449,7 +448,7 @@ namespace ts.server { } function getProjectKey(project: Project) { - return project.projectKind === ProjectKind.Configured ? (project as ConfiguredProject).canonicalConfigFilePath : project.projectName; + return isConfiguredProject(project) ? project.canonicalConfigFilePath : project.projectName; } function callbackProjectAndLocation( From e422dae2c747bcde7850ee237fb085e58de6d53f Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Tue, 30 Oct 2018 13:25:08 -0700 Subject: [PATCH 4/7] Create ancestor project if load is pending or if project is composite --- src/server/editorServices.ts | 14 ++++++++++---- src/server/project.ts | 5 ++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index 58a29f134f788..dd3af55dec2c2 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -2509,7 +2509,10 @@ namespace ts.server { * Traverse till project Root and create those configured projects */ private createAncestorConfiguredProjects(info: ScriptInfo, project: ConfiguredProject) { - if (!project.containsScriptInfo(info) || !project.getCompilerOptions().composite) return; + if (!project.containsScriptInfo(info) || !project.getCompilerOptions().composite) { + return; + } + const configPath = this.toPath(project.canonicalConfigFilePath); const configInfo: OriginalFileInfo = { fileName: project.getConfigFilePath(), @@ -2522,11 +2525,14 @@ namespace ts.server { const configFileName = this.getConfigFileNameForFile(configInfo); if (!configFileName) return; - // TODO: may be we should create only first project and then once its loaded, - // do pending search if this is composite ? const ancestor = this.findConfiguredProjectByProjectName(configFileName) || this.createConfiguredProjectWithDelayLoad(configFileName, `Project possibly referencing default composite project ${project.getProjectName()} of open file ${info.fileName}`); - ancestor.setPotentialProjectRefence(configPath); + if (ancestor.isInitialLoadPending()) { + ancestor.setPotentialProjectRefence(configPath); + } + else if (!project.getCompilerOptions().composite) { + return; + } configInfo.fileName = configFileName; configInfo.path = this.toPath(configFileName); diff --git a/src/server/project.ts b/src/server/project.ts index bf60acbebfa4d..d07be0116f373 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1440,9 +1440,8 @@ namespace ts.server { setPotentialProjectRefence(path: Path) { // We know the composites if we have read the config file - if (this.isInitialLoadPending()) { - (this.potentialProjectReferences || (this.potentialProjectReferences = createMap())).set(path, true); - } + Debug.assert(this.isInitialLoadPending()); + (this.potentialProjectReferences || (this.potentialProjectReferences = createMap())).set(path, true); } /*@internal*/ From 367112b6c6591626f03179de73079bdd540914e5 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 12 Nov 2018 11:02:07 -0800 Subject: [PATCH 5/7] Update Public API --- src/server/project.ts | 4 ++++ tests/baselines/reference/api/tsserverlibrary.d.ts | 8 ++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/src/server/project.ts b/src/server/project.ts index d07be0116f373..475868ec604c0 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -92,6 +92,7 @@ namespace ts.server { return value instanceof ScriptInfo; } + /* @internal */ export function isConfiguredProject(p: Project): p is ConfiguredProject { return p.projectKind === ProjectKind.Configured; } @@ -985,6 +986,7 @@ namespace ts.server { return this.projectService.getScriptInfo(uncheckedFileName); } + /* @internal */ filesToString(writeProjectFileNames: boolean) { if (!this.program) { return "\tFiles (0)\n"; @@ -1385,6 +1387,7 @@ namespace ts.server { this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName)); } + /* @internal */ filesToString(writeProjectFileNames: boolean) { if (this.isInitialLoadPending()) { return "\tFiles (0) InitialLoadPending\n"; @@ -1438,6 +1441,7 @@ namespace ts.server { this.potentialProjectReferences = undefined; } + /*@internal*/ setPotentialProjectRefence(path: Path) { // We know the composites if we have read the config file Debug.assert(this.isInitialLoadPending()); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index c024d92620747..615c9d089f576 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -8254,7 +8254,6 @@ declare namespace ts.server { private isWatchedMissingFile; getScriptInfoForNormalizedPath(fileName: NormalizedPath): ScriptInfo | undefined; getScriptInfo(uncheckedFileName: string): ScriptInfo | undefined; - filesToString(writeProjectFileNames: boolean): string; setCompilerOptions(compilerOptions: CompilerOptions): void; protected removeRoot(info: ScriptInfo): void; protected enableGlobalPlugins(options: CompilerOptions, pluginConfigOverrides: Map | undefined): void; @@ -8293,7 +8292,8 @@ declare namespace ts.server { private externalProjectRefCount; private projectErrors; private projectReferences; - protected isInitialLoadPending: () => boolean; + /** Portentual project references before the project is actually loaded (read config file) */ + private potentialProjectReferences; /** * If the project has reload from disk pending, it reloads (and then updates graph as part of that) instead of just updating the graph * @returns: true if set of files in the project stays the same and false - otherwise. @@ -8719,6 +8719,10 @@ declare namespace ts.server { openClientFile(fileName: string, fileContent?: string, scriptKind?: ScriptKind, projectRootPath?: string): OpenConfiguredProjectResult; private findExternalProjectContainingOpenScriptInfo; openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, projectRootPath?: NormalizedPath): OpenConfiguredProjectResult; + /** + * Traverse till project Root and create those configured projects + */ + private createAncestorConfiguredProjects; private removeOrphanConfiguredProjects; private telemetryOnOpenFile; /** From 170b69bb8fc02db175f658194f40f38720fc4d46 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 12 Nov 2018 12:01:03 -0800 Subject: [PATCH 6/7] Test case to verify that rename loads ancestor project and references causing all the locations to be renamed --- src/testRunner/unittests/tsserverProjectSystem.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index 085b5cf18af31..bffcc55515b09 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -10672,7 +10672,9 @@ declare class TestLib { const session = createSession(host); openFilesForSession([containerCompositeExec[1]], session); const service = session.getProjectService(); - checkNumberOfProjects(service, { configuredProjects: 1 }); + checkNumberOfProjects(service, { configuredProjects: 2 }); // compositeExec and solution + const solutionProject = service.configuredProjects.get(containerConfig.path)!; + assert.isTrue(solutionProject.isInitialLoadPending()); const locationOfMyConst = protocolLocationFromSubstring(containerCompositeExec[1].content, "myConst"); const response = session.executeCommandSeq({ command: protocol.CommandTypes.Rename, @@ -10682,13 +10684,16 @@ declare class TestLib { } }).response as protocol.RenameResponseBody; - const myConstLen = "myConst".length; const locationOfMyConstInLib = protocolLocationFromSubstring(containerLib[1].content, "myConst"); + const locationOfMyConstInExec = protocolLocationFromSubstring(containerExec[1].content, "myConst"); assert.deepEqual(response.locs, [ { file: containerCompositeExec[1].path, locs: [{ start: locationOfMyConst, end: { line: locationOfMyConst.line, offset: locationOfMyConst.offset + myConstLen } }] }, + { file: containerExec[1].path, locs: [{ start: locationOfMyConstInExec, end: { line: locationOfMyConstInExec.line, offset: locationOfMyConstInExec.offset + myConstLen } }] }, { file: containerLib[1].path, locs: [{ start: locationOfMyConstInLib, end: { line: locationOfMyConstInLib.line, offset: locationOfMyConstInLib.offset + myConstLen } }] } ]); + checkNumberOfProjects(service, { configuredProjects: 4 }); + assert.isFalse(solutionProject.isInitialLoadPending()); }); }); From d37e8222c7d2174372e115d30a9e3222fde09859 Mon Sep 17 00:00:00 2001 From: Sheetal Nandi Date: Mon, 12 Nov 2018 12:01:11 -0800 Subject: [PATCH 7/7] Keep whole set of original projects, referenced projects and projects with open ref alive --- src/server/editorServices.ts | 35 ++++++------ src/server/project.ts | 9 ++++ .../unittests/tsserverProjectSystem.ts | 54 +++++++++++++++++-- 3 files changed, 77 insertions(+), 21 deletions(-) diff --git a/src/server/editorServices.ts b/src/server/editorServices.ts index dd3af55dec2c2..b26e0246a6d7a 100644 --- a/src/server/editorServices.ts +++ b/src/server/editorServices.ts @@ -2550,7 +2550,6 @@ namespace ts.server { // Load the project project.updateGraph(); // We want to also load the referenced projects - // TODO:: Save them when project stays alive but at lower priority project.forEachProjectReference(ref => { if (ref) { const configFileName = toNormalizedPath(ref.sourceFile.fileName); @@ -2566,37 +2565,41 @@ namespace ts.server { private removeOrphanConfiguredProjects() { const toRemoveConfiguredProjects = cloneMap(this.configuredProjects); + const markOriginalProjectsAsUsed = (project: Project) => { + if (!project.isOrphan() && project.originalConfiguredProjects) { + project.originalConfiguredProjects.forEach((_value, configuredProjectPath) => markConfiguredProjectAsUsed(this.configuredProjects.get(configuredProjectPath))); + } + }; + // Do not remove configured projects that are used as original projects of other this.inferredProjects.forEach(markOriginalProjectsAsUsed); this.externalProjects.forEach(markOriginalProjectsAsUsed); this.configuredProjects.forEach(project => { // If project has open ref (there are more than zero references from external project/open file), keep it alive as well as any project it references if (project.hasOpenRef()) { - toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath); - markOriginalProjectsAsUsed(project); + markConfiguredProjectAsUsed(project); } - else { - project.forEachProjectReference( - resolvedRef => markProjectAsUsedIfReferencedConfigWithOpenRef(project, resolvedRef && this.configuredProjects.get(resolvedRef.sourceFile.path)), - projectRef => markProjectAsUsedIfReferencedConfigWithOpenRef(project, this.configuredProjects.get(this.toPath(projectRef.path))), - potentialProjectRef => markProjectAsUsedIfReferencedConfigWithOpenRef(project, this.configuredProjects.get(potentialProjectRef)) - ); + else if (toRemoveConfiguredProjects.has(project.canonicalConfigFilePath)) { + project.forEachReferencedConfiguredProject(markProjectAsUsedIfReferencedConfigWithOpenRef); } }); // Remove all the non marked projects toRemoveConfiguredProjects.forEach(project => this.removeProject(project)); - function markOriginalProjectsAsUsed(project: Project) { - if (!project.isOrphan() && project.originalConfiguredProjects) { - project.originalConfiguredProjects.forEach((_value, configuredProjectPath) => toRemoveConfiguredProjects.delete(configuredProjectPath)); + function markProjectAsUsedIfReferencedConfigWithOpenRef(refProject: ConfiguredProject | undefined, project: ConfiguredProject) { + if (refProject && refProject.hasOpenRef()) { + markConfiguredProjectAsUsed(project); + return true; } } - function markProjectAsUsedIfReferencedConfigWithOpenRef(project: ConfiguredProject, refProject: ConfiguredProject | undefined) { - if (refProject && refProject.hasOpenRef()) { - toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath); - return true; + function markConfiguredProjectAsUsed(project: ConfiguredProject | undefined) { + if (project) { + if (toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath)) { + markOriginalProjectsAsUsed(project); + project.forEachReferencedConfiguredProject(markConfiguredProjectAsUsed); + } } } } diff --git a/src/server/project.ts b/src/server/project.ts index 475868ec604c0..c243967e282c5 100644 --- a/src/server/project.ts +++ b/src/server/project.ts @@ -1464,6 +1464,15 @@ namespace ts.server { return forEach(this.projectReferences, cbProjectRef); } + /*@internal*/ + forEachReferencedConfiguredProject(cb: (refProject: ConfiguredProject | undefined, thisProject: ConfiguredProject) => T | undefined) { + return this.forEachProjectReference( + resolvedRef => cb(resolvedRef && this.projectService.configuredProjects.get(resolvedRef.sourceFile.path), this), + projectRef => cb(this.projectService.configuredProjects.get(this.toPath(projectRef.path)), this), + potentialProjectRef => cb(this.projectService.configuredProjects.get(potentialProjectRef), this) + ); + } + /*@internal*/ enablePluginsWithOptions(options: CompilerOptions, pluginConfigOverrides: Map | undefined) { const host = this.projectService.host; diff --git a/src/testRunner/unittests/tsserverProjectSystem.ts b/src/testRunner/unittests/tsserverProjectSystem.ts index bffcc55515b09..1264342269f31 100644 --- a/src/testRunner/unittests/tsserverProjectSystem.ts +++ b/src/testRunner/unittests/tsserverProjectSystem.ts @@ -408,7 +408,7 @@ namespace ts.projectSystem { } export function checkNumberOfConfiguredProjects(projectService: server.ProjectService, expected: number) { - assert.equal(projectService.configuredProjects.size, expected, `expected ${expected} configured project(s)`); + assert.equal(projectService.configuredProjects.size, expected, `expected ${expected} configured project(s): ${JSON.stringify(arrayFrom(projectService.configuredProjects.keys()))}`); } function checkNumberOfExternalProjects(projectService: server.ProjectService, expected: number) { @@ -10135,7 +10135,8 @@ declare class TestLib { openFilesForSession([userTs], session); const service = session.getProjectService(); - checkNumberOfProjects(service, addUserTsConfig ? { configuredProjects: 1 } : { inferredProjects: 1 }); + // If config file then userConfig project and bConfig project since it is referenced + checkNumberOfProjects(service, addUserTsConfig ? { configuredProjects: 2 } : { inferredProjects: 1 }); return session; } @@ -10215,7 +10216,7 @@ declare class TestLib { textSpan: protocolTextSpanFromSubstring(userTs.content, "fnA", { index: 1 }), definitions: [protocolFileSpanFromSubstring(aTs, "fnA")], }); - checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); verifyUserTsConfigProject(session); // Navigate to the definition @@ -10223,7 +10224,7 @@ declare class TestLib { openFilesForSession([aTs], session); // UserTs configured project should be alive - checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 3 }); verifyUserTsConfigProject(session); verifyATsConfigProject(session); @@ -10398,7 +10399,7 @@ declare class TestLib { const session = createSession(createServerHost([aTs, aTsconfig, bTs, bTsconfig, aDts, aDtsMap])); checkDeclarationFiles(aTs, session, [aDtsMap, aDts]); openFilesForSession([bTs], session); - checkNumberOfProjects(session.getProjectService(), { configuredProjects: 1 }); + checkNumberOfProjects(session.getProjectService(), { configuredProjects: 2 }); // configured project of b is alive since a references b const responseFull = executeSessionRequest(session, protocol.CommandTypes.ReferencesFull, protocolFileLocationFromSubstring(bTs, "f()")); @@ -10695,6 +10696,49 @@ declare class TestLib { checkNumberOfProjects(service, { configuredProjects: 4 }); assert.isFalse(solutionProject.isInitialLoadPending()); }); + + it("ancestor and project ref management", () => { + const tempFile: File = { + path: `/user/username/projects/temp/temp.ts`, + content: "let x = 10" + }; + const host = createHost(files.concat([tempFile]), [containerConfig.path]); + const session = createSession(host); + openFilesForSession([containerCompositeExec[1]], session); + const service = session.getProjectService(); + checkNumberOfProjects(service, { configuredProjects: 2 }); // compositeExec and solution + const solutionProject = service.configuredProjects.get(containerConfig.path)!; + assert.isTrue(solutionProject.isInitialLoadPending()); + + // Open temp file and verify all projects alive + openFilesForSession([tempFile], session); + checkNumberOfProjects(service, { configuredProjects: 2, inferredProjects: 1 }); + assert.isTrue(solutionProject.isInitialLoadPending()); + + const locationOfMyConst = protocolLocationFromSubstring(containerCompositeExec[1].content, "myConst"); + session.executeCommandSeq({ + command: protocol.CommandTypes.Rename, + arguments: { + file: containerCompositeExec[1].path, + ...locationOfMyConst + } + }); + + // Ref projects are loaded + checkNumberOfProjects(service, { configuredProjects: 4, inferredProjects: 1 }); + assert.isFalse(solutionProject.isInitialLoadPending()); + + // Open temp file and verify all projects alive + service.closeClientFile(tempFile.path); + openFilesForSession([tempFile], session); + checkNumberOfProjects(service, { configuredProjects: 4, inferredProjects: 1 }); + + // Close all files and open temp file, only inferred project should be alive + service.closeClientFile(containerCompositeExec[1].path); + service.closeClientFile(tempFile.path); + openFilesForSession([tempFile], session); + checkNumberOfProjects(service, { inferredProjects: 1 }); + }); }); it("can go to definition correctly", () => {