mirror of
https://github.com/microsoft/TypeScript.git
synced 2026-05-15 21:36:50 -05:00
Handle the delayed updates due to user action correctly when ensuring the project structure is upto date
Fixes #20629
This commit is contained in:
@@ -376,9 +376,9 @@ namespace ts.server {
|
||||
private safelist: SafeList = defaultTypeSafeList;
|
||||
private legacySafelist: { [key: string]: string } = {};
|
||||
|
||||
private changedFiles: ScriptInfo[];
|
||||
private pendingProjectUpdates = createMap<Project>();
|
||||
private pendingInferredProjectUpdate: boolean;
|
||||
/* @internal */
|
||||
pendingEnsureProjectForOpenFiles: boolean;
|
||||
|
||||
readonly currentDirectory: string;
|
||||
readonly toCanonicalFileName: (f: string) => string;
|
||||
@@ -483,11 +483,6 @@ namespace ts.server {
|
||||
return getNormalizedAbsolutePath(fileName, this.host.getCurrentDirectory());
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
getChangedFiles_TestOnly() {
|
||||
return this.changedFiles;
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
ensureInferredProjectsUpToDate_TestOnly() {
|
||||
this.ensureProjectStructuresUptoDate();
|
||||
@@ -552,19 +547,18 @@ namespace ts.server {
|
||||
this.typingsCache.deleteTypingsForProject(response.projectName);
|
||||
break;
|
||||
}
|
||||
this.delayUpdateProjectGraphAndInferredProjectsRefresh(project);
|
||||
this.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project);
|
||||
}
|
||||
|
||||
private delayInferredProjectsRefresh() {
|
||||
this.pendingInferredProjectUpdate = true;
|
||||
this.throttledOperations.schedule("*refreshInferredProjects*", /*delay*/ 250, () => {
|
||||
private delayEnsureProjectForOpenFiles() {
|
||||
this.pendingEnsureProjectForOpenFiles = true;
|
||||
this.throttledOperations.schedule("*ensureProjectForOpenFiles*", /*delay*/ 250, () => {
|
||||
if (this.pendingProjectUpdates.size !== 0) {
|
||||
this.delayInferredProjectsRefresh();
|
||||
this.delayEnsureProjectForOpenFiles();
|
||||
}
|
||||
else {
|
||||
if (this.pendingInferredProjectUpdate) {
|
||||
this.pendingInferredProjectUpdate = false;
|
||||
this.refreshInferredProjects();
|
||||
if (this.pendingEnsureProjectForOpenFiles) {
|
||||
this.ensureProjectForOpenFiles();
|
||||
}
|
||||
// Send the event to notify that there were background project updates
|
||||
// send current list of open files
|
||||
@@ -574,6 +568,7 @@ namespace ts.server {
|
||||
}
|
||||
|
||||
private delayUpdateProjectGraph(project: Project) {
|
||||
project.markAsDirty();
|
||||
const projectName = project.getProjectName();
|
||||
this.pendingProjectUpdates.set(projectName, project);
|
||||
this.throttledOperations.schedule(projectName, /*delay*/ 250, () => {
|
||||
@@ -603,17 +598,16 @@ namespace ts.server {
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
delayUpdateProjectGraphAndInferredProjectsRefresh(project: Project) {
|
||||
project.markAsDirty();
|
||||
delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project: Project) {
|
||||
this.delayUpdateProjectGraph(project);
|
||||
this.delayInferredProjectsRefresh();
|
||||
this.delayEnsureProjectForOpenFiles();
|
||||
}
|
||||
|
||||
private delayUpdateProjectGraphs(projects: Project[]) {
|
||||
private delayUpdateProjectGraphs(projects: ReadonlyArray<Project>) {
|
||||
for (const project of projects) {
|
||||
this.delayUpdateProjectGraph(project);
|
||||
}
|
||||
this.delayInferredProjectsRefresh();
|
||||
this.delayEnsureProjectForOpenFiles();
|
||||
}
|
||||
|
||||
setCompilerOptionsForInferredProjects(projectCompilerOptions: protocol.ExternalProjectCompilerOptions, projectRootPath?: string): void {
|
||||
@@ -632,7 +626,6 @@ namespace ts.server {
|
||||
this.compilerOptionsForInferredProjects = compilerOptions;
|
||||
}
|
||||
|
||||
const projectsToUpdate: Project[] = [];
|
||||
for (const project of this.inferredProjects) {
|
||||
// Only update compiler options in the following cases:
|
||||
// - Inferred projects without a projectRootPath, if the new options do not apply to
|
||||
@@ -648,11 +641,11 @@ namespace ts.server {
|
||||
project.setCompilerOptions(compilerOptions);
|
||||
project.compileOnSaveEnabled = compilerOptions.compileOnSave;
|
||||
project.markAsDirty();
|
||||
projectsToUpdate.push(project);
|
||||
this.delayUpdateProjectGraph(project);
|
||||
}
|
||||
}
|
||||
|
||||
this.delayUpdateProjectGraphs(projectsToUpdate);
|
||||
this.delayEnsureProjectForOpenFiles();
|
||||
}
|
||||
|
||||
findProject(projectName: string): Project | undefined {
|
||||
@@ -687,41 +680,27 @@ namespace ts.server {
|
||||
/**
|
||||
* Ensures the project structures are upto date
|
||||
* This means,
|
||||
* - if there are changedFiles (the files were updated but their containing project graph was not upto date),
|
||||
* their project graph is updated
|
||||
* - If there are pendingProjectUpdates (scheduled to be updated with delay so they can batch update the graph if there are several changes in short time span)
|
||||
* their project graph is updated
|
||||
* - If there were project graph updates and/or there was pending inferred project update and/or called forced the inferred project structure refresh
|
||||
* Inferred projects are created/updated/deleted based on open files states
|
||||
* @param forceInferredProjectsRefresh when true updates the inferred projects even if there is no pending work to update the files/project structures
|
||||
* - we go through all the projects and update them if they are dirty
|
||||
* - if updates reflect some change in structure or there was pending request to ensure projects for open files
|
||||
* ensure that each open script info has project
|
||||
*/
|
||||
private ensureProjectStructuresUptoDate(forceInferredProjectsRefresh?: boolean) {
|
||||
if (this.changedFiles) {
|
||||
let projectsToUpdate: Project[];
|
||||
if (this.changedFiles.length === 1) {
|
||||
// simpliest case - no allocations
|
||||
projectsToUpdate = this.changedFiles[0].containingProjects;
|
||||
}
|
||||
else {
|
||||
projectsToUpdate = [];
|
||||
for (const f of this.changedFiles) {
|
||||
addRange(projectsToUpdate, f.containingProjects);
|
||||
}
|
||||
}
|
||||
this.changedFiles = undefined;
|
||||
this.updateProjectGraphs(projectsToUpdate);
|
||||
}
|
||||
private ensureProjectStructuresUptoDate() {
|
||||
let hasChanges = this.pendingEnsureProjectForOpenFiles;
|
||||
this.pendingProjectUpdates.clear();
|
||||
const updateGraph = (project: Project) => {
|
||||
hasChanges = this.updateProjectIfDirty(project) || hasChanges;
|
||||
};
|
||||
|
||||
if (this.pendingProjectUpdates.size !== 0) {
|
||||
const projectsToUpdate = arrayFrom(this.pendingProjectUpdates.values());
|
||||
this.pendingProjectUpdates.clear();
|
||||
this.updateProjectGraphs(projectsToUpdate);
|
||||
this.externalProjects.forEach(updateGraph);
|
||||
this.configuredProjects.forEach(updateGraph);
|
||||
this.inferredProjects.forEach(updateGraph);
|
||||
if (hasChanges) {
|
||||
this.ensureProjectForOpenFiles();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.pendingInferredProjectUpdate || forceInferredProjectsRefresh) {
|
||||
this.pendingInferredProjectUpdate = false;
|
||||
this.refreshInferredProjects();
|
||||
}
|
||||
private updateProjectIfDirty(project: Project) {
|
||||
return project.dirty && project.updateGraph();
|
||||
}
|
||||
|
||||
getFormatCodeOptions(file?: NormalizedPath) {
|
||||
@@ -735,14 +714,6 @@ namespace ts.server {
|
||||
return formatCodeSettings || this.hostConfiguration.formatCodeOptions;
|
||||
}
|
||||
|
||||
private updateProjectGraphs(projects: Project[]) {
|
||||
for (const p of projects) {
|
||||
if (!p.updateGraph()) {
|
||||
this.pendingInferredProjectUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private onSourceFileChanged(fileName: NormalizedPath, eventKind: FileWatcherEventKind) {
|
||||
const info = this.getScriptInfoForNormalizedPath(fileName);
|
||||
if (!info) {
|
||||
@@ -770,8 +741,6 @@ namespace ts.server {
|
||||
private handleDeletedFile(info: ScriptInfo) {
|
||||
this.stopWatchingScriptInfo(info);
|
||||
|
||||
// TODO: handle isOpen = true case
|
||||
|
||||
if (!info.isScriptOpen()) {
|
||||
this.deleteScriptInfo(info);
|
||||
|
||||
@@ -808,7 +777,7 @@ namespace ts.server {
|
||||
// Reload is pending, do the reload
|
||||
if (project.pendingReload !== ConfigFileProgramReloadLevel.Full) {
|
||||
project.pendingReload = ConfigFileProgramReloadLevel.Partial;
|
||||
this.delayUpdateProjectGraphAndInferredProjectsRefresh(project);
|
||||
this.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project);
|
||||
}
|
||||
},
|
||||
flags,
|
||||
@@ -1317,7 +1286,11 @@ namespace ts.server {
|
||||
|
||||
this.logger.info("Open files: ");
|
||||
this.openFiles.forEach((projectRootPath, path) => {
|
||||
this.logger.info(`\tFileName: ${this.getScriptInfoForPath(path as Path).fileName} ProjectRootPath: ${projectRootPath}`);
|
||||
const info = this.getScriptInfoForPath(path as Path);
|
||||
this.logger.info(`\tFileName: ${info.fileName} ProjectRootPath: ${projectRootPath}`);
|
||||
if (writeProjectFileNames) {
|
||||
this.logger.info(`\t\tProjects: ${info.containingProjects.map(p => p.getProjectName())}`);
|
||||
}
|
||||
});
|
||||
|
||||
this.logger.endGroup();
|
||||
@@ -1896,7 +1869,7 @@ namespace ts.server {
|
||||
|
||||
// Reload Projects
|
||||
this.reloadConfiguredProjectForFiles(this.openFiles, /*delayReload*/ false, returnTrue);
|
||||
this.refreshInferredProjects();
|
||||
this.ensureProjectForOpenFiles();
|
||||
}
|
||||
|
||||
private delayReloadConfiguredProjectForFiles(configFileExistenceInfo: ConfigFileExistenceInfo, ignoreIfNotRootOfInferredProject: boolean) {
|
||||
@@ -1908,7 +1881,7 @@ namespace ts.server {
|
||||
isRootOfInferredProject => isRootOfInferredProject : // Reload open files if they are root of inferred project
|
||||
returnTrue // Reload all the open files impacted by config file
|
||||
);
|
||||
this.delayInferredProjectsRefresh();
|
||||
this.delayEnsureProjectForOpenFiles();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1992,8 +1965,8 @@ namespace ts.server {
|
||||
* This will go through open files and assign them to inferred project if open file is not part of any other project
|
||||
* After that all the inferred project graphs are updated
|
||||
*/
|
||||
private refreshInferredProjects() {
|
||||
this.logger.info("refreshInferredProjects: updating project structure from ...");
|
||||
private ensureProjectForOpenFiles() {
|
||||
this.logger.info("Structure before ensureProjectForOpenFiles:");
|
||||
this.printProjects();
|
||||
|
||||
this.openFiles.forEach((projectRootPath, path) => {
|
||||
@@ -2007,12 +1980,10 @@ namespace ts.server {
|
||||
this.removeRootOfInferredProjectIfNowPartOfOtherProject(info);
|
||||
}
|
||||
});
|
||||
this.pendingEnsureProjectForOpenFiles = false;
|
||||
this.inferredProjects.forEach(p => this.updateProjectIfDirty(p));
|
||||
|
||||
for (const p of this.inferredProjects) {
|
||||
p.updateGraph();
|
||||
}
|
||||
|
||||
this.logger.info("refreshInferredProjects: updated project structure ...");
|
||||
this.logger.info("Structure after ensureProjectForOpenFiles:");
|
||||
this.printProjects();
|
||||
}
|
||||
|
||||
@@ -2149,11 +2120,6 @@ namespace ts.server {
|
||||
this.closeClientFile(file);
|
||||
}
|
||||
}
|
||||
// if files were open or closed then explicitly refresh list of inferred projects
|
||||
// otherwise if there were only changes in files - record changed files in `changedFiles` and defer the update
|
||||
if (openFiles || closedFiles) {
|
||||
this.ensureProjectStructuresUptoDate(/*refreshInferredProjects*/ true);
|
||||
}
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
@@ -2163,49 +2129,33 @@ namespace ts.server {
|
||||
const change = changes[i];
|
||||
scriptInfo.editContent(change.span.start, change.span.start + change.span.length, change.newText);
|
||||
}
|
||||
if (!this.changedFiles) {
|
||||
this.changedFiles = [scriptInfo];
|
||||
}
|
||||
else if (!contains(this.changedFiles, scriptInfo)) {
|
||||
this.changedFiles.push(scriptInfo);
|
||||
}
|
||||
}
|
||||
|
||||
private closeConfiguredProjectReferencedFromExternalProject(configFile: NormalizedPath): boolean {
|
||||
private closeConfiguredProjectReferencedFromExternalProject(configFile: NormalizedPath) {
|
||||
const configuredProject = this.findConfiguredProjectByProjectName(configFile);
|
||||
if (configuredProject) {
|
||||
configuredProject.deleteExternalProjectReference();
|
||||
if (!configuredProject.hasOpenRef()) {
|
||||
this.removeProject(configuredProject);
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
closeExternalProject(uncheckedFileName: string, suppressRefresh = false): void {
|
||||
closeExternalProject(uncheckedFileName: string): void {
|
||||
const fileName = toNormalizedPath(uncheckedFileName);
|
||||
const configFiles = this.externalProjectToConfiguredProjectMap.get(fileName);
|
||||
if (configFiles) {
|
||||
let shouldRefreshInferredProjects = false;
|
||||
for (const configFile of configFiles) {
|
||||
if (this.closeConfiguredProjectReferencedFromExternalProject(configFile)) {
|
||||
shouldRefreshInferredProjects = true;
|
||||
}
|
||||
this.closeConfiguredProjectReferencedFromExternalProject(configFile);
|
||||
}
|
||||
this.externalProjectToConfiguredProjectMap.delete(fileName);
|
||||
if (shouldRefreshInferredProjects && !suppressRefresh) {
|
||||
this.ensureProjectStructuresUptoDate(/*refreshInferredProjects*/ true);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// close external project
|
||||
const externalProject = this.findExternalProjectByProjectName(uncheckedFileName);
|
||||
if (externalProject) {
|
||||
this.removeProject(externalProject);
|
||||
if (!suppressRefresh) {
|
||||
this.ensureProjectStructuresUptoDate(/*refreshInferredProjects*/ true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2218,17 +2168,15 @@ namespace ts.server {
|
||||
});
|
||||
|
||||
for (const externalProject of projects) {
|
||||
this.openExternalProject(externalProject, /*suppressRefreshOfInferredProjects*/ true);
|
||||
this.openExternalProject(externalProject);
|
||||
// delete project that is present in input list
|
||||
projectsToClose.delete(externalProject.projectFileName);
|
||||
}
|
||||
|
||||
// close projects that were missing in the input list
|
||||
forEachKey(projectsToClose, externalProjectName => {
|
||||
this.closeExternalProject(externalProjectName, /*suppressRefresh*/ true);
|
||||
this.closeExternalProject(externalProjectName);
|
||||
});
|
||||
|
||||
this.ensureProjectStructuresUptoDate(/*refreshInferredProjects*/ true);
|
||||
}
|
||||
|
||||
/** Makes a filename safe to insert in a RegExp */
|
||||
@@ -2349,7 +2297,7 @@ namespace ts.server {
|
||||
return excludedFiles;
|
||||
}
|
||||
|
||||
openExternalProject(proj: protocol.ExternalProject, suppressRefreshOfInferredProjects = false): void {
|
||||
openExternalProject(proj: protocol.ExternalProject): void {
|
||||
// typingOptions has been deprecated and is only supported for backward compatibility
|
||||
// purposes. It should be removed in future releases - use typeAcquisition instead.
|
||||
if (proj.typingOptions && !proj.typeAcquisition) {
|
||||
@@ -2403,13 +2351,13 @@ namespace ts.server {
|
||||
}
|
||||
// some config files were added to external project (that previously were not there)
|
||||
// close existing project and later we'll open a set of configured projects for these files
|
||||
this.closeExternalProject(proj.projectFileName, /*suppressRefresh*/ true);
|
||||
this.closeExternalProject(proj.projectFileName);
|
||||
}
|
||||
else if (this.externalProjectToConfiguredProjectMap.get(proj.projectFileName)) {
|
||||
// this project used to include config files
|
||||
if (!tsConfigFiles) {
|
||||
// config files were removed from the project - close existing external project which in turn will close configured projects
|
||||
this.closeExternalProject(proj.projectFileName, /*suppressRefresh*/ true);
|
||||
this.closeExternalProject(proj.projectFileName);
|
||||
}
|
||||
else {
|
||||
// project previously had some config files - compare them with new set of files and close all configured projects that correspond to unused files
|
||||
@@ -2459,9 +2407,6 @@ namespace ts.server {
|
||||
this.externalProjectToConfiguredProjectMap.delete(proj.projectFileName);
|
||||
this.createExternalProject(proj.projectFileName, rootFiles, proj.options, proj.typeAcquisition, excludedFiles);
|
||||
}
|
||||
if (!suppressRefreshOfInferredProjects) {
|
||||
this.ensureProjectStructuresUptoDate(/*refreshInferredProjects*/ true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,6 +168,9 @@ namespace ts.server {
|
||||
*/
|
||||
private projectStateVersion = 0;
|
||||
|
||||
/*@internal*/
|
||||
dirty = false;
|
||||
|
||||
/*@internal*/
|
||||
hasChangedAutomaticTypeDirectiveNames = false;
|
||||
|
||||
@@ -250,6 +253,7 @@ namespace ts.server {
|
||||
this.disableLanguageService(lastFileExceededProgramSize);
|
||||
}
|
||||
this.markAsDirty();
|
||||
this.projectService.pendingEnsureProjectForOpenFiles = true;
|
||||
}
|
||||
|
||||
isKnownTypesPackageName(name: string): boolean {
|
||||
@@ -399,7 +403,7 @@ namespace ts.server {
|
||||
|
||||
/*@internal*/
|
||||
onInvalidatedResolution() {
|
||||
this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this);
|
||||
this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this);
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
@@ -417,7 +421,7 @@ namespace ts.server {
|
||||
/*@internal*/
|
||||
onChangedAutomaticTypeDirectiveNames() {
|
||||
this.hasChangedAutomaticTypeDirectiveNames = true;
|
||||
this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this);
|
||||
this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this);
|
||||
}
|
||||
|
||||
/*@internal*/
|
||||
@@ -565,6 +569,7 @@ namespace ts.server {
|
||||
for (const root of this.rootFiles) {
|
||||
root.detachFromProject(this);
|
||||
}
|
||||
this.projectService.pendingEnsureProjectForOpenFiles = true;
|
||||
|
||||
this.rootFiles = undefined;
|
||||
this.rootFilesMap = undefined;
|
||||
@@ -748,7 +753,10 @@ namespace ts.server {
|
||||
}
|
||||
|
||||
markAsDirty() {
|
||||
this.projectStateVersion++;
|
||||
if (!this.dirty) {
|
||||
this.projectStateVersion++;
|
||||
this.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
/* @internal */
|
||||
@@ -823,7 +831,9 @@ namespace ts.server {
|
||||
}
|
||||
|
||||
const cachedTypings = this.projectService.typingsCache.getTypingsForProject(this, this.lastCachedUnresolvedImportsList, hasChanges);
|
||||
if (this.setTypings(cachedTypings)) {
|
||||
if (!arrayIsEqualTo(this.typingFiles, cachedTypings)) {
|
||||
this.typingFiles = cachedTypings;
|
||||
this.markAsDirty();
|
||||
hasChanges = this.updateGraphWorker() || hasChanges;
|
||||
}
|
||||
}
|
||||
@@ -847,15 +857,6 @@ namespace ts.server {
|
||||
return include.filter(i => existing.indexOf(i) < 0);
|
||||
}
|
||||
|
||||
private setTypings(typings: SortedReadonlyArray<string>): boolean {
|
||||
if (arrayIsEqualTo(this.typingFiles, typings)) {
|
||||
return false;
|
||||
}
|
||||
this.typingFiles = typings;
|
||||
this.markAsDirty();
|
||||
return true;
|
||||
}
|
||||
|
||||
private updateGraphWorker() {
|
||||
const oldProgram = this.program;
|
||||
Debug.assert(!this.isClosed(), "Called update graph worker of closed project");
|
||||
@@ -864,6 +865,7 @@ namespace ts.server {
|
||||
this.hasInvalidatedResolution = this.resolutionCache.createHasInvalidatedResolution();
|
||||
this.resolutionCache.startCachingPerDirectoryResolution();
|
||||
this.program = this.languageService.getProgram();
|
||||
this.dirty = false;
|
||||
this.resolutionCache.finishCachingPerDirectoryResolution();
|
||||
|
||||
// bump up the version if
|
||||
@@ -910,7 +912,7 @@ namespace ts.server {
|
||||
compareStringsCaseSensitive
|
||||
);
|
||||
const elapsed = timestamp() - start;
|
||||
this.writeLog(`Finishing updateGraphWorker: Project: ${this.getProjectName()} structureChanged: ${hasChanges} Elapsed: ${elapsed}ms`);
|
||||
this.writeLog(`Finishing updateGraphWorker: Project: ${this.getProjectName()} Version: ${this.getProjectVersion()} structureChanged: ${hasChanges} Elapsed: ${elapsed}ms`);
|
||||
return hasChanges;
|
||||
}
|
||||
|
||||
@@ -936,7 +938,7 @@ namespace ts.server {
|
||||
fileWatcher.close();
|
||||
|
||||
// When a missing file is created, we should update the graph.
|
||||
this.projectService.delayUpdateProjectGraphAndInferredProjectsRefresh(this);
|
||||
this.projectService.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(this);
|
||||
}
|
||||
},
|
||||
WatchType.MissingFilePath,
|
||||
|
||||
@@ -1769,7 +1769,7 @@ namespace ts.server {
|
||||
return this.requiredResponse(response);
|
||||
},
|
||||
[CommandNames.OpenExternalProject]: (request: protocol.OpenExternalProjectRequest) => {
|
||||
this.projectService.openExternalProject(request.arguments, /*suppressRefreshOfInferredProjects*/ false);
|
||||
this.projectService.openExternalProject(request.arguments);
|
||||
// TODO: GH#20447 report errors
|
||||
return this.requiredResponse(/*response*/ true);
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user