Skip to content

Commit e3de872

Browse files
committed
Open a tree of projects when doing findAllRefs or rename operations
1 parent ea2bb85 commit e3de872

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 {
@@ -1261,7 +1321,7 @@ namespace ts.server {
12611321
}
12621322
}
12631323

1264-
private configFileExists(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: OpenScriptInfoOrClosedFileInfo) {
1324+
private configFileExists(configFileName: NormalizedPath, canonicalConfigFilePath: string, info: OpenScriptInfoOrClosedOrConfigFileInfo) {
12651325
let configFileExistenceInfo = this.configFileExistenceInfoCache.get(canonicalConfigFilePath);
12661326
if (configFileExistenceInfo) {
12671327
// By default the info would get impacted by presence of config file since its in the detection path
@@ -1492,7 +1552,7 @@ namespace ts.server {
14921552
* The server must start searching from the directory containing
14931553
* the newly opened file.
14941554
*/
1495-
private forEachConfigFileLocation(info: OpenScriptInfoOrClosedFileInfo, action: (configFileName: NormalizedPath, canonicalConfigFilePath: string) => boolean | void) {
1555+
private forEachConfigFileLocation(info: OpenScriptInfoOrClosedOrConfigFileInfo, action: (configFileName: NormalizedPath, canonicalConfigFilePath: string) => boolean | void) {
14961556
if (this.syntaxOnly) {
14971557
return undefined;
14981558
}
@@ -1505,25 +1565,24 @@ namespace ts.server {
15051565

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

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

15221582
const parentPath = asNormalizedPath(getDirectoryPath(searchPath));
1523-
if (parentPath === searchPath) {
1524-
break;
1525-
}
1583+
if (parentPath === searchPath) break;
15261584
searchPath = parentPath;
1585+
searchInDirectory = true;
15271586
} while (anySearchPathOk || isSearchPathInProjectRoot());
15281587

15291588
return undefined;
@@ -1539,7 +1598,7 @@ namespace ts.server {
15391598
* If script info is passed in, it is asserted to be open script info
15401599
* otherwise just file name
15411600
*/
1542-
private getConfigFileNameForFile(info: OpenScriptInfoOrClosedFileInfo) {
1601+
private getConfigFileNameForFile(info: OpenScriptInfoOrClosedOrConfigFileInfo) {
15431602
if (isOpenScriptInfo(info)) Debug.assert(info.isScriptOpen());
15441603
this.logger.info(`Search path: ${getDirectoryPath(info.fileName)}`);
15451604
const configFileName = this.forEachConfigFileLocation(info, (configFileName, canonicalConfigFilePath) =>
@@ -2062,7 +2121,7 @@ namespace ts.server {
20622121
if (project.languageServiceEnabled &&
20632122
!project.isOrphan() &&
20642123
!project.getCompilerOptions().preserveSymlinks &&
2065-
!contains(info.containingProjects, project)) {
2124+
!info.isAttached(project)) {
20662125
if (!projects) {
20672126
projects = createMultiMap();
20682127
projects.add(toAddInfo.path, project);
@@ -2662,7 +2721,7 @@ namespace ts.server {
26622721
if (!project) {
26632722
project = this.createLoadAndUpdateConfiguredProject(configFileName, `Creating possible configured project for ${info.fileName} to open`);
26642723
// Send the event only if the project got created as part of this open request and info is part of the project
2665-
if (info.isOrphan()) {
2724+
if (!project.containsScriptInfo(info)) {
26662725
// Since the file isnt part of configured project, do not send config file info
26672726
configFileName = undefined;
26682727
}
@@ -2676,6 +2735,8 @@ namespace ts.server {
26762735
updateProjectIfDirty(project);
26772736
}
26782737
defaultConfigProject = project;
2738+
// Create ancestor configured project
2739+
this.createAncestorProjects(info, defaultConfigProject);
26792740
}
26802741
}
26812742

@@ -2698,6 +2759,74 @@ namespace ts.server {
26982759
return { configFileName, configFileErrors, defaultConfigProject };
26992760
}
27002761

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

27302859
private removeOrphanConfiguredProjects(toRetainConfiguredProjects: ConfiguredProject[] | ConfiguredProject | undefined) {
27312860
const toRemoveConfiguredProjects = cloneMap(this.configuredProjects);
2861+
const markOriginalProjectsAsUsed = (project: Project) => {
2862+
if (!project.isOrphan() && project.originalConfiguredProjects) {
2863+
project.originalConfiguredProjects.forEach(
2864+
(_value, configuredProjectPath) => {
2865+
const project = this.getConfiguredProjectByCanonicalConfigFilePath(configuredProjectPath);
2866+
return project && retainConfiguredProject(project);
2867+
}
2868+
);
2869+
}
2870+
};
27322871
if (toRetainConfiguredProjects) {
27332872
if (isArray(toRetainConfiguredProjects)) {
27342873
toRetainConfiguredProjects.forEach(retainConfiguredProject);
@@ -2745,32 +2884,30 @@ namespace ts.server {
27452884
// 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
27462885
if (project.hasOpenRef()) {
27472886
retainConfiguredProject(project);
2748-
markOriginalProjectsAsUsed(project);
27492887
}
2750-
else {
2888+
else if (toRemoveConfiguredProjects.has(project.canonicalConfigFilePath)) {
27512889
// If the configured project for project reference has more than zero references, keep it alive
2752-
project.forEachResolvedProjectReference(ref => {
2753-
if (ref) {
2754-
const refProject = this.configuredProjects.get(ref.sourceFile.path);
2755-
if (refProject && refProject.hasOpenRef()) {
2756-
retainConfiguredProject(project);
2757-
}
2758-
}
2759-
});
2890+
forEachReferencedProject(
2891+
project,
2892+
ref => isRetained(ref) && retainConfiguredProject(project)
2893+
);
27602894
}
27612895
});
27622896

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

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

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

src/server/project.ts

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

15131513
private projectReferences: ReadonlyArray<ProjectReference> | undefined;
15141514

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

@@ -1630,12 +1634,13 @@ namespace ts.server {
16301634

16311635
updateReferences(refs: ReadonlyArray<ProjectReference> | undefined) {
16321636
this.projectReferences = refs;
1637+
this.potentialProjectReferences = undefined;
16331638
}
16341639

16351640
/*@internal*/
1636-
forEachResolvedProjectReference<T>(cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined): T | undefined {
1637-
const program = this.getCurrentProgram();
1638-
return program && program.forEachResolvedProjectReference(cb);
1641+
setPotentialProjectReference(canonicalConfigPath: NormalizedPath) {
1642+
Debug.assert(this.isInitialLoadPending());
1643+
(this.potentialProjectReferences || (this.potentialProjectReferences = createMap())).set(canonicalConfigPath, true);
16391644
}
16401645

16411646
/*@internal*/

0 commit comments

Comments
 (0)