Skip to content

[WIP] Open bigger set of configured projects when opening composite project for operations that operate over multiple projects like rename #28261

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
142 changes: 109 additions & 33 deletions src/server/editorServices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,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 {
Expand All @@ -407,8 +407,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;
}
}

Expand Down Expand Up @@ -1058,6 +1058,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);
Expand Down Expand Up @@ -1496,32 +1500,38 @@ 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));
if (parentPath === searchPath) {
break;
}
searchPath = parentPath;
ignoreDirectory = false;
} while (anySearchPathOk || isSearchPathInProjectRoot());

return undefined;
Expand Down Expand Up @@ -1663,7 +1673,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),
Expand Down Expand Up @@ -2606,8 +2616,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;
Expand Down Expand Up @@ -2651,7 +2661,7 @@ namespace ts.server {
if (!project) {
project = this.createLoadAndUpdateConfiguredProject(configFileName, `Creating possible configured project for ${info.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;
}
Expand All @@ -2665,6 +2675,8 @@ namespace ts.server {
updateProjectIfDirty(project);
}
defaultConfigProject = project;
// Traverse till project Root and create those configured projects
this.createAncestorConfiguredProjects(info, project);
}
}

Expand All @@ -2687,6 +2699,63 @@ namespace ts.server {
return { configFileName, configFileErrors, defaultConfigProject };
}

/**
* 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;

const ancestor = this.findConfiguredProjectByProjectName(configFileName) ||
this.createConfiguredProjectWithDelayLoad(configFileName, `Project possibly referencing default composite project ${project.getProjectName()} of open file ${info.fileName}`);
if (ancestor.isInitialLoadPending()) {
ancestor.setPotentialProjectRefence(configPath);
}
else if (!project.getCompilerOptions().composite) {
return;
}

configInfo.fileName = configFileName;
configInfo.path = this.toPath(configFileName);
}
}

/*@internal*/
loadAncestorAndReferenceConfiguredProjects(forProjects: ReadonlyMap<Project>) {
// Load all the projects ancestor projects for seen projects
// 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
project.updateGraph();
// We want to also load the referenced projects
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 cleanupAfterOpeningFile(toRetainConfigProjects: ConfiguredProject[] | ConfiguredProject | undefined) {
// This was postponed from closeOpenFile to after opening next file,
// so that we can reuse the project if we need to right away
Expand Down Expand Up @@ -2718,6 +2787,17 @@ namespace ts.server {

private removeOrphanConfiguredProjects(toRetainConfiguredProjects: ConfiguredProject[] | ConfiguredProject | undefined) {
const toRemoveConfiguredProjects = cloneMap(this.configuredProjects);
const markOriginalProjectsAsUsed = (project: Project) => {
if (!project.isOrphan() && project.originalConfiguredProjects) {
project.originalConfiguredProjects.forEach(
(_value, configuredProjectPath) => retainConfiguredProject(
this.configuredProjects.get(configuredProjectPath)
)
);
}
};

// Retain pinned projects
if (toRetainConfiguredProjects) {
if (isArray(toRetainConfiguredProjects)) {
toRetainConfiguredProjects.forEach(retainConfiguredProject);
Expand All @@ -2734,32 +2814,28 @@ namespace ts.server {
// 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()) {
retainConfiguredProject(project);
markOriginalProjectsAsUsed(project);
}
else {
else if (toRemoveConfiguredProjects.has(project.canonicalConfigFilePath)) {
// 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()) {
retainConfiguredProject(project);
}
}
});
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()) {
retainConfiguredProject(project);
return true;
}
}

function retainConfiguredProject(project: ConfiguredProject) {
toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath);
function retainConfiguredProject(project: ConfiguredProject | undefined) {
if (project && toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath)) {
markOriginalProjectsAsUsed(project);
project.forEachReferencedConfiguredProject(retainConfiguredProject);
}
}
}

Expand Down
51 changes: 48 additions & 3 deletions src/server/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@ namespace ts.server {
return value instanceof ScriptInfo;
}

/* @internal */
export function isConfiguredProject(p: Project): p is ConfiguredProject {
return p.projectKind === ProjectKind.Configured;
}

interface GeneratedFileWatcher {
generatedFilePath: Path;
watcher: FileWatcher;
Expand Down Expand Up @@ -1123,6 +1128,7 @@ namespace ts.server {
return this.projectService.getScriptInfo(uncheckedFileName);
}

/* @internal */
filesToString(writeProjectFileNames: boolean) {
if (!this.program) {
return "\tFiles (0)\n";
Expand Down Expand Up @@ -1493,10 +1499,14 @@ namespace ts.server {

private projectReferences: ReadonlyArray<ProjectReference> | undefined;

/** Portentual project references before the project is actually loaded (read config file) */
private potentialProjectReferences: Map<true> | undefined;

/*@internal*/
projectOptions?: ProjectOptions | true;

protected isInitialLoadPending: () => boolean = returnTrue;
/*@internal*/
isInitialLoadPending: () => boolean = returnTrue;

/*@internal*/
sendLoadingProjectFinish = false;
Expand All @@ -1519,6 +1529,14 @@ namespace ts.server {
this.canonicalConfigFilePath = asNormalizedPath(projectService.toCanonicalFileName(configFileName));
}

/* @internal */
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.
Expand Down Expand Up @@ -1561,12 +1579,39 @@ namespace ts.server {

updateReferences(refs: ReadonlyArray<ProjectReference> | undefined) {
this.projectReferences = refs;
this.potentialProjectReferences = undefined;
}

/*@internal*/
forEachResolvedProjectReference<T>(cb: (resolvedProjectReference: ResolvedProjectReference | undefined, resolvedProjectReferencePath: Path) => T | undefined): T | undefined {
setPotentialProjectRefence(path: Path) {
// We know the composites if we have read the config file
Debug.assert(this.isInitialLoadPending());
(this.potentialProjectReferences || (this.potentialProjectReferences = createMap())).set(path, true);
}

/*@internal*/
forEachProjectReference<T>(
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*/
forEachReferencedConfiguredProject<T>(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*/
Expand Down
Loading