Skip to content

Commit 4e29496

Browse files
authored
Keep scriptInfo and project alive even after file delete till next file open (microsoft#57492)
1 parent 551a600 commit 4e29496

File tree

244 files changed

+210236
-3626
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

244 files changed

+210236
-3626
lines changed

src/harness/projectServiceStateLogger.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
import {
1414
AutoImportProviderProject,
1515
AuxiliaryProject,
16+
ConfiguredProject,
1617
isBackgroundProject,
1718
isConfiguredProject,
1819
LogLevel,
@@ -32,6 +33,7 @@ interface ProjectData {
3233
isClosed: ReturnType<Project["isClosed"]>;
3334
isOrphan: ReturnType<Project["isOrphan"]>;
3435
noOpenRef: boolean;
36+
deferredClose: ConfiguredProject["deferredClose"];
3537
documentPositionMappers: SourceMapper["documentPositionMappers"];
3638
autoImportProviderHost: Project["autoImportProviderHost"];
3739
noDtsResolutionProject: Project["noDtsResolutionProject"];
@@ -46,6 +48,7 @@ interface ScriptInfoData {
4648
open: ReturnType<ScriptInfo["isScriptOpen"]>;
4749
version: ReturnType<TextStorage["getVersion"]>;
4850
pendingReloadFromDisk: TextStorage["pendingReloadFromDisk"];
51+
deferredDelete: ScriptInfo["deferredDelete"];
4952
sourceMapFilePath: Exclude<ScriptInfo["sourceMapFilePath"], SourceMapFileWatcher> | SourceMapFileWatcherData | undefined;
5053
declarationInfoPath: ScriptInfo["declarationInfoPath"];
5154
sourceInfos: ScriptInfo["sourceInfos"];
@@ -116,6 +119,7 @@ export function patchServiceForStateBaseline(service: ProjectService) {
116119
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "isClosed", project.isClosed(), projectDiff, projectPropertyLogs);
117120
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "isOrphan", !isBackgroundProject(project) && project.isOrphan(), projectDiff, projectPropertyLogs);
118121
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "noOpenRef", isConfiguredProject(project) && !project.hasOpenRef(), projectDiff, projectPropertyLogs);
122+
projectDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "deferredClose", isConfiguredProject(project) && project.deferredClose, projectDiff, projectPropertyLogs);
119123
projectDiff = printMapPropertyValue(
120124
PrintPropertyWhen.Changed,
121125
data?.documentPositionMappers,
@@ -146,6 +150,7 @@ export function patchServiceForStateBaseline(service: ProjectService) {
146150
isClosed: project.isClosed(),
147151
isOrphan: !isBackgroundProject(project) && project.isOrphan(),
148152
noOpenRef: isConfiguredProject(project) && !project.hasOpenRef(),
153+
deferredClose: isConfiguredProject(project) && project.deferredClose,
149154
autoImportProviderHost: project.autoImportProviderHost,
150155
noDtsResolutionProject: project.noDtsResolutionProject,
151156
originalConfiguredProjects: project.originalConfiguredProjects && new Set(project.originalConfiguredProjects),
@@ -166,6 +171,7 @@ export function patchServiceForStateBaseline(service: ProjectService) {
166171
infoDiff = printProperty(PrintPropertyWhen.Changed, data, "open", isOpen, infoDiff, infoPropertyLogs);
167172
infoDiff = printProperty(PrintPropertyWhen.Always, data, "version", info.textStorage.getVersion(), infoDiff, infoPropertyLogs);
168173
infoDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "pendingReloadFromDisk", info.textStorage.pendingReloadFromDisk, infoDiff, infoPropertyLogs);
174+
infoDiff = printProperty(PrintPropertyWhen.TruthyOrChangedOrNew, data, "deferredDelete", info.deferredDelete, infoDiff, infoPropertyLogs);
169175
infoDiff = printScriptInfoSourceMapFilePath(data, info, infoDiff, infoPropertyLogs);
170176
infoDiff = printProperty(PrintPropertyWhen.DefinedOrChangedOrNew, data, "declarationInfoPath", info.declarationInfoPath, infoDiff, infoPropertyLogs);
171177
infoDiff = printSetPropertyValueWorker(PrintPropertyWhen.DefinedOrChangedOrNew, data?.sourceInfos, "sourceInfos", info.sourceInfos, infoDiff, infoPropertyLogs, identity);
@@ -200,6 +206,7 @@ export function patchServiceForStateBaseline(service: ProjectService) {
200206
sourceInfos: info.sourceInfos && new Set(info.sourceInfos),
201207
documentPositionMapper: info.documentPositionMapper,
202208
containingProjects: new Set(info.containingProjects),
209+
deferredDelete: info.deferredDelete,
203210
}),
204211
);
205212
}

src/server/editorServices.ts

Lines changed: 295 additions & 176 deletions
Large diffs are not rendered by default.

src/server/project.ts

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -657,7 +657,12 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
657657
}
658658

659659
private getOrCreateScriptInfoAndAttachToProject(fileName: string) {
660-
const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(fileName, this.currentDirectory, this.directoryStructureHost);
660+
const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(
661+
fileName,
662+
this.currentDirectory,
663+
this.directoryStructureHost,
664+
/*deferredDeleteOk*/ false,
665+
);
661666
if (scriptInfo) {
662667
const existingValue = this.rootFilesMap.get(scriptInfo.path);
663668
if (existingValue && existingValue.info !== scriptInfo) {
@@ -678,7 +683,12 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
678683
getScriptVersion(filename: string) {
679684
// Don't attach to the project if version is asked
680685

681-
const info = this.projectService.getOrCreateScriptInfoNotOpenedByClient(filename, this.currentDirectory, this.directoryStructureHost);
686+
const info = this.projectService.getOrCreateScriptInfoNotOpenedByClient(
687+
filename,
688+
this.currentDirectory,
689+
this.directoryStructureHost,
690+
/*deferredDeleteOk*/ false,
691+
);
682692
return (info && info.getLatestVersion())!; // TODO: GH#18217
683693
}
684694

@@ -1298,6 +1308,7 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
12981308
}
12991309
}
13001310

1311+
/** @internal */
13011312
markAsDirty() {
13021313
if (!this.dirty) {
13031314
this.projectStateVersion++;
@@ -1658,7 +1669,12 @@ export abstract class Project implements LanguageServiceHost, ModuleResolutionHo
16581669
// by the host for files in the program when the program is retrieved above but
16591670
// the program doesn't contain external files so this must be done explicitly.
16601671
inserted => {
1661-
const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(inserted, this.currentDirectory, this.directoryStructureHost);
1672+
const scriptInfo = this.projectService.getOrCreateScriptInfoNotOpenedByClient(
1673+
inserted,
1674+
this.currentDirectory,
1675+
this.directoryStructureHost,
1676+
/*deferredDeleteOk*/ false,
1677+
);
16621678
scriptInfo?.attachToProject(this);
16631679
},
16641680
removed => this.detachScriptInfoFromProject(removed),
@@ -2584,6 +2600,7 @@ export class AutoImportProviderProject extends Project {
25842600
return !some(this.rootFileNames);
25852601
}
25862602

2603+
/** @internal */
25872604
override isOrphan() {
25882605
return true;
25892606
}
@@ -2619,6 +2636,7 @@ export class AutoImportProviderProject extends Project {
26192636
return !!this.rootFileNames?.length;
26202637
}
26212638

2639+
/** @internal */
26222640
override markAsDirty() {
26232641
this.rootFileNames = undefined;
26242642
super.markAsDirty();
@@ -2713,6 +2731,9 @@ export class ConfiguredProject extends Project {
27132731
/** @internal */
27142732
skipConfigDiagEvent?: true;
27152733

2734+
/** @internal */
2735+
deferredClose?: boolean;
2736+
27162737
/** @internal */
27172738
constructor(
27182739
configFileName: NormalizedPath,
@@ -2773,6 +2794,7 @@ export class ConfiguredProject extends Project {
27732794
* @returns: true if set of files in the project stays the same and false - otherwise.
27742795
*/
27752796
override updateGraph(): boolean {
2797+
if (this.deferredClose) return false;
27762798
const isInitialLoad = this.isInitialLoadPending();
27772799
this.isInitialLoadPending = returnFalse;
27782800
const updateLevel = this.pendingUpdateLevel;
@@ -2892,6 +2914,12 @@ export class ConfiguredProject extends Project {
28922914
super.close();
28932915
}
28942916

2917+
/** @internal */
2918+
override markAsDirty() {
2919+
if (this.deferredClose) return;
2920+
super.markAsDirty();
2921+
}
2922+
28952923
/** @internal */
28962924
addExternalProjectReference() {
28972925
this.externalProjectRefCount++;
@@ -2941,6 +2969,7 @@ export class ConfiguredProject extends Project {
29412969
}
29422970

29432971
const configFileExistenceInfo = this.projectService.configFileExistenceInfoCache.get(this.canonicalConfigFilePath)!;
2972+
if (this.deferredClose) return !!configFileExistenceInfo.openFilesImpactedByConfigFile?.size;
29442973
if (this.projectService.hasPendingProjectUpdate(this)) {
29452974
// If there is pending update for this project,
29462975
// we dont know if this project would be needed by any of the open files impacted by this config file
@@ -2966,6 +2995,11 @@ export class ConfiguredProject extends Project {
29662995
) || false;
29672996
}
29682997

2998+
/** @internal */
2999+
override isOrphan(): boolean {
3000+
return !!this.deferredClose;
3001+
}
3002+
29693003
/** @internal */
29703004
hasExternalProjectRef() {
29713005
return !!this.externalProjectRefCount;
@@ -3023,3 +3057,8 @@ export function isExternalProject(project: Project): project is ExternalProject
30233057
export function isBackgroundProject(project: Project): project is AutoImportProviderProject | AuxiliaryProject {
30243058
return project.projectKind === ProjectKind.AutoImportProvider || project.projectKind === ProjectKind.Auxiliary;
30253059
}
3060+
3061+
/** @internal */
3062+
export function isProjectDeferredClose(project: Project): project is ConfiguredProject {
3063+
return isConfiguredProject(project) && !!project.deferredClose;
3064+
}

src/server/scriptInfo.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
IScriptSnapshot,
2626
isString,
2727
LineInfo,
28+
missingFileModifiedTime,
2829
orderedRemoveItem,
2930
Path,
3031
ScriptKind,
@@ -44,6 +45,7 @@ import {
4445
isConfiguredProject,
4546
isExternalProject,
4647
isInferredProject,
48+
isProjectDeferredClose,
4749
maxFileSize,
4850
NormalizedPath,
4951
Project,
@@ -180,6 +182,14 @@ export class TextStorage {
180182
const reloaded = this.reload(newText);
181183
this.fileSize = fileSize; // NB: after reload since reload clears it
182184
this.ownFileText = !tempFileName || tempFileName === this.info.fileName;
185+
// In case we update this text before mTime gets updated to present file modified time
186+
// because its schedule to do that later, update the mTime so we dont re-update the text
187+
// Eg. with npm ci where file gets created and editor calls say get error request before
188+
// the timeout to update the file stamps in node_modules is run
189+
// Test:: watching npm install in codespaces where workspaces folder is hosted at root
190+
if (this.ownFileText && this.info.mTime === missingFileModifiedTime.getTime()) {
191+
this.info.mTime = (this.host.getModifiedTime!(this.info.fileName) || missingFileModifiedTime).getTime();
192+
}
183193
return reloaded;
184194
}
185195

@@ -398,6 +408,9 @@ export class ScriptInfo {
398408
/** @internal */
399409
documentPositionMapper?: DocumentPositionMapper | false;
400410

411+
/** @internal */
412+
deferredDelete?: boolean;
413+
401414
constructor(
402415
private readonly host: ServerHost,
403416
readonly fileName: NormalizedPath,
@@ -567,7 +580,10 @@ export class ScriptInfo {
567580
case 0:
568581
return Errors.ThrowNoProject();
569582
case 1:
570-
return ensurePrimaryProjectKind(this.containingProjects[0]);
583+
return ensurePrimaryProjectKind(
584+
!isProjectDeferredClose(this.containingProjects[0]) ?
585+
this.containingProjects[0] : undefined,
586+
);
571587
default:
572588
// If this file belongs to multiple projects, below is the order in which default project is used
573589
// - for open script info, its default configured project during opening is default if info is part of it
@@ -583,6 +599,7 @@ export class ScriptInfo {
583599
for (let index = 0; index < this.containingProjects.length; index++) {
584600
const project = this.containingProjects[index];
585601
if (isConfiguredProject(project)) {
602+
if (project.deferredClose) continue;
586603
if (!project.isSourceOfProjectReferenceRedirect(this.fileName)) {
587604
// If we havent found default configuredProject and
588605
// its not the last one, find it and use that one if there
@@ -676,7 +693,7 @@ export class ScriptInfo {
676693
}
677694

678695
isOrphan() {
679-
return !forEach(this.containingProjects, p => !p.isOrphan());
696+
return this.deferredDelete || !forEach(this.containingProjects, p => !p.isOrphan());
680697
}
681698

682699
/** @internal */

src/server/session.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1575,6 +1575,7 @@ export class Session<TMessage = string> implements EventSender {
15751575
fileNameToSearch,
15761576
noDtsProject.currentDirectory,
15771577
noDtsProject.directoryStructureHost,
1578+
/*deferredDeleteOk*/ false,
15781579
);
15791580
if (!info) continue;
15801581
if (!noDtsProject.containsScriptInfo(info)) {

0 commit comments

Comments
 (0)