Skip to content

Commit 9b421e8

Browse files
committed
Open a tree of projects when doing findAllRefs or rename operations
1 parent 6db21c2 commit 9b421e8

File tree

7 files changed

+265
-46
lines changed

7 files changed

+265
-46
lines changed

src/compiler/core.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,17 @@ namespace ts {
758758
};
759759
}
760760

761+
export function mapDefinedMap<T, U>(map: ReadonlyMap<T>, mapValue: (value: T, key: string) => U | undefined, mapKey: (key: string) => string = identity): Map<U> {
762+
const result = createMap<U>();
763+
map.forEach((value, key) => {
764+
const mapped = mapValue(value, key);
765+
if (mapped !== undefined) {
766+
result.set(mapKey(key), mapped);
767+
}
768+
});
769+
return result;
770+
}
771+
761772
export const emptyIterator: Iterator<never> = { next: () => ({ value: undefined as never, done: true }) };
762773

763774
export function singleIterator<T>(value: T): Iterator<T> {

src/server/editorServices.ts

Lines changed: 172 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -383,10 +383,70 @@ namespace ts.server {
383383
}
384384

385385
interface OriginalFileInfo { fileName: NormalizedPath; path: Path; }
386+
interface AncestorConfigFileInfo {
387+
/** config file name */ fileName: string;
388+
/** path of open file so we can look at correct root */path: Path;
389+
configFileInfo: true;
390+
}
386391
type OpenScriptInfoOrClosedFileInfo = ScriptInfo | OriginalFileInfo;
392+
type OpenScriptInfoOrClosedOrConfigFileInfo = OpenScriptInfoOrClosedFileInfo | AncestorConfigFileInfo;
393+
394+
function isOpenScriptInfo(infoOrFileNameOrConfig: OpenScriptInfoOrClosedOrConfigFileInfo): infoOrFileNameOrConfig is ScriptInfo {
395+
return !!(infoOrFileNameOrConfig as ScriptInfo).containingProjects;
396+
}
397+
398+
function isAncestorConfigFileInfo(infoOrFileNameOrConfig: OpenScriptInfoOrClosedOrConfigFileInfo): infoOrFileNameOrConfig is AncestorConfigFileInfo {
399+
return !!(infoOrFileNameOrConfig as AncestorConfigFileInfo).configFileInfo;
400+
}
401+
402+
function forEachResolvedProjectReference<T>(
403+
project: ConfiguredProject,
404+
cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined
405+
): T | undefined {
406+
const program = project.getCurrentProgram();
407+
return program && program.forEachResolvedProjectReference(cb);
408+
}
409+
410+
function forEachPotentialProjectReference<T>(
411+
project: ConfiguredProject,
412+
cb: (potentialProjectReference: Path) => T | undefined
413+
): T | undefined {
414+
return project.potentialProjectReferences &&
415+
forEachKey(project.potentialProjectReferences, cb);
416+
}
417+
418+
function forEachAnyProjectReferenceKind<T>(
419+
project: ConfiguredProject,
420+
cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined,
421+
cbProjectRef: (projectReference: ProjectReference) => T | undefined,
422+
cbPotentialProjectRef: (potentialProjectReference: Path) => T | undefined
423+
): T | undefined {
424+
return project.getCurrentProgram() ?
425+
forEachResolvedProjectReference(project, cb) :
426+
project.isInitialLoadPending() ?
427+
forEachPotentialProjectReference(project, cbPotentialProjectRef) :
428+
forEach(project.getProjectReferences(), cbProjectRef);
429+
}
430+
431+
function callbackRefProject<T>(
432+
project: ConfiguredProject,
433+
cb: (refProj: ConfiguredProject) => T | undefined,
434+
refPath: Path | undefined
435+
) {
436+
const refProject = refPath && project.projectService.configuredProjects.get(refPath);
437+
return refProject && cb(refProject);
438+
}
387439

388-
function isOpenScriptInfo(infoOrFileName: OpenScriptInfoOrClosedFileInfo): infoOrFileName is ScriptInfo {
389-
return !!(infoOrFileName as ScriptInfo).containingProjects;
440+
function forEachReferencedProject<T>(
441+
project: ConfiguredProject,
442+
cb: (refProj: ConfiguredProject) => T | undefined
443+
): T | undefined {
444+
return forEachAnyProjectReferenceKind(
445+
project,
446+
resolvedRef => callbackRefProject(project, cb, resolvedRef && resolvedRef.sourceFile.path),
447+
projectRef => callbackRefProject(project, cb, project.toPath(projectRef.path)),
448+
potentialProjectRef => callbackRefProject(project, cb, potentialProjectRef)
449+
);
390450
}
391451

392452
interface ScriptInfoInNodeModulesWatcher extends FileWatcher {
@@ -1260,7 +1320,7 @@ namespace ts.server {
12601320
}
12611321
}
12621322

1263-
private configFileExists(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: OpenScriptInfoOrClosedFileInfo) {
1323+
private configFileExists(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: OpenScriptInfoOrClosedOrConfigFileInfo) {
12641324
let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
12651325
if (configFileExistenceInfo) {
12661326
// By default the info would get impacted by presence of config file since its in the detection path
@@ -1491,7 +1551,7 @@ namespace ts.server {
14911551
* The server must start searching from the directory containing
14921552
* the newly opened file.
14931553
*/
1494-
private forEachConfigFileLocation(info: OpenScriptInfoOrClosedFileInfo, action: (configFileName: NormalizedPath, canonicalConfigFilePath: string) => boolean | void) {
1554+
private forEachConfigFileLocation(info: OpenScriptInfoOrClosedOrConfigFileInfo, action: (configFileName: NormalizedPath, canonicalConfigFilePath: string) => boolean | void) {
14951555
if (this.syntaxOnly) {
14961556
return undefined;
14971557
}
@@ -1504,25 +1564,24 @@ namespace ts.server {
15041564

15051565
// If projectRootPath doesn't contain info.path, then do normal search for config file
15061566
const anySearchPathOk = !projectRootPath || !isSearchPathInProjectRoot();
1567+
// For ancestor of config file always ignore its own directory since its going to result in itself
1568+
let searchInDirectory = !isAncestorConfigFileInfo(info);
15071569
do {
1508-
const canonicalSearchPath = normalizedPathToPath(searchPath, this.currentDirectory, this.toCanonicalFileName);
1509-
const tsconfigFileName = asNormalizedPath(combinePaths(searchPath, "tsconfig.json"));
1510-
let result = action(tsconfigFileName, combinePaths(canonicalSearchPath, "tsconfig.json"));
1511-
if (result) {
1512-
return tsconfigFileName;
1513-
}
1570+
if (searchInDirectory) {
1571+
const canonicalSearchPath = normalizedPathToPath(searchPath, this.currentDirectory, this.toCanonicalFileName);
1572+
const tsconfigFileName = asNormalizedPath(combinePaths(searchPath, "tsconfig.json"));
1573+
let result = action(tsconfigFileName, combinePaths(canonicalSearchPath, "tsconfig.json"));
1574+
if (result) return tsconfigFileName;
15141575

1515-
const jsconfigFileName = asNormalizedPath(combinePaths(searchPath, "jsconfig.json"));
1516-
result = action(jsconfigFileName, combinePaths(canonicalSearchPath, "jsconfig.json"));
1517-
if (result) {
1518-
return jsconfigFileName;
1576+
const jsconfigFileName = asNormalizedPath(combinePaths(searchPath, "jsconfig.json"));
1577+
result = action(jsconfigFileName, combinePaths(canonicalSearchPath, "jsconfig.json"));
1578+
if (result) return jsconfigFileName;
15191579
}
15201580

15211581
const parentPath = asNormalizedPath(getDirectoryPath(searchPath));
1522-
if (parentPath === searchPath) {
1523-
break;
1524-
}
1582+
if (parentPath === searchPath) break;
15251583
searchPath = parentPath;
1584+
searchInDirectory = true;
15261585
} while (anySearchPathOk || isSearchPathInProjectRoot());
15271586

15281587
return undefined;
@@ -1538,7 +1597,7 @@ namespace ts.server {
15381597
* If script info is passed in, it is asserted to be open script info
15391598
* otherwise just file name
15401599
*/
1541-
private getConfigFileNameForFile(info: OpenScriptInfoOrClosedFileInfo) {
1600+
private getConfigFileNameForFile(info: OpenScriptInfoOrClosedOrConfigFileInfo) {
15421601
if (isOpenScriptInfo(info)) Debug.assert(info.isScriptOpen());
15431602
this.logger.info(`Search path: ${getDirectoryPath(info.fileName)}`);
15441603
const configFileName = this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) =>
@@ -2061,7 +2120,7 @@ namespace ts.server {
20612120
if (project.languageServiceEnabled &&
20622121
!project.isOrphan() &&
20632122
!project.getCompilerOptions().preserveSymlinks &&
2064-
!contains(info.containingProjects, project)) {
2123+
!info.isAttached(project)) {
20652124
if (!projects) {
20662125
projects = createMultiMap();
20672126
projects.add(toAddInfo.path, project);
@@ -2661,7 +2720,7 @@ namespace ts.server {
26612720
if (!project) {
26622721
project = this.createLoadAndUpdateConfiguredProject(configFileName, `Creating possible configured project for ${info.fileName} to open`);
26632722
// Send the event only if the project got created as part of this open request and info is part of the project
2664-
if (info.isOrphan()) {
2723+
if (!project.containsScriptInfo(info)) {
26652724
// Since the file isnt part of configured project, do not send config file info
26662725
configFileName = undefined;
26672726
}
@@ -2675,6 +2734,8 @@ namespace ts.server {
26752734
updateProjectIfDirty(project);
26762735
}
26772736
defaultConfigProject = project;
2737+
// Create ancestor configured project
2738+
this.createAncestorProjects(info, defaultConfigProject);
26782739
}
26792740
}
26802741

@@ -2697,6 +2758,74 @@ namespace ts.server {
26972758
return { configFileName, configFileErrors, defaultConfigProject };
26982759
}
26992760

2761+
private createAncestorProjects(info: ScriptInfo, project: ConfiguredProject) {
2762+
// Skip if info is not part of default configured project
2763+
if (!info.isAttached(project)) return;
2764+
2765+
// Create configured project till project root
2766+
while (true) {
2767+
// Skip if project is not composite
2768+
if (!project.isInitialLoadPending() && !project.getCompilerOptions().composite) return;
2769+
2770+
// Get config file name
2771+
const configFileName = this.getConfigFileNameForFile({
2772+
fileName: project.getConfigFilePath(),
2773+
path: info.path,
2774+
configFileInfo: true
2775+
});
2776+
if (!configFileName) return;
2777+
2778+
// find or delay load the project
2779+
const ancestor = this.findConfiguredProjectByProjectName(configFileName) ||
2780+
this.createConfiguredProjectWithDelayLoad(configFileName, `Creating project possibly referencing default composite project ${project.getProjectName()} of open file ${info.fileName}`);
2781+
if (ancestor.isInitialLoadPending()) {
2782+
// Set a potential project reference
2783+
ancestor.setPotentialProjectReference(project.canonicalConfigFilePath);
2784+
}
2785+
project = ancestor;
2786+
}
2787+
}
2788+
2789+
/*@internal*/
2790+
loadAncestorProjectTree(forProjects?: ReadonlyMap<true>) {
2791+
forProjects = forProjects || mapDefinedMap(
2792+
this.configuredProjects,
2793+
project => !project.isInitialLoadPending() || undefined
2794+
);
2795+
2796+
const seenProjects = createMap<true>();
2797+
// Work on array copy as we could add more projects as part of callback
2798+
for (const project of arrayFrom(this.configuredProjects.values())) {
2799+
// If this project has potential project reference for any of the project we are loading ancestor tree for
2800+
// we need to load this project tree
2801+
if (forEachPotentialProjectReference(
2802+
project,
2803+
potentialRefPath => forProjects!.has(potentialRefPath)
2804+
)) {
2805+
// Load children
2806+
this.ensureProjectChildren(project, seenProjects);
2807+
}
2808+
}
2809+
}
2810+
2811+
private ensureProjectChildren(project: ConfiguredProject, seenProjects: Map<true>) {
2812+
if (!addToSeen(seenProjects, project.canonicalConfigFilePath)) return;
2813+
// Update the project
2814+
updateProjectIfDirty(project);
2815+
2816+
// Create tree because project is uptodate we only care of resolved references
2817+
forEachResolvedProjectReference(
2818+
project,
2819+
ref => {
2820+
if (!ref) return;
2821+
const configFileName = toNormalizedPath(ref.sourceFile.fileName);
2822+
const child = this.findConfiguredProjectByProjectName(configFileName) ||
2823+
this.createAndLoadConfiguredProject(configFileName, `Creating project for reference of project: ${project.projectName}`);
2824+
this.ensureProjectChildren(child, seenProjects);
2825+
}
2826+
);
2827+
}
2828+
27002829
private cleanupAfterOpeningFile(toRetainConfigProjects: ConfiguredProject[] | ConfiguredProject | undefined) {
27012830
// This was postponed from closeOpenFile to after opening next file,
27022831
// so that we can reuse the project if we need to right away
@@ -2728,6 +2857,16 @@ namespace ts.server {
27282857

27292858
private removeOrphanConfiguredProjects(toRetainConfiguredProjects: ConfiguredProject[] | ConfiguredProject | undefined) {
27302859
const toRemoveConfiguredProjects = cloneMap(this.configuredProjects);
2860+
const markOriginalProjectsAsUsed = (project: Project) => {
2861+
if (!project.isOrphan() && project.originalConfiguredProjects) {
2862+
project.originalConfiguredProjects.forEach(
2863+
(_value, configuredProjectPath) => {
2864+
const project = this.getConfiguredProjectByCanonicalConfigFilePath(configuredProjectPath);
2865+
return project && retainConfiguredProject(project);
2866+
}
2867+
);
2868+
}
2869+
};
27312870
if (toRetainConfiguredProjects) {
27322871
if (isArray(toRetainConfiguredProjects)) {
27332872
toRetainConfiguredProjects.forEach(retainConfiguredProject);
@@ -2744,32 +2883,30 @@ namespace ts.server {
27442883
// 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
27452884
if (project.hasOpenRef()) {
27462885
retainConfiguredProject(project);
2747-
markOriginalProjectsAsUsed(project);
27482886
}
2749-
else {
2887+
else if (toRemoveConfiguredProjects.has(project.canonicalConfigFilePath)) {
27502888
// If the configured project for project reference has more than zero references, keep it alive
2751-
project.forEachResolvedProjectReference(ref => {
2752-
if (ref) {
2753-
const refProject = this.configuredProjects.get(ref.sourceFile.path);
2754-
if (refProject && refProject.hasOpenRef()) {
2755-
retainConfiguredProject(project);
2756-
}
2757-
}
2758-
});
2889+
forEachReferencedProject(
2890+
project,
2891+
ref => isRetained(ref) && retainConfiguredProject(project)
2892+
);
27592893
}
27602894
});
27612895

27622896
// Remove all the non marked projects
27632897
toRemoveConfiguredProjects.forEach(project => this.removeProject(project));
27642898

2765-
function markOriginalProjectsAsUsed(project: Project) {
2766-
if (!project.isOrphan() && project.originalConfiguredProjects) {
2767-
project.originalConfiguredProjects.forEach((_value, configuredProjectPath) => toRemoveConfiguredProjects.delete(configuredProjectPath));
2768-
}
2899+
function isRetained(project: ConfiguredProject) {
2900+
return project.hasOpenRef() || !toRemoveConfiguredProjects.has(project.canonicalConfigFilePath);
27692901
}
27702902

27712903
function retainConfiguredProject(project: ConfiguredProject) {
2772-
toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath);
2904+
if (toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath)) {
2905+
// Keep original projects used
2906+
markOriginalProjectsAsUsed(project);
2907+
// Keep all the references alive
2908+
forEachReferencedProject(project, retainConfiguredProject);
2909+
}
27732910
}
27742911
}
27752912

src/server/project.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1513,6 +1513,10 @@ namespace ts.server {
15131513

15141514
private projectReferences: readonly ProjectReference[] | undefined;
15151515

1516+
/** Potential project references before the project is actually loaded (read config file) */
1517+
/*@internal*/
1518+
potentialProjectReferences: Map<true> | undefined;
1519+
15161520
/*@internal*/
15171521
projectOptions?: ProjectOptions | true;
15181522

@@ -1641,12 +1645,13 @@ namespace ts.server {
16411645

16421646
updateReferences(refs: readonly ProjectReference[] | undefined) {
16431647
this.projectReferences = refs;
1648+
this.potentialProjectReferences = undefined;
16441649
}
16451650

16461651
/*@internal*/
1647-
forEachResolvedProjectReference<T>(cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined): T | undefined {
1648-
const program = this.getCurrentProgram();
1649-
return program && program.forEachResolvedProjectReference(cb);
1652+
setPotentialProjectReference(canonicalConfigPath: NormalizedPath) {
1653+
Debug.assert(this.isInitialLoadPending());
1654+
(this.potentialProjectReferences || (this.potentialProjectReferences = createMap())).set(canonicalConfigPath, true);
16501655
}
16511656

16521657
/*@internal*/

0 commit comments

Comments
 (0)