Skip to content

Commit ddbd654

Browse files
authored
Merge pull request microsoft#19730 from Microsoft/fileNotOnDisk
Handles script infos that dont exist on the disk and are opened with non-rooted disk path
2 parents c016f5b + 163e40c commit ddbd654

6 files changed

Lines changed: 192 additions & 67 deletions

File tree

src/harness/unittests/tsserverProjectSystem.ts

Lines changed: 125 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,50 @@ namespace ts.projectSystem {
437437
verifyDiagnostics(actual, []);
438438
}
439439

440+
function assertEvent(actualOutput: string, expectedEvent: protocol.Event, host: TestServerHost) {
441+
assert.equal(actualOutput, server.formatMessage(expectedEvent, nullLogger, Utils.byteLength, host.newLine));
442+
}
443+
444+
function checkErrorMessage(host: TestServerHost, eventName: "syntaxDiag" | "semanticDiag", diagnostics: protocol.DiagnosticEventBody) {
445+
const outputs = host.getOutput();
446+
assert.isTrue(outputs.length >= 1, outputs.toString());
447+
const event: protocol.Event = {
448+
seq: 0,
449+
type: "event",
450+
event: eventName,
451+
body: diagnostics
452+
};
453+
assertEvent(outputs[0], event, host);
454+
}
455+
456+
function checkCompleteEvent(host: TestServerHost, numberOfCurrentEvents: number, expectedSequenceId: number) {
457+
const outputs = host.getOutput();
458+
assert.equal(outputs.length, numberOfCurrentEvents, outputs.toString());
459+
const event: protocol.RequestCompletedEvent = {
460+
seq: 0,
461+
type: "event",
462+
event: "requestCompleted",
463+
body: {
464+
request_seq: expectedSequenceId
465+
}
466+
};
467+
assertEvent(outputs[numberOfCurrentEvents - 1], event, host);
468+
}
469+
470+
function checkProjectUpdatedInBackgroundEvent(host: TestServerHost, openFiles: string[]) {
471+
const outputs = host.getOutput();
472+
assert.equal(outputs.length, 1, outputs.toString());
473+
const event: protocol.ProjectsUpdatedInBackgroundEvent = {
474+
seq: 0,
475+
type: "event",
476+
event: "projectsUpdatedInBackground",
477+
body: {
478+
openFiles
479+
}
480+
};
481+
assertEvent(outputs[0], event, host);
482+
}
483+
440484
describe("tsserverProjectSystem", () => {
441485
const commonFile1: FileOrFolder = {
442486
path: "/a/b/commonFile1.ts",
@@ -2744,6 +2788,87 @@ namespace ts.projectSystem {
27442788
const project = projectService.findProject(corruptedConfig.path);
27452789
checkProjectRootFiles(project, [file1.path]);
27462790
});
2791+
2792+
describe("when opening new file that doesnt exist on disk yet", () => {
2793+
function verifyNonExistentFile(useProjectRoot: boolean) {
2794+
const host = createServerHost([libFile]);
2795+
let hasError = false;
2796+
const errLogger: server.Logger = {
2797+
close: noop,
2798+
hasLevel: () => true,
2799+
loggingEnabled: () => true,
2800+
perftrc: noop,
2801+
info: noop,
2802+
msg: (_s, type) => {
2803+
if (type === server.Msg.Err) {
2804+
hasError = true;
2805+
}
2806+
},
2807+
startGroup: noop,
2808+
endGroup: noop,
2809+
getLogFileName: (): string => undefined
2810+
};
2811+
const session = createSession(host, { canUseEvents: true, logger: errLogger, useInferredProjectPerProjectRoot: true });
2812+
2813+
const folderPath = "/user/someuser/projects/someFolder";
2814+
const projectService = session.getProjectService();
2815+
const untitledFile = "untitled:Untitled-1";
2816+
session.executeCommandSeq<protocol.OpenRequest>({
2817+
command: server.CommandNames.Open,
2818+
arguments: {
2819+
file: untitledFile,
2820+
fileContent: "",
2821+
scriptKindName: "JS",
2822+
projectRootPath: useProjectRoot ? folderPath : undefined
2823+
}
2824+
});
2825+
checkNumberOfProjects(projectService, { inferredProjects: 1 });
2826+
const infoForUntitledAtProjectRoot = projectService.getScriptInfoForPath(`${folderPath.toLowerCase()}/${untitledFile.toLowerCase()}` as Path);
2827+
const infoForUnitiledAtRoot = projectService.getScriptInfoForPath(`/${untitledFile.toLowerCase()}` as Path);
2828+
if (useProjectRoot) {
2829+
assert.isDefined(infoForUntitledAtProjectRoot);
2830+
assert.isUndefined(infoForUnitiledAtRoot);
2831+
}
2832+
else {
2833+
assert.isDefined(infoForUnitiledAtRoot);
2834+
assert.isUndefined(infoForUntitledAtProjectRoot);
2835+
}
2836+
host.checkTimeoutQueueLength(2);
2837+
2838+
const newTimeoutId = host.getNextTimeoutId();
2839+
const expectedSequenceId = session.getNextSeq();
2840+
session.executeCommandSeq<protocol.GeterrRequest>({
2841+
command: server.CommandNames.Geterr,
2842+
arguments: {
2843+
delay: 0,
2844+
files: [untitledFile]
2845+
}
2846+
});
2847+
host.checkTimeoutQueueLength(3);
2848+
2849+
// Run the last one = get error request
2850+
host.runQueuedTimeoutCallbacks(newTimeoutId);
2851+
2852+
assert.isFalse(hasError);
2853+
host.checkTimeoutQueueLength(2);
2854+
checkErrorMessage(host, "syntaxDiag", { file: untitledFile, diagnostics: [] });
2855+
host.clearOutput();
2856+
2857+
host.runQueuedImmediateCallbacks();
2858+
assert.isFalse(hasError);
2859+
checkErrorMessage(host, "semanticDiag", { file: untitledFile, diagnostics: [] });
2860+
2861+
checkCompleteEvent(host, 2, expectedSequenceId);
2862+
}
2863+
2864+
it("has projectRoot", () => {
2865+
verifyNonExistentFile(/*useProjectRoot*/ true);
2866+
});
2867+
2868+
it("does not have projectRoot", () => {
2869+
verifyNonExistentFile(/*useProjectRoot*/ false);
2870+
});
2871+
});
27472872
});
27482873

27492874
describe("autoDiscovery", () => {
@@ -3446,50 +3571,6 @@ namespace ts.projectSystem {
34463571
verifyNoDiagnostics(diags);
34473572
});
34483573

3449-
function assertEvent(actualOutput: string, expectedEvent: protocol.Event, host: TestServerHost) {
3450-
assert.equal(actualOutput, server.formatMessage(expectedEvent, nullLogger, Utils.byteLength, host.newLine));
3451-
}
3452-
3453-
function checkErrorMessage(host: TestServerHost, eventName: "syntaxDiag" | "semanticDiag", diagnostics: protocol.DiagnosticEventBody) {
3454-
const outputs = host.getOutput();
3455-
assert.isTrue(outputs.length >= 1, outputs.toString());
3456-
const event: protocol.Event = {
3457-
seq: 0,
3458-
type: "event",
3459-
event: eventName,
3460-
body: diagnostics
3461-
};
3462-
assertEvent(outputs[0], event, host);
3463-
}
3464-
3465-
function checkCompleteEvent(host: TestServerHost, numberOfCurrentEvents: number, expectedSequenceId: number) {
3466-
const outputs = host.getOutput();
3467-
assert.equal(outputs.length, numberOfCurrentEvents, outputs.toString());
3468-
const event: protocol.RequestCompletedEvent = {
3469-
seq: 0,
3470-
type: "event",
3471-
event: "requestCompleted",
3472-
body: {
3473-
request_seq: expectedSequenceId
3474-
}
3475-
};
3476-
assertEvent(outputs[numberOfCurrentEvents - 1], event, host);
3477-
}
3478-
3479-
function checkProjectUpdatedInBackgroundEvent(host: TestServerHost, openFiles: string[]) {
3480-
const outputs = host.getOutput();
3481-
assert.equal(outputs.length, 1, outputs.toString());
3482-
const event: protocol.ProjectsUpdatedInBackgroundEvent = {
3483-
seq: 0,
3484-
type: "event",
3485-
event: "projectsUpdatedInBackground",
3486-
body: {
3487-
openFiles
3488-
}
3489-
};
3490-
assertEvent(outputs[0], event, host);
3491-
}
3492-
34933574
it("npm install @types works", () => {
34943575
const folderPath = "/a/b/projects/temp";
34953576
const file1: FileOrFolder = {

src/harness/virtualFileSystemWithWatch.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,10 @@ interface Array<T> {}`
182182
private map: TimeOutCallback[] = [];
183183
private nextId = 1;
184184

185+
getNextId() {
186+
return this.nextId;
187+
}
188+
185189
register(cb: (...args: any[]) => void, args: any[]) {
186190
const timeoutId = this.nextId;
187191
this.nextId++;
@@ -203,7 +207,13 @@ interface Array<T> {}`
203207
return n;
204208
}
205209

206-
invoke() {
210+
invoke(invokeKey?: number) {
211+
if (invokeKey) {
212+
this.map[invokeKey]();
213+
delete this.map[invokeKey];
214+
return;
215+
}
216+
207217
// Note: invoking a callback may result in new callbacks been queued,
208218
// so do not clear the entire callback list regardless. Only remove the
209219
// ones we have invoked.
@@ -553,6 +563,10 @@ interface Array<T> {}`
553563
return this.timeoutCallbacks.register(callback, args);
554564
}
555565

566+
getNextTimeoutId() {
567+
return this.timeoutCallbacks.getNextId();
568+
}
569+
556570
clearTimeout(timeoutId: any): void {
557571
this.timeoutCallbacks.unregister(timeoutId);
558572
}
@@ -567,9 +581,9 @@ interface Array<T> {}`
567581
assert.equal(callbacksCount, expected, `expected ${expected} timeout callbacks queued but found ${callbacksCount}.`);
568582
}
569583

570-
runQueuedTimeoutCallbacks() {
584+
runQueuedTimeoutCallbacks(timeoutId?: number) {
571585
try {
572-
this.timeoutCallbacks.invoke();
586+
this.timeoutCallbacks.invoke(timeoutId);
573587
}
574588
catch (e) {
575589
if (e.message === this.existMessage) {

src/server/editorServices.ts

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,10 @@ namespace ts.server {
355355
* Open files: with value being project root path, and key being Path of the file that is open
356356
*/
357357
readonly openFiles = createMap<NormalizedPath>();
358+
/**
359+
* Map of open files that are opened without complete path but have projectRoot as current directory
360+
*/
361+
private readonly openFilesWithNonRootedDiskPath = createMap<ScriptInfo>();
358362

359363
private compilerOptionsForInferredProjects: CompilerOptions;
360364
private compilerOptionsForInferredProjectsPerProjectRoot = createMap<CompilerOptions>();
@@ -932,12 +936,16 @@ namespace ts.server {
932936
// Closing file should trigger re-reading the file content from disk. This is
933937
// because the user may chose to discard the buffer content before saving
934938
// to the disk, and the server's version of the file can be out of sync.
935-
info.close();
939+
const fileExists = this.host.fileExists(info.fileName);
940+
info.close(fileExists);
936941
this.stopWatchingConfigFilesForClosedScriptInfo(info);
937942

938943
this.openFiles.delete(info.path);
944+
const canonicalFileName = this.toCanonicalFileName(info.fileName);
945+
if (this.openFilesWithNonRootedDiskPath.get(canonicalFileName) === info) {
946+
this.openFilesWithNonRootedDiskPath.delete(canonicalFileName);
947+
}
939948

940-
const fileExists = this.host.fileExists(info.fileName);
941949

942950
// collect all projects that should be removed
943951
let projectsToRemove: Project[];
@@ -1537,7 +1545,7 @@ namespace ts.server {
15371545
else {
15381546
const scriptKind = propertyReader.getScriptKind(f, this.hostConfiguration.extraFileExtensions);
15391547
const hasMixedContent = propertyReader.hasMixedContent(f, this.hostConfiguration.extraFileExtensions);
1540-
scriptInfo = this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(normalizedPath, scriptKind, hasMixedContent, project.directoryStructureHost);
1548+
scriptInfo = this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(normalizedPath, project.currentDirectory, scriptKind, hasMixedContent, project.directoryStructureHost);
15411549
path = scriptInfo.path;
15421550
// If this script info is not already a root add it
15431551
if (!project.isRoot(scriptInfo)) {
@@ -1691,9 +1699,9 @@ namespace ts.server {
16911699
}
16921700

16931701
/*@internal*/
1694-
getOrCreateScriptInfoNotOpenedByClient(uncheckedFileName: string, hostToQueryFileExistsOn: DirectoryStructureHost) {
1702+
getOrCreateScriptInfoNotOpenedByClient(uncheckedFileName: string, currentDirectory: string, hostToQueryFileExistsOn: DirectoryStructureHost) {
16951703
return this.getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(
1696-
toNormalizedPath(uncheckedFileName), /*scriptKind*/ undefined,
1704+
toNormalizedPath(uncheckedFileName), currentDirectory, /*scriptKind*/ undefined,
16971705
/*hasMixedContent*/ undefined, hostToQueryFileExistsOn
16981706
);
16991707
}
@@ -1724,20 +1732,26 @@ namespace ts.server {
17241732
}
17251733

17261734
/*@internal*/
1727-
getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName: NormalizedPath, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost) {
1728-
return this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent, hostToQueryFileExistsOn);
1735+
getOrCreateScriptInfoNotOpenedByClientForNormalizedPath(fileName: NormalizedPath, currentDirectory: string, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined, hostToQueryFileExistsOn: DirectoryStructureHost | undefined) {
1736+
return this.getOrCreateScriptInfoWorker(fileName, currentDirectory, /*openedByClient*/ false, /*fileContent*/ undefined, scriptKind, hasMixedContent, hostToQueryFileExistsOn);
17291737
}
17301738

17311739
/*@internal*/
1732-
getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName: NormalizedPath, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost) {
1733-
return this.getOrCreateScriptInfoForNormalizedPath(fileName, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent, hostToQueryFileExistsOn);
1740+
getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName: NormalizedPath, currentDirectory: string, fileContent: string | undefined, scriptKind: ScriptKind | undefined, hasMixedContent: boolean | undefined) {
1741+
return this.getOrCreateScriptInfoWorker(fileName, currentDirectory, /*openedByClient*/ true, fileContent, scriptKind, hasMixedContent);
17341742
}
17351743

17361744
getOrCreateScriptInfoForNormalizedPath(fileName: NormalizedPath, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost) {
1745+
return this.getOrCreateScriptInfoWorker(fileName, this.currentDirectory, openedByClient, fileContent, scriptKind, hasMixedContent, hostToQueryFileExistsOn);
1746+
}
1747+
1748+
private getOrCreateScriptInfoWorker(fileName: NormalizedPath, currentDirectory: string, openedByClient: boolean, fileContent?: string, scriptKind?: ScriptKind, hasMixedContent?: boolean, hostToQueryFileExistsOn?: DirectoryStructureHost) {
17371749
Debug.assert(fileContent === undefined || openedByClient, "ScriptInfo needs to be opened by client to be able to set its user defined content");
1738-
const path = normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName);
1750+
const path = normalizedPathToPath(fileName, currentDirectory, this.toCanonicalFileName);
17391751
let info = this.getScriptInfoForPath(path);
17401752
if (!info) {
1753+
Debug.assert(isRootedDiskPath(fileName) || openedByClient, "Script info with relative file name can only be open script info");
1754+
Debug.assert(!isRootedDiskPath(fileName) || this.currentDirectory === currentDirectory || !this.openFilesWithNonRootedDiskPath.has(this.toCanonicalFileName(fileName)), "Open script files with non rooted disk path opened with current directory context cannot have same canonical names");
17411755
const isDynamic = isDynamicFileName(fileName);
17421756
// If the file is not opened by client and the file doesnot exist on the disk, return
17431757
if (!openedByClient && !isDynamic && !(hostToQueryFileExistsOn || this.host).fileExists(fileName)) {
@@ -1748,6 +1762,10 @@ namespace ts.server {
17481762
if (!openedByClient) {
17491763
this.watchClosedScriptInfo(info);
17501764
}
1765+
else if (!isRootedDiskPath(fileName) && currentDirectory !== this.currentDirectory) {
1766+
// File that is opened by user but isn't rooted disk path
1767+
this.openFilesWithNonRootedDiskPath.set(this.toCanonicalFileName(fileName), info);
1768+
}
17511769
}
17521770
if (openedByClient && !info.isScriptOpen()) {
17531771
// Opening closed script info
@@ -1764,8 +1782,12 @@ namespace ts.server {
17641782
return info;
17651783
}
17661784

1785+
/**
1786+
* This gets the script info for the normalized path. If the path is not rooted disk path then the open script info with project root context is preferred
1787+
*/
17671788
getScriptInfoForNormalizedPath(fileName: NormalizedPath) {
1768-
return this.getScriptInfoForPath(normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName));
1789+
return !isRootedDiskPath(fileName) && this.openFilesWithNonRootedDiskPath.get(this.toCanonicalFileName(fileName)) ||
1790+
this.getScriptInfoForPath(normalizedPathToPath(fileName, this.currentDirectory, this.toCanonicalFileName));
17691791
}
17701792

17711793
getScriptInfoForPath(fileName: Path) {
@@ -1950,7 +1972,7 @@ namespace ts.server {
19501972
let sendConfigFileDiagEvent = false;
19511973
let configFileErrors: ReadonlyArray<Diagnostic>;
19521974

1953-
const info = this.getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName, fileContent, scriptKind, hasMixedContent);
1975+
const info = this.getOrCreateScriptInfoOpenedByClientForNormalizedPath(fileName, projectRootPath ? this.getNormalizedAbsolutePath(projectRootPath) : this.currentDirectory, fileContent, scriptKind, hasMixedContent);
19541976
let project: ConfiguredProject | ExternalProject = this.findContainingExternalProject(fileName);
19551977
if (!project) {
19561978
configFileName = this.getConfigFileNameForFile(info, projectRootPath);

0 commit comments

Comments
 (0)