Skip to content

Commit 096e1b1

Browse files
authored
Handle error reporting of files when new file is created after its opened in editor (#36271)
* If script info is not attached to the project on which wild card is invoked, update it. * Instead of getting default project before starting error list timer, get it at that time if no project is specified Fixes #35794 * Fix the open File watch triggered setting
1 parent 8e0b091 commit 096e1b1

File tree

7 files changed

+182
-25
lines changed

7 files changed

+182
-25
lines changed

src/server/editorServices.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1110,7 +1110,14 @@ namespace ts.server {
11101110
// don't trigger callback on open, existing files
11111111
if (project.fileIsOpen(fileOrDirectoryPath)) {
11121112
if (project.pendingReload !== ConfigFileProgramReloadLevel.Full) {
1113-
project.openFileWatchTriggered.set(fileOrDirectoryPath, true);
1113+
const info = Debug.assertDefined(this.getScriptInfoForPath(fileOrDirectoryPath));
1114+
if (info.isAttached(project)) {
1115+
project.openFileWatchTriggered.set(fileOrDirectoryPath, true);
1116+
}
1117+
else {
1118+
project.pendingReload = ConfigFileProgramReloadLevel.Partial;
1119+
this.delayUpdateProjectGraphAndEnsureProjectStructureForOpenFiles(project);
1120+
}
11141121
}
11151122
return;
11161123
}

src/server/session.ts

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -719,10 +719,8 @@ namespace ts.server {
719719
this.projectService.logger.info(`got projects updated in background, updating diagnostics for ${openFiles}`);
720720
if (openFiles.length) {
721721
if (!this.suppressDiagnosticEvents && !this.noGetErrOnBackgroundUpdate) {
722-
const checkList = this.createCheckList(openFiles);
723-
724722
// For now only queue error checking for open files. We can change this to include non open files as well
725-
this.errorCheck.startNew(next => this.updateErrorCheck(next, checkList, 100, /*requireOpen*/ true));
723+
this.errorCheck.startNew(next => this.updateErrorCheck(next, openFiles, 100, /*requireOpen*/ true));
726724
}
727725

728726
// Send project changed event
@@ -870,20 +868,37 @@ namespace ts.server {
870868
}
871869

872870
/** It is the caller's responsibility to verify that `!this.suppressDiagnosticEvents`. */
873-
private updateErrorCheck(next: NextStep, checkList: PendingErrorCheck[], ms: number, requireOpen = true) {
871+
private updateErrorCheck(next: NextStep, checkList: readonly string[] | readonly PendingErrorCheck[], ms: number, requireOpen = true) {
874872
Debug.assert(!this.suppressDiagnosticEvents); // Caller's responsibility
875873

876874
const seq = this.changeSeq;
877875
const followMs = Math.min(ms, 200);
878876

879877
let index = 0;
878+
const goNext = () => {
879+
index++;
880+
if (checkList.length > index) {
881+
next.delay(followMs, checkOne);
882+
}
883+
};
880884
const checkOne = () => {
881885
if (this.changeSeq !== seq) {
882886
return;
883887
}
884888

885-
const { fileName, project } = checkList[index];
886-
index++;
889+
let item: string | PendingErrorCheck | undefined = checkList[index];
890+
if (isString(item)) {
891+
// Find out project for the file name
892+
item = this.toPendingErrorCheck(item);
893+
if (!item) {
894+
// Ignore file if there is no project for the file
895+
goNext();
896+
return;
897+
}
898+
}
899+
900+
const { fileName, project } = item;
901+
887902
// Ensure the project is upto date before checking if this file is present in the project
888903
updateProjectIfDirty(project);
889904
if (!project.containsFile(fileName, requireOpen)) {
@@ -901,11 +916,6 @@ namespace ts.server {
901916
return;
902917
}
903918

904-
const goNext = () => {
905-
if (checkList.length > index) {
906-
next.delay(followMs, checkOne);
907-
}
908-
};
909919
if (this.getPreferences(fileName).disableSuggestions) {
910920
goNext();
911921
}
@@ -1727,22 +1737,19 @@ namespace ts.server {
17271737
}
17281738
}
17291739

1730-
private createCheckList(fileNames: string[]): PendingErrorCheck[] {
1731-
return mapDefined<string, PendingErrorCheck>(fileNames, uncheckedFileName => {
1732-
const fileName = toNormalizedPath(uncheckedFileName);
1733-
const project = this.projectService.tryGetDefaultProjectForFile(fileName);
1734-
return project && { fileName, project };
1735-
});
1740+
private toPendingErrorCheck(uncheckedFileName: string): PendingErrorCheck | undefined {
1741+
const fileName = toNormalizedPath(uncheckedFileName);
1742+
const project = this.projectService.tryGetDefaultProjectForFile(fileName);
1743+
return project && { fileName, project };
17361744
}
17371745

17381746
private getDiagnostics(next: NextStep, delay: number, fileNames: string[]): void {
17391747
if (this.suppressDiagnosticEvents) {
17401748
return;
17411749
}
17421750

1743-
const checkList = this.createCheckList(fileNames);
1744-
if (checkList.length > 0) {
1745-
this.updateErrorCheck(next, checkList, delay);
1751+
if (fileNames.length > 0) {
1752+
this.updateErrorCheck(next, fileNames, delay);
17461753
}
17471754
}
17481755

src/testRunner/unittests/tsserver/cancellationToken.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,10 @@ namespace ts.projectSystem {
8484
command: "geterr",
8585
arguments: { files: ["/a/missing"] }
8686
});
87-
// no files - expect 'completed' event
87+
// Queued files
88+
assert.equal(host.getOutput().length, 0, "expected 0 message");
89+
host.checkTimeoutQueueLengthAndRun(1);
90+
// Completed event since file is missing
8891
assert.equal(host.getOutput().length, 1, "expect 1 message");
8992
verifyRequestCompleted(session.getSeq(), 0);
9093
}

src/testRunner/unittests/tsserver/configuredProjects.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -923,6 +923,146 @@ declare var console: {
923923
const service = createProjectService(host);
924924
service.openClientFile(file.path);
925925
});
926+
927+
describe("when creating new file", () => {
928+
const foo: File = {
929+
path: `${tscWatch.projectRoot}/src/foo.ts`,
930+
content: "export function foo() { }"
931+
};
932+
const bar: File = {
933+
path: `${tscWatch.projectRoot}/src/bar.ts`,
934+
content: "export function bar() { }"
935+
};
936+
const config: File = {
937+
path: `${tscWatch.projectRoot}/tsconfig.json`,
938+
content: JSON.stringify({
939+
include: ["./src"]
940+
})
941+
};
942+
const fooBar: File = {
943+
path: `${tscWatch.projectRoot}/src/sub/fooBar.ts`,
944+
content: "export function fooBar() { }"
945+
};
946+
function verifySessionWorker({ withExclude, openFileBeforeCreating, checkProjectBeforeError, checkProjectAfterError, }: VerifySession, errorOnNewFileBeforeOldFile: boolean) {
947+
const host = createServerHost([
948+
foo, bar, libFile, { path: `${tscWatch.projectRoot}/src/sub` },
949+
withExclude ?
950+
{
951+
path: config.path,
952+
content: JSON.stringify({
953+
include: ["./src"],
954+
exclude: ["./src/sub"]
955+
})
956+
} :
957+
config
958+
]);
959+
const session = createSession(host, {
960+
canUseEvents: true
961+
});
962+
session.executeCommandSeq<protocol.OpenRequest>({
963+
command: protocol.CommandTypes.Open,
964+
arguments: {
965+
file: foo.path,
966+
fileContent: foo.content,
967+
projectRootPath: tscWatch.projectRoot
968+
}
969+
});
970+
if (!openFileBeforeCreating) {
971+
host.writeFile(fooBar.path, fooBar.content);
972+
}
973+
session.executeCommandSeq<protocol.OpenRequest>({
974+
command: protocol.CommandTypes.Open,
975+
arguments: {
976+
file: fooBar.path,
977+
fileContent: fooBar.content,
978+
projectRootPath: tscWatch.projectRoot
979+
}
980+
});
981+
if (openFileBeforeCreating) {
982+
host.writeFile(fooBar.path, fooBar.content);
983+
}
984+
const service = session.getProjectService();
985+
checkProjectBeforeError(service);
986+
verifyGetErrRequest({
987+
session,
988+
host,
989+
expected: errorOnNewFileBeforeOldFile ?
990+
[
991+
{ file: fooBar, syntax: [], semantic: [], suggestion: [] },
992+
{ file: foo, syntax: [], semantic: [], suggestion: [] },
993+
] :
994+
[
995+
{ file: foo, syntax: [], semantic: [], suggestion: [] },
996+
{ file: fooBar, syntax: [], semantic: [], suggestion: [] },
997+
],
998+
existingTimeouts: 2
999+
});
1000+
checkProjectAfterError(service);
1001+
}
1002+
interface VerifySession {
1003+
withExclude?: boolean;
1004+
openFileBeforeCreating: boolean;
1005+
checkProjectBeforeError: (service: server.ProjectService) => void;
1006+
checkProjectAfterError: (service: server.ProjectService) => void;
1007+
}
1008+
function verifySession(input: VerifySession) {
1009+
it("when error on new file are asked before old one", () => {
1010+
verifySessionWorker(input, /*errorOnNewFileBeforeOldFile*/ true);
1011+
});
1012+
1013+
it("when error on new file are asked after old one", () => {
1014+
verifySessionWorker(input, /*errorOnNewFileBeforeOldFile*/ false);
1015+
});
1016+
}
1017+
function checkFooBarInInferredProject(service: server.ProjectService) {
1018+
checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 });
1019+
checkProjectActualFiles(service.configuredProjects.get(config.path)!, [foo.path, bar.path, libFile.path, config.path]);
1020+
checkProjectActualFiles(service.inferredProjects[0], [fooBar.path, libFile.path]);
1021+
}
1022+
function checkFooBarInConfiguredProject(service: server.ProjectService) {
1023+
checkNumberOfProjects(service, { configuredProjects: 1 });
1024+
checkProjectActualFiles(service.configuredProjects.get(config.path)!, [foo.path, bar.path, fooBar.path, libFile.path, config.path]);
1025+
}
1026+
describe("when new file creation directory watcher is invoked before file is opened in editor", () => {
1027+
verifySession({
1028+
openFileBeforeCreating: false,
1029+
checkProjectBeforeError: checkFooBarInConfiguredProject,
1030+
checkProjectAfterError: checkFooBarInConfiguredProject
1031+
});
1032+
describe("when new file is excluded from config", () => {
1033+
verifySession({
1034+
withExclude: true,
1035+
openFileBeforeCreating: false,
1036+
checkProjectBeforeError: checkFooBarInInferredProject,
1037+
checkProjectAfterError: checkFooBarInInferredProject
1038+
});
1039+
});
1040+
});
1041+
1042+
describe("when new file creation directory watcher is invoked after file is opened in editor", () => {
1043+
verifySession({
1044+
openFileBeforeCreating: true,
1045+
checkProjectBeforeError: checkFooBarInInferredProject,
1046+
checkProjectAfterError: service => {
1047+
// Both projects exist but fooBar is in configured project after the update
1048+
// Inferred project is yet to be updated so still has fooBar
1049+
checkNumberOfProjects(service, { configuredProjects: 1, inferredProjects: 1 });
1050+
checkProjectActualFiles(service.configuredProjects.get(config.path)!, [foo.path, bar.path, fooBar.path, libFile.path, config.path]);
1051+
checkProjectActualFiles(service.inferredProjects[0], [fooBar.path, libFile.path]);
1052+
assert.isTrue(service.inferredProjects[0].dirty);
1053+
assert.equal(service.inferredProjects[0].getRootFilesMap().size, 0);
1054+
}
1055+
});
1056+
describe("when new file is excluded from config", () => {
1057+
verifySession({
1058+
withExclude: true,
1059+
openFileBeforeCreating: true,
1060+
checkProjectBeforeError: checkFooBarInInferredProject,
1061+
checkProjectAfterError: checkFooBarInInferredProject
1062+
});
1063+
});
1064+
});
1065+
});
9261066
});
9271067

9281068
describe("unittests:: tsserver:: ConfiguredProjects:: non-existing directories listed in config file input array", () => {

src/testRunner/unittests/tsserver/projectErrors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -382,7 +382,7 @@ namespace ts.projectSystem {
382382
}
383383
});
384384

385-
host.runQueuedImmediateCallbacks();
385+
host.checkTimeoutQueueLengthAndRun(1);
386386
assert.isFalse(hasError());
387387
checkCompleteEvent(session, 1, expectedSequenceId);
388388
session.clearMessages();

src/testRunner/unittests/tsserver/projects.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1508,7 +1508,7 @@ var x = 10;`
15081508
host,
15091509
expected: [
15101510
{ file: fileB, syntax: [], semantic: [], suggestion: [] },
1511-
{ file: fileSubA },
1511+
{ file: fileSubA, syntax: [], semantic: [], suggestion: [] },
15121512
],
15131513
existingTimeouts: 2,
15141514
onErrEvent: () => assert.isFalse(hasErrorMsg())

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9483,7 +9483,7 @@ declare namespace ts.server {
94839483
private getCompileOnSaveAffectedFileList;
94849484
private emitFile;
94859485
private getSignatureHelpItems;
9486-
private createCheckList;
9486+
private toPendingErrorCheck;
94879487
private getDiagnostics;
94889488
private change;
94899489
private reload;

0 commit comments

Comments
 (0)