Skip to content

Commit 2fe3c1b

Browse files
authored
Merge pull request microsoft#32561 from microsoft/retainFreshlyCreatedProject
Retain the configured project opened during opening client file even if opened file isnt included in that project
2 parents 599e36a + 10ee85c commit 2fe3c1b

File tree

4 files changed

+138
-22
lines changed

4 files changed

+138
-22
lines changed

src/server/editorServices.ts

+30-11
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,10 @@ namespace ts.server {
284284
configFileErrors?: ReadonlyArray<Diagnostic>;
285285
}
286286

287+
interface AssignProjectResult extends OpenConfiguredProjectResult {
288+
defaultConfigProject: ConfiguredProject | undefined;
289+
}
290+
287291
interface FilePropertyReader<T> {
288292
getFileName(f: T): string;
289293
getScriptKind(f: T, extraFileExtensions?: FileExtensionInfo[]): ScriptKind;
@@ -2635,10 +2639,11 @@ namespace ts.server {
26352639
return info;
26362640
}
26372641

2638-
private assignProjectToOpenedScriptInfo(info: ScriptInfo): OpenConfiguredProjectResult {
2642+
private assignProjectToOpenedScriptInfo(info: ScriptInfo): AssignProjectResult {
26392643
let configFileName: NormalizedPath | undefined;
26402644
let configFileErrors: ReadonlyArray<Diagnostic> | undefined;
26412645
let project: ConfiguredProject | ExternalProject | undefined = this.findExternalProjectContainingOpenScriptInfo(info);
2646+
let defaultConfigProject: ConfiguredProject | undefined;
26422647
if (!project && !this.syntaxOnly) { // Checking syntaxOnly is an optimization
26432648
configFileName = this.getConfigFileNameForFile(info);
26442649
if (configFileName) {
@@ -2659,6 +2664,7 @@ namespace ts.server {
26592664
// Ensure project is ready to check if it contains opened script info
26602665
updateProjectIfDirty(project);
26612666
}
2667+
defaultConfigProject = project;
26622668
}
26632669
}
26642670

@@ -2678,13 +2684,13 @@ namespace ts.server {
26782684
this.assignOrphanScriptInfoToInferredProject(info, this.openFiles.get(info.path));
26792685
}
26802686
Debug.assert(!info.isOrphan());
2681-
return { configFileName, configFileErrors };
2687+
return { configFileName, configFileErrors, defaultConfigProject };
26822688
}
26832689

2684-
private cleanupAfterOpeningFile() {
2690+
private cleanupAfterOpeningFile(toRetainConfigProjects: ConfiguredProject[] | ConfiguredProject | undefined) {
26852691
// This was postponed from closeOpenFile to after opening next file,
26862692
// so that we can reuse the project if we need to right away
2687-
this.removeOrphanConfiguredProjects();
2693+
this.removeOrphanConfiguredProjects(toRetainConfigProjects);
26882694

26892695
// Remove orphan inferred projects now that we have reused projects
26902696
// We need to create a duplicate because we cant guarantee order after removal
@@ -2705,22 +2711,30 @@ namespace ts.server {
27052711

27062712
openClientFileWithNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, projectRootPath?: NormalizedPath): OpenConfiguredProjectResult {
27072713
const info = this.getOrCreateOpenScriptInfo(fileName, fileContent, scriptKind, hasMixedContent, projectRootPath);
2708-
const result = this.assignProjectToOpenedScriptInfo(info);
2709-
this.cleanupAfterOpeningFile();
2714+
const { defaultConfigProject, ...result } = this.assignProjectToOpenedScriptInfo(info);
2715+
this.cleanupAfterOpeningFile(defaultConfigProject);
27102716
this.telemetryOnOpenFile(info);
27112717
return result;
27122718
}
27132719

2714-
private removeOrphanConfiguredProjects() {
2720+
private removeOrphanConfiguredProjects(toRetainConfiguredProjects: ConfiguredProject[] | ConfiguredProject | undefined) {
27152721
const toRemoveConfiguredProjects = cloneMap(this.configuredProjects);
2722+
if (toRetainConfiguredProjects) {
2723+
if (isArray(toRetainConfiguredProjects)) {
2724+
toRetainConfiguredProjects.forEach(retainConfiguredProject);
2725+
}
2726+
else {
2727+
retainConfiguredProject(toRetainConfiguredProjects);
2728+
}
2729+
}
27162730

27172731
// Do not remove configured projects that are used as original projects of other
27182732
this.inferredProjects.forEach(markOriginalProjectsAsUsed);
27192733
this.externalProjects.forEach(markOriginalProjectsAsUsed);
27202734
this.configuredProjects.forEach(project => {
27212735
// 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
27222736
if (project.hasOpenRef()) {
2723-
toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath);
2737+
retainConfiguredProject(project);
27242738
markOriginalProjectsAsUsed(project);
27252739
}
27262740
else {
@@ -2729,7 +2743,7 @@ namespace ts.server {
27292743
if (ref) {
27302744
const refProject = this.configuredProjects.get(ref.sourceFile.path);
27312745
if (refProject && refProject.hasOpenRef()) {
2732-
toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath);
2746+
retainConfiguredProject(project);
27332747
}
27342748
}
27352749
});
@@ -2744,6 +2758,10 @@ namespace ts.server {
27442758
project.originalConfiguredProjects.forEach((_value, configuredProjectPath) => toRemoveConfiguredProjects.delete(configuredProjectPath));
27452759
}
27462760
}
2761+
2762+
function retainConfiguredProject(project: ConfiguredProject) {
2763+
toRemoveConfiguredProjects.delete(project.canonicalConfigFilePath);
2764+
}
27472765
}
27482766

27492767
private removeOrphanScriptInfos() {
@@ -2886,8 +2904,9 @@ namespace ts.server {
28862904
}
28872905

28882906
// All the script infos now exist, so ok to go update projects for open files
2907+
let defaultConfigProjects: ConfiguredProject[] | undefined;
28892908
if (openScriptInfos) {
2890-
openScriptInfos.forEach(info => this.assignProjectToOpenedScriptInfo(info));
2909+
defaultConfigProjects = mapDefined(openScriptInfos, info => this.assignProjectToOpenedScriptInfo(info).defaultConfigProject);
28912910
}
28922911

28932912
// While closing files there could be open files that needed assigning new inferred projects, do it now
@@ -2896,7 +2915,7 @@ namespace ts.server {
28962915
}
28972916

28982917
// Cleanup projects
2899-
this.cleanupAfterOpeningFile();
2918+
this.cleanupAfterOpeningFile(defaultConfigProjects);
29002919

29012920
// Telemetry
29022921
forEach(openScriptInfos, info => this.telemetryOnOpenFile(info));

src/testRunner/unittests/tsserver/configuredProjects.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -866,12 +866,12 @@ namespace ts.projectSystem {
866866
const projectService = createProjectService(host);
867867
projectService.openClientFile(file1.path);
868868
host.runQueuedTimeoutCallbacks();
869-
// Since there is no file open from configFile it would be closed
870-
checkNumberOfConfiguredProjects(projectService, 0);
871-
checkNumberOfInferredProjects(projectService, 1);
872869

870+
// Since file1 refers to config file as the default project, it needs to be kept alive
871+
checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 });
873872
const inferredProject = projectService.inferredProjects[0];
874873
assert.isTrue(inferredProject.containsFile(<server.NormalizedPath>file1.path));
874+
assert.isFalse(projectService.configuredProjects.get(configFile.path)!.containsFile(<server.NormalizedPath>file1.path));
875875
});
876876

877877
it("should be able to handle @types if input file list is empty", () => {
@@ -898,8 +898,8 @@ namespace ts.projectSystem {
898898
const projectService = createProjectService(host);
899899

900900
projectService.openClientFile(f.path);
901-
// Since no file from the configured project is open, it would be closed immediately
902-
projectService.checkNumberOfProjects({ configuredProjects: 0, inferredProjects: 1 });
901+
// Since f refers to config file as the default project, it needs to be kept alive
902+
projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 });
903903
});
904904

905905
it("should tolerate invalid include files that start in subDirectory", () => {
@@ -924,8 +924,8 @@ namespace ts.projectSystem {
924924
const projectService = createProjectService(host);
925925

926926
projectService.openClientFile(f.path);
927-
// Since no file from the configured project is open, it would be closed immediately
928-
projectService.checkNumberOfProjects({ configuredProjects: 0, inferredProjects: 1 });
927+
// Since f refers to config file as the default project, it needs to be kept alive
928+
projectService.checkNumberOfProjects({ configuredProjects: 1, inferredProjects: 1 });
929929
});
930930

931931
it("Changed module resolution reflected when specifying files list", () => {

src/testRunner/unittests/tsserver/inferredProjects.ts

+87
Original file line numberDiff line numberDiff line change
@@ -345,5 +345,92 @@ namespace ts.projectSystem {
345345
it("inferred projects per project root with case insensitive system", () => {
346346
verifyProjectRootWithCaseSensitivity(/*useCaseSensitiveFileNames*/ false);
347347
});
348+
349+
it("should still retain configured project created while opening the file", () => {
350+
const projectRoot = "/user/username/projects/project";
351+
const appFile: File = {
352+
path: `${projectRoot}/app.ts`,
353+
content: `const app = 20;`
354+
};
355+
const config: File = {
356+
path: `${projectRoot}/tsconfig.json`,
357+
content: "{}"
358+
};
359+
const jsFile1: File = {
360+
path: `${projectRoot}/jsFile1.js`,
361+
content: `const jsFile1 = 10;`
362+
};
363+
const jsFile2: File = {
364+
path: `${projectRoot}/jsFile2.js`,
365+
content: `const jsFile2 = 10;`
366+
};
367+
const host = createServerHost([appFile, libFile, config, jsFile1, jsFile2]);
368+
const projectService = createProjectService(host);
369+
const originalSet = projectService.configuredProjects.set;
370+
const originalDelete = projectService.configuredProjects.delete;
371+
const configuredCreated = createMap<true>();
372+
const configuredRemoved = createMap<true>();
373+
projectService.configuredProjects.set = (key, value) => {
374+
assert.isFalse(configuredCreated.has(key));
375+
configuredCreated.set(key, true);
376+
return originalSet.call(projectService.configuredProjects, key, value);
377+
};
378+
projectService.configuredProjects.delete = key => {
379+
assert.isFalse(configuredRemoved.has(key));
380+
configuredRemoved.set(key, true);
381+
return originalDelete.call(projectService.configuredProjects, key);
382+
};
383+
384+
// Do not remove config project when opening jsFile that is not present as part of config project
385+
projectService.openClientFile(jsFile1.path);
386+
checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 });
387+
checkProjectActualFiles(projectService.inferredProjects[0], [jsFile1.path, libFile.path]);
388+
const project = projectService.configuredProjects.get(config.path)!;
389+
checkProjectActualFiles(project, [appFile.path, config.path, libFile.path]);
390+
checkConfiguredProjectCreatedAndNotDeleted();
391+
392+
// Do not remove config project when opening jsFile that is not present as part of config project
393+
projectService.closeClientFile(jsFile1.path);
394+
checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 });
395+
projectService.openClientFile(jsFile2.path);
396+
checkNumberOfProjects(projectService, { inferredProjects: 1, configuredProjects: 1 });
397+
checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, libFile.path]);
398+
checkProjectActualFiles(project, [appFile.path, config.path, libFile.path]);
399+
checkConfiguredProjectNotCreatedAndNotDeleted();
400+
401+
// Do not remove config project when opening jsFile that is not present as part of config project
402+
projectService.openClientFile(jsFile1.path);
403+
checkNumberOfProjects(projectService, { inferredProjects: 2, configuredProjects: 1 });
404+
checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, libFile.path]);
405+
checkProjectActualFiles(projectService.inferredProjects[1], [jsFile1.path, libFile.path]);
406+
checkProjectActualFiles(project, [appFile.path, config.path, libFile.path]);
407+
checkConfiguredProjectNotCreatedAndNotDeleted();
408+
409+
// When opening file that doesnt fall back to the config file, we remove the config project
410+
projectService.openClientFile(libFile.path);
411+
checkNumberOfProjects(projectService, { inferredProjects: 2 });
412+
checkProjectActualFiles(projectService.inferredProjects[0], [jsFile2.path, libFile.path]);
413+
checkProjectActualFiles(projectService.inferredProjects[1], [jsFile1.path, libFile.path]);
414+
checkConfiguredProjectNotCreatedButDeleted();
415+
416+
function checkConfiguredProjectCreatedAndNotDeleted() {
417+
assert.equal(configuredCreated.size, 1);
418+
assert.isTrue(configuredCreated.has(config.path));
419+
assert.equal(configuredRemoved.size, 0);
420+
configuredCreated.clear();
421+
}
422+
423+
function checkConfiguredProjectNotCreatedAndNotDeleted() {
424+
assert.equal(configuredCreated.size, 0);
425+
assert.equal(configuredRemoved.size, 0);
426+
}
427+
428+
function checkConfiguredProjectNotCreatedButDeleted() {
429+
assert.equal(configuredCreated.size, 0);
430+
assert.equal(configuredRemoved.size, 1);
431+
assert.isTrue(configuredRemoved.has(config.path));
432+
configuredRemoved.clear();
433+
}
434+
});
348435
});
349436
}

src/testRunner/unittests/tsserver/projects.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -634,13 +634,17 @@ namespace ts.projectSystem {
634634
path: "/a/main.js",
635635
content: "var y = 1"
636636
};
637+
const f3 = {
638+
path: "/main.js",
639+
content: "var y = 1"
640+
};
637641
const config = {
638642
path: "/a/tsconfig.json",
639643
content: JSON.stringify({
640644
compilerOptions: { allowJs: true }
641645
})
642646
};
643-
const host = createServerHost([f1, f2, config]);
647+
const host = createServerHost([f1, f2, f3, config]);
644648
const projectService = createProjectService(host);
645649
projectService.setHostConfiguration({
646650
extraFileExtensions: [
@@ -652,13 +656,19 @@ namespace ts.projectSystem {
652656
projectService.checkNumberOfProjects({ configuredProjects: 1 });
653657
checkProjectActualFiles(configuredProjectAt(projectService, 0), [f1.path, config.path]);
654658

655-
// Should close configured project with next file open
659+
// Since f2 refers to config file as the default project, it needs to be kept alive
656660
projectService.closeClientFile(f1.path);
657-
658661
projectService.openClientFile(f2.path);
662+
projectService.checkNumberOfProjects({ inferredProjects: 1, configuredProjects: 1 });
663+
assert.isDefined(projectService.configuredProjects.get(config.path));
664+
checkProjectActualFiles(projectService.inferredProjects[0], [f2.path]);
665+
666+
// Should close configured project with next file open
667+
projectService.closeClientFile(f2.path);
668+
projectService.openClientFile(f3.path);
659669
projectService.checkNumberOfProjects({ inferredProjects: 1 });
660670
assert.isUndefined(projectService.configuredProjects.get(config.path));
661-
checkProjectActualFiles(projectService.inferredProjects[0], [f2.path]);
671+
checkProjectActualFiles(projectService.inferredProjects[0], [f3.path]);
662672
});
663673

664674
it("tsconfig script block support", () => {

0 commit comments

Comments
 (0)