Skip to content

Handle config file change and default project management #58486

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

Merged
merged 2 commits into from
May 10, 2024
Merged
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
6 changes: 5 additions & 1 deletion src/harness/projectServiceStateLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,11 @@ export function patchServiceForStateBaseline(service: ProjectService) {
function baselineProjects(currentMappers: Set<DocumentPositionMapper>) {
const autoImportProviderProjects = [] as AutoImportProviderProject[];
const auxiliaryProjects = [] as AuxiliaryProject[];
const orphanConfiguredProjects = service.getOrphanConfiguredProjects(/*toRetainConfiguredProjects*/ undefined);
const orphanConfiguredProjects = service.getOrphanConfiguredProjects(
/*toRetainConfiguredProjects*/ undefined,
/*openFilesWithRetainedConfiguredProject*/ undefined,
/*externalProjectsRetainingConfiguredProjects*/ undefined,
);
const noOpenRef = (project: Project) => isConfiguredProject(project) && (project.isClosed() || orphanConfiguredProjects.has(project));
return baselineState(
[service.externalProjects, service.configuredProjects, service.inferredProjects, autoImportProviderProjects, auxiliaryProjects],
Expand Down
1,226 changes: 744 additions & 482 deletions src/server/editorServices.ts

Large diffs are not rendered by default.

89 changes: 4 additions & 85 deletions src/server/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,15 +141,12 @@ import {
emptyArray,
Errors,
FileStats,
forEachResolvedProjectReferenceProject,
LogLevel,
ModuleImportResult,
Msg,
NormalizedPath,
PackageJsonWatcher,
projectContainsInfoDirectly,
ProjectOptions,
ProjectReferenceProjectLoadKind,
ProjectService,
ScriptInfo,
ServerHost,
Expand Down Expand Up @@ -2211,7 +2208,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
private isDefaultProjectForOpenFiles(): boolean {
return !!forEachEntry(
this.projectService.openFiles,
(_, fileName) => this.projectService.tryGetDefaultProjectForFile(toNormalizedPath(fileName)) === this,
(_projectRootPath, path) => this.projectService.tryGetDefaultProjectForFile(this.projectService.getScriptInfoForPath(path)!) === this,
);
}

Expand Down Expand Up @@ -2388,7 +2385,7 @@ export class InferredProject extends Project {
}

override removeRoot(info: ScriptInfo) {
this.projectService.stopWatchingConfigFilesForInferredProjectRoot(info);
this.projectService.stopWatchingConfigFilesForScriptInfo(info);
super.removeRoot(info);
// Delay toggling to isJsInferredProject = false till we actually need it again
if (!this.isOrphan() && this._isJsInferredProject && info.isJavaScript()) {
Expand All @@ -2412,7 +2409,7 @@ export class InferredProject extends Project {
}

override close() {
forEach(this.getRootScriptInfos(), info => this.projectService.stopWatchingConfigFilesForInferredProjectRoot(info));
forEach(this.getRootScriptInfos(), info => this.projectService.stopWatchingConfigFilesForScriptInfo(info));
super.close();
}

Expand Down Expand Up @@ -2763,9 +2760,6 @@ export class ConfiguredProject extends Project {
/** @internal */
canConfigFileJsonReportNoInputFiles = false;

/** Ref count to the project when opened from external project */
private externalProjectRefCount = 0;

private projectReferences: readonly ProjectReference[] | undefined;

/**
Expand Down Expand Up @@ -2873,8 +2867,7 @@ export class ConfiguredProject extends Project {
case ProgramUpdateLevel.Full:
this.openFileWatchTriggered.clear();
const reason = Debug.checkDefined(this.pendingUpdateReason);
this.pendingUpdateReason = undefined;
this.projectService.reloadConfiguredProject(this, reason, /*clearSemanticCache*/ false);
this.projectService.reloadConfiguredProject(this, reason);
result = true;
break;
default:
Expand Down Expand Up @@ -2998,91 +2991,17 @@ export class ConfiguredProject extends Project {
super.markAsDirty();
}

/** @internal */
addExternalProjectReference() {
this.externalProjectRefCount++;
}

/** @internal */
deleteExternalProjectReference() {
this.externalProjectRefCount--;
}

/** @internal */
isSolution() {
return this.getRootFilesMap().size === 0 &&
!this.canConfigFileJsonReportNoInputFiles;
}

/**
* Find the configured project from the project references in project which contains the info directly
*
* @internal
*/
getDefaultChildProjectFromProjectWithReferences(info: ScriptInfo) {
return forEachResolvedProjectReferenceProject(
this,
info.path,
child =>
projectContainsInfoDirectly(child, info) ?
child :
undefined,
ProjectReferenceProjectLoadKind.Find,
);
}

/**
* Returns true if the project is needed by any of the open script info/external project
*
* @internal
*/
hasOpenRef() {
if (!!this.externalProjectRefCount) {
return true;
}

// Closed project doesnt have any reference
if (this.isClosed()) {
return false;
}

const configFileExistenceInfo = this.projectService.configFileExistenceInfoCache.get(this.canonicalConfigFilePath)!;
if (this.deferredClose) return !!configFileExistenceInfo.openFilesImpactedByConfigFile?.size;
if (this.projectService.hasPendingProjectUpdate(this)) {
// If there is pending update for this project,
// we dont know if this project would be needed by any of the open files impacted by this config file
// In that case keep the project alive if there are open files impacted by this project
return !!configFileExistenceInfo.openFilesImpactedByConfigFile?.size;
}

// If there is no pending update for this project,
// We know exact set of open files that get impacted by this configured project as the files in the project
// The project is referenced only if open files impacted by this project are present in this project
return !!configFileExistenceInfo.openFilesImpactedByConfigFile && forEachEntry(
configFileExistenceInfo.openFilesImpactedByConfigFile,
(_value, infoPath) => {
const info = this.projectService.getScriptInfoForPath(infoPath)!;
return this.containsScriptInfo(info) ||
!!forEachResolvedProjectReferenceProject(
this,
info.path,
child => child.containsScriptInfo(info),
ProjectReferenceProjectLoadKind.Find,
);
},
) || false;
}

/** @internal */
override isOrphan(): boolean {
return !!this.deferredClose;
}

/** @internal */
hasExternalProjectRef() {
return !!this.externalProjectRefCount;
}

getEffectiveTypeRoots() {
return getEffectiveTypeRoots(this.getCompilationSettings(), this) || [];
}
Expand Down
38 changes: 10 additions & 28 deletions src/server/scriptInfo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ import {
AbsolutePositionAndLineText,
ConfiguredProject,
Errors,
ExternalProject,
InferredProject,
isBackgroundProject,
isConfiguredProject,
Expand Down Expand Up @@ -580,18 +579,16 @@ export class ScriptInfo {
case 0:
return Errors.ThrowNoProject();
case 1:
return ensurePrimaryProjectKind(
!isProjectDeferredClose(this.containingProjects[0]) ?
this.containingProjects[0] : undefined,
);
return isProjectDeferredClose(this.containingProjects[0]) || isBackgroundProject(this.containingProjects[0]) ?
Errors.ThrowNoProject() :
this.containingProjects[0];
default:
// If this file belongs to multiple projects, below is the order in which default project is used
// - first external project
// - for open script info, its default configured project during opening is default if info is part of it
// - first configured project of which script info is not a source of project reference redirect
// - first configured project
// - first external project
// - first inferred project
let firstExternalProject: ExternalProject | undefined;
let firstConfiguredProject: ConfiguredProject | undefined;
let firstInferredProject: InferredProject | undefined;
let firstNonSourceOfProjectReferenceRedirect: ConfiguredProject | undefined;
Expand All @@ -614,20 +611,17 @@ export class ScriptInfo {
}
if (!firstConfiguredProject) firstConfiguredProject = project;
}
else if (!firstExternalProject && isExternalProject(project)) {
firstExternalProject = project;
else if (isExternalProject(project)) {
return project;
}
else if (!firstInferredProject && isInferredProject(project)) {
firstInferredProject = project;
}
}
return ensurePrimaryProjectKind(
defaultConfiguredProject ||
firstNonSourceOfProjectReferenceRedirect ||
firstConfiguredProject ||
firstExternalProject ||
firstInferredProject,
);
return (defaultConfiguredProject ||
firstNonSourceOfProjectReferenceRedirect ||
firstConfiguredProject ||
firstInferredProject) ?? Errors.ThrowNoProject();
}
}

Expand Down Expand Up @@ -742,18 +736,6 @@ export class ScriptInfo {
}
}

/**
* Throws an error if `project` is an AutoImportProvider or AuxiliaryProject,
* which are used in the background by other Projects and should never be
* reported as the default project for a ScriptInfo.
*/
function ensurePrimaryProjectKind(project: Project | undefined) {
if (!project || isBackgroundProject(project)) {
return Errors.ThrowNoProject();
}
return project;
}

function failIfInvalidPosition(position: number) {
Debug.assert(typeof position === "number", `Expected position ${position} to be a number.`);
Debug.assert(position >= 0, `Expected position to be non-negative.`);
Expand Down
4 changes: 2 additions & 2 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3214,7 +3214,7 @@ export class Session<TMessage = string> implements EventSender {
return this.requiredResponse(response);
},
[protocol.CommandTypes.OpenExternalProject]: (request: protocol.OpenExternalProjectRequest) => {
this.projectService.openExternalProject(request.arguments, /*print*/ true);
this.projectService.openExternalProject(request.arguments, /*cleanupAfter*/ true);
// TODO: GH#20447 report errors
return this.requiredResponse(/*response*/ true);
},
Expand All @@ -3224,7 +3224,7 @@ export class Session<TMessage = string> implements EventSender {
return this.requiredResponse(/*response*/ true);
},
[protocol.CommandTypes.CloseExternalProject]: (request: protocol.CloseExternalProjectRequest) => {
this.projectService.closeExternalProject(request.arguments.projectFileName, /*print*/ true);
this.projectService.closeExternalProject(request.arguments.projectFileName, /*cleanupAfter*/ true);
// TODO: GH#20447 report errors
return this.requiredResponse(/*response*/ true);
},
Expand Down
Loading