Skip to content

Commit 8a9cc94

Browse files
committed
Support go-to-definition for non-existent files
1 parent 5a039af commit 8a9cc94

File tree

5 files changed

+53
-13
lines changed

5 files changed

+53
-13
lines changed

src/harness/client.ts

+3
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,9 @@ namespace ts.server {
527527
private decodeSpan(span: protocol.TextSpan & { file: string }): TextSpan;
528528
private decodeSpan(span: protocol.TextSpan, fileName: string, lineMap?: number[]): TextSpan;
529529
private decodeSpan(span: protocol.TextSpan & { file: string }, fileName?: string, lineMap?: number[]): TextSpan {
530+
if (span.start.line === 1 && span.start.offset === 1 && span.end.line === 1 && span.end.offset === 1) {
531+
return { start: 0, length: 0 };
532+
}
530533
fileName = fileName || span.file;
531534
lineMap = lineMap || this.getLineMap(fileName);
532535
return createTextSpanFromBounds(

src/harness/fourslashImpl.ts

+13-12
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ namespace FourSlash {
688688
this.verifyGoToXWorker(toArray(endMarker), () => this.getGoToDefinition());
689689
}
690690

691-
public verifyGoToDefinition(arg0: any, endMarkerNames?: ArrayOrSingle<string>) {
691+
public verifyGoToDefinition(arg0: any, endMarkerNames?: ArrayOrSingle<string> | { file: string }) {
692692
this.verifyGoToX(arg0, endMarkerNames, () => this.getGoToDefinitionAndBoundSpan());
693693
}
694694

@@ -705,7 +705,7 @@ namespace FourSlash {
705705
this.languageService.getTypeDefinitionAtPosition(this.activeFile.fileName, this.currentCaretPosition));
706706
}
707707

708-
private verifyGoToX(arg0: any, endMarkerNames: ArrayOrSingle<string> | undefined, getDefs: () => readonly ts.DefinitionInfo[] | ts.DefinitionInfoAndBoundSpan | undefined) {
708+
private verifyGoToX(arg0: any, endMarkerNames: ArrayOrSingle<string> | { file: string } | undefined, getDefs: () => readonly ts.DefinitionInfo[] | ts.DefinitionInfoAndBoundSpan | undefined) {
709709
if (endMarkerNames) {
710710
this.verifyGoToXPlain(arg0, endMarkerNames, getDefs);
711711
}
@@ -725,7 +725,7 @@ namespace FourSlash {
725725
}
726726
}
727727

728-
private verifyGoToXPlain(startMarkerNames: ArrayOrSingle<string>, endMarkerNames: ArrayOrSingle<string>, getDefs: () => readonly ts.DefinitionInfo[] | ts.DefinitionInfoAndBoundSpan | undefined) {
728+
private verifyGoToXPlain(startMarkerNames: ArrayOrSingle<string>, endMarkerNames: ArrayOrSingle<string> | { file: string }, getDefs: () => readonly ts.DefinitionInfo[] | ts.DefinitionInfoAndBoundSpan | undefined) {
729729
for (const start of toArray(startMarkerNames)) {
730730
this.verifyGoToXSingle(start, endMarkerNames, getDefs);
731731
}
@@ -737,12 +737,12 @@ namespace FourSlash {
737737
}
738738
}
739739

740-
private verifyGoToXSingle(startMarkerName: string, endMarkerNames: ArrayOrSingle<string>, getDefs: () => readonly ts.DefinitionInfo[] | ts.DefinitionInfoAndBoundSpan | undefined) {
740+
private verifyGoToXSingle(startMarkerName: string, endMarkerNames: ArrayOrSingle<string> | { file: string }, getDefs: () => readonly ts.DefinitionInfo[] | ts.DefinitionInfoAndBoundSpan | undefined) {
741741
this.goToMarker(startMarkerName);
742742
this.verifyGoToXWorker(toArray(endMarkerNames), getDefs, startMarkerName);
743743
}
744744

745-
private verifyGoToXWorker(endMarkers: readonly string[], getDefs: () => readonly ts.DefinitionInfo[] | ts.DefinitionInfoAndBoundSpan | undefined, startMarkerName?: string) {
745+
private verifyGoToXWorker(endMarkers: readonly (string | { file: string })[], getDefs: () => readonly ts.DefinitionInfo[] | ts.DefinitionInfoAndBoundSpan | undefined, startMarkerName?: string) {
746746
const defs = getDefs();
747747
let definitions: readonly ts.DefinitionInfo[];
748748
let testName: string;
@@ -762,21 +762,22 @@ namespace FourSlash {
762762
this.raiseError(`${testName} failed - expected to find ${endMarkers.length} definitions but got ${definitions.length}`);
763763
}
764764

765-
ts.zipWith(endMarkers, definitions, (endMarker, definition, i) => {
766-
const marker = this.getMarkerByName(endMarker);
767-
if (ts.comparePaths(marker.fileName, definition.fileName, /*ignoreCase*/ true) !== ts.Comparison.EqualTo || marker.position !== definition.textSpan.start) {
768-
const filesToDisplay = ts.deduplicate([marker.fileName, definition.fileName], ts.equateValues);
769-
const markers = [{ text: "EXPECTED", fileName: marker.fileName, position: marker.position }, { text: "ACTUAL", fileName: definition.fileName, position: definition.textSpan.start }];
765+
ts.zipWith(endMarkers, definitions, (endMarkerOrFileResult, definition, i) => {
766+
const expectedFileName = typeof endMarkerOrFileResult === "string" ? this.getMarkerByName(endMarkerOrFileResult).fileName : endMarkerOrFileResult.file;
767+
const expectedPosition = typeof endMarkerOrFileResult === "string" ? this.getMarkerByName(endMarkerOrFileResult).position : 0;
768+
if (ts.comparePaths(expectedFileName, definition.fileName, /*ignoreCase*/ true) !== ts.Comparison.EqualTo || expectedPosition !== definition.textSpan.start) {
769+
const filesToDisplay = ts.deduplicate([expectedFileName, definition.fileName], ts.equateValues);
770+
const markers = [{ text: "EXPECTED", fileName: expectedFileName, position: expectedPosition }, { text: "ACTUAL", fileName: definition.fileName, position: definition.textSpan.start }];
770771
const text = filesToDisplay.map(fileName => {
771772
const markersToRender = markers.filter(m => m.fileName === fileName).sort((a, b) => b.position - a.position);
772-
let fileContent = this.getFileContent(fileName);
773+
let fileContent = this.tryGetFileContent(fileName) || "";
773774
for (const marker of markersToRender) {
774775
fileContent = fileContent.slice(0, marker.position) + `\x1b[1;4m/*${marker.text}*/\x1b[0;31m` + fileContent.slice(marker.position);
775776
}
776777
return `// @Filename: ${fileName}\n${fileContent}`;
777778
}).join("\n\n");
778779

779-
this.raiseError(`${testName} failed for definition ${endMarker} (${i}): expected ${marker.fileName} at ${marker.position}, got ${definition.fileName} at ${definition.textSpan.start}\n\n${text}\n`);
780+
this.raiseError(`${testName} failed for definition ${endMarkerOrFileResult} (${i}): expected ${expectedFileName} at ${expectedPosition}, got ${definition.fileName} at ${definition.textSpan.start}\n\n${text}\n`);
780781
}
781782
});
782783
}

src/services/services.ts

+12-1
Original file line numberDiff line numberDiff line change
@@ -2492,6 +2492,17 @@ namespace ts {
24922492
return refactor.getEditsForRefactor(getRefactorContext(file, positionOrRange, preferences, formatOptions), refactorName, actionName);
24932493
}
24942494

2495+
function toLineColumnOffset(fileName: string, position: number): LineAndCharacter {
2496+
// Go to Definition supports returning a zero-length span at position 0 for
2497+
// non-existent files. We need to special-case the conversion of position 0
2498+
// to avoid a crash trying to get the text for that file, since this function
2499+
// otherwise assumes that 'fileName' is the name of a file that exists.
2500+
if (position === 0) {
2501+
return { line: 0, character: 0 };
2502+
}
2503+
return sourceMapper.toLineColumnOffset(fileName, position);
2504+
}
2505+
24952506
function prepareCallHierarchy(fileName: string, position: number): CallHierarchyItem | CallHierarchyItem[] | undefined {
24962507
synchronizeHostData();
24972508
const declarations = CallHierarchy.resolveCallHierarchyDeclaration(program, getTouchingPropertyName(getValidSourceFile(fileName), position));
@@ -2567,7 +2578,7 @@ namespace ts {
25672578
getAutoImportProvider,
25682579
getApplicableRefactors,
25692580
getEditsForRefactor,
2570-
toLineColumnOffset: sourceMapper.toLineColumnOffset,
2581+
toLineColumnOffset,
25712582
getSourceMapper: () => sourceMapper,
25722583
clearSourceMapperCache: () => sourceMapper.clearCache(),
25732584
prepareCallHierarchy,

tests/cases/fourslash/fourslash.ts

+1
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,7 @@ declare namespace FourSlashInterface {
278278
* `verify.goToDefinition(["a", "aa"], "b");` verifies that markers "a" and "aa" have the same definition "b".
279279
* `verify.goToDefinition("a", ["b", "bb"]);` verifies that "a" has multiple definitions available.
280280
*/
281+
goToDefinition(startMarkerNames: ArrayOrSingle<string>, fileResult: { file: string }): void;
281282
goToDefinition(startMarkerNames: ArrayOrSingle<string>, endMarkerNames: ArrayOrSingle<string>): void;
282283
goToDefinition(startMarkerNames: ArrayOrSingle<string>, endMarkerNames: ArrayOrSingle<string>, range: Range): void;
283284
/** Performs `goToDefinition` for each pair. */
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/// <reference path="../fourslash.ts" />
2+
3+
// @filename: /scriptThing.ts
4+
//// /*1d*/console.log("woooo side effects")
5+
6+
// @filename: /stylez.css
7+
//// /*2d*/div {
8+
//// color: magenta;
9+
//// }
10+
11+
// @filename: /moduleThing.ts
12+
13+
// not a module, but we should let you jump to it.
14+
//// import [|/*1*/"./scriptThing"|];
15+
16+
// not JS/TS, but if we can, you should be able to jump to it.
17+
//// import [|/*2*/"./stylez.css"|];
18+
19+
// does not exist, but should return a response to it anyway so an editor can create it.
20+
//// import [|/*3*/"./foo.txt"|];
21+
22+
verify.goToDefinition("1", "1d");
23+
verify.goToDefinition("2", "2d");
24+
verify.goToDefinition("3", { file: "/foo.txt" });

0 commit comments

Comments
 (0)