Skip to content

Commit 50d243e

Browse files
authored
Merge pull request #10482 from Microsoft/go_to_implementation_pr
Go to Implementation
2 parents 98a3fc5 + 29d85cd commit 50d243e

File tree

67 files changed

+1609
-38
lines changed

Some content is hidden

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

67 files changed

+1609
-38
lines changed

Diff for: Jakefile.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ var servicesSources = [
129129
"documentRegistry.ts",
130130
"findAllReferences.ts",
131131
"goToDefinition.ts",
132+
"goToImplementation.ts",
132133
"jsDoc.ts",
133134
"jsTyping.ts",
134135
"navigateTo.ts",
@@ -789,7 +790,7 @@ function cleanTestDirs() {
789790

790791
// used to pass data from jake command line directly to run.js
791792
function writeTestConfigFile(tests, light, taskConfigsFolder, workerCount, stackTraceLimit) {
792-
var testConfigContents = JSON.stringify({
793+
var testConfigContents = JSON.stringify({
793794
test: tests ? [tests] : undefined,
794795
light: light,
795796
workerCount: workerCount,

Diff for: src/harness/fourslash.ts

+103
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ namespace FourSlash {
8888
marker?: Marker;
8989
}
9090

91+
interface ImplementationLocationInformation extends ts.ImplementationLocation {
92+
matched?: boolean;
93+
}
94+
9195
export interface TextSpan {
9296
start: number;
9397
end: number;
@@ -1699,6 +1703,17 @@ namespace FourSlash {
16991703
assertFn(actualCount, expectedCount, this.messageAtLastKnownMarker("Type definitions Count"));
17001704
}
17011705

1706+
public verifyImplementationListIsEmpty(negative: boolean) {
1707+
const implementations = this.languageService.getImplementationAtPosition(this.activeFile.fileName, this.currentCaretPosition);
1708+
1709+
if (negative) {
1710+
assert.isTrue(implementations && implementations.length > 0, "Expected at least one implementation but got 0");
1711+
}
1712+
else {
1713+
assert.isUndefined(implementations, "Expected implementation list to be empty but implementations returned");
1714+
}
1715+
}
1716+
17021717
public verifyGoToDefinitionName(expectedName: string, expectedContainerName: string) {
17031718
const definitions = this.languageService.getDefinitionAtPosition(this.activeFile.fileName, this.currentCaretPosition);
17041719
const actualDefinitionName = definitions && definitions.length ? definitions[0].name : "";
@@ -1707,6 +1722,82 @@ namespace FourSlash {
17071722
assert.equal(actualDefinitionContainerName, expectedContainerName, this.messageAtLastKnownMarker("Definition Info Container Name"));
17081723
}
17091724

1725+
public goToImplementation() {
1726+
const implementations = this.languageService.getImplementationAtPosition(this.activeFile.fileName, this.currentCaretPosition);
1727+
if (!implementations || !implementations.length) {
1728+
this.raiseError("goToImplementation failed - expected to find at least one implementation location but got 0");
1729+
}
1730+
if (implementations.length > 1) {
1731+
this.raiseError(`goToImplementation failed - more than 1 implementation returned (${implementations.length})`);
1732+
}
1733+
1734+
const implementation = implementations[0];
1735+
this.openFile(implementation.fileName);
1736+
this.currentCaretPosition = implementation.textSpan.start;
1737+
}
1738+
1739+
public verifyRangesInImplementationList(markerName: string) {
1740+
this.goToMarker(markerName);
1741+
const implementations: ImplementationLocationInformation[] = this.languageService.getImplementationAtPosition(this.activeFile.fileName, this.currentCaretPosition);
1742+
if (!implementations || !implementations.length) {
1743+
this.raiseError("verifyRangesInImplementationList failed - expected to find at least one implementation location but got 0");
1744+
}
1745+
1746+
for (let i = 0; i < implementations.length; i++) {
1747+
for (let j = 0; j < implementations.length; j++) {
1748+
if (i !== j && implementationsAreEqual(implementations[i], implementations[j])) {
1749+
const { textSpan, fileName } = implementations[i];
1750+
const end = textSpan.start + textSpan.length;
1751+
this.raiseError(`Duplicate implementations returned for range (${textSpan.start}, ${end}) in ${fileName}`);
1752+
}
1753+
}
1754+
}
1755+
1756+
const ranges = this.getRanges();
1757+
1758+
if (!ranges || !ranges.length) {
1759+
this.raiseError("verifyRangesInImplementationList failed - expected to find at least one range in test source");
1760+
}
1761+
1762+
const unsatisfiedRanges: Range[] = [];
1763+
1764+
for (const range of ranges) {
1765+
const length = range.end - range.start;
1766+
const matchingImpl = ts.find(implementations, impl =>
1767+
range.fileName === impl.fileName && range.start === impl.textSpan.start && length === impl.textSpan.length);
1768+
if (matchingImpl) {
1769+
matchingImpl.matched = true;
1770+
}
1771+
else {
1772+
unsatisfiedRanges.push(range);
1773+
}
1774+
}
1775+
1776+
const unmatchedImplementations = implementations.filter(impl => !impl.matched);
1777+
if (unmatchedImplementations.length || unsatisfiedRanges.length) {
1778+
let error = "Not all ranges or implementations are satisfied";
1779+
if (unsatisfiedRanges.length) {
1780+
error += "\nUnsatisfied ranges:";
1781+
for (const range of unsatisfiedRanges) {
1782+
error += `\n (${range.start}, ${range.end}) in ${range.fileName}: ${this.rangeText(range)}`;
1783+
}
1784+
}
1785+
1786+
if (unmatchedImplementations.length) {
1787+
error += "\nUnmatched implementations:";
1788+
for (const impl of unmatchedImplementations) {
1789+
const end = impl.textSpan.start + impl.textSpan.length;
1790+
error += `\n (${impl.textSpan.start}, ${end}) in ${impl.fileName}: ${this.getFileContent(impl.fileName).slice(impl.textSpan.start, end)}`;
1791+
}
1792+
}
1793+
this.raiseError(error);
1794+
}
1795+
1796+
function implementationsAreEqual(a: ImplementationLocationInformation, b: ImplementationLocationInformation) {
1797+
return a.fileName === b.fileName && TestState.textSpansEqual(a.textSpan, b.textSpan);
1798+
}
1799+
}
1800+
17101801
public getMarkers(): Marker[] {
17111802
// Return a copy of the list
17121803
return this.testData.markers.slice(0);
@@ -2885,6 +2976,10 @@ namespace FourSlashInterface {
28852976
this.state.goToTypeDefinition(definitionIndex);
28862977
}
28872978

2979+
public implementation() {
2980+
this.state.goToImplementation();
2981+
}
2982+
28882983
public position(position: number, fileIndex?: number): void;
28892984
public position(position: number, fileName?: string): void;
28902985
public position(position: number, fileNameOrIndex?: any): void {
@@ -2985,6 +3080,10 @@ namespace FourSlashInterface {
29853080
this.state.verifyTypeDefinitionsCount(this.negative, expectedCount);
29863081
}
29873082

3083+
public implementationListIsEmpty() {
3084+
this.state.verifyImplementationListIsEmpty(this.negative);
3085+
}
3086+
29883087
public isValidBraceCompletionAtPosition(openingBrace: string) {
29893088
this.state.verifyBraceCompletionAtPosition(this.negative, openingBrace);
29903089
}
@@ -3253,6 +3352,10 @@ namespace FourSlashInterface {
32533352
public ProjectInfo(expected: string[]) {
32543353
this.state.verifyProjectInfo(expected);
32553354
}
3355+
3356+
public allRangesAppearInImplementationList(markerName: string) {
3357+
this.state.verifyRangesInImplementationList(markerName);
3358+
}
32563359
}
32573360

32583361
export class Edit {

Diff for: src/harness/harnessLanguageService.ts

+3
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,9 @@ namespace Harness.LanguageService {
435435
getTypeDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] {
436436
return unwrapJSONCallResult(this.shim.getTypeDefinitionAtPosition(fileName, position));
437437
}
438+
getImplementationAtPosition(fileName: string, position: number): ts.ImplementationLocation[] {
439+
return unwrapJSONCallResult(this.shim.getImplementationAtPosition(fileName, position));
440+
}
438441
getReferencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] {
439442
return unwrapJSONCallResult(this.shim.getReferencesAtPosition(fileName, position));
440443
}

Diff for: src/server/client.ts

+22
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,28 @@ namespace ts.server {
368368
});
369369
}
370370

371+
getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] {
372+
const lineOffset = this.positionToOneBasedLineOffset(fileName, position);
373+
const args: protocol.FileLocationRequestArgs = {
374+
file: fileName,
375+
line: lineOffset.line,
376+
offset: lineOffset.offset,
377+
};
378+
379+
const request = this.processRequest<protocol.ImplementationRequest>(CommandNames.Implementation, args);
380+
const response = this.processResponse<protocol.ImplementationResponse>(request);
381+
382+
return response.body.map(entry => {
383+
const fileName = entry.file;
384+
const start = this.lineOffsetToPosition(fileName, entry.start);
385+
const end = this.lineOffsetToPosition(fileName, entry.end);
386+
return {
387+
fileName,
388+
textSpan: ts.createTextSpanFromBounds(start, end)
389+
};
390+
});
391+
}
392+
371393
findReferences(fileName: string, position: number): ReferencedSymbol[] {
372394
// Not yet implemented.
373395
return [];

Diff for: src/server/protocol.d.ts

+15
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,14 @@ declare namespace ts.server.protocol {
193193
export interface TypeDefinitionRequest extends FileLocationRequest {
194194
}
195195

196+
/**
197+
* Go to implementation request; value of command field is
198+
* "implementation". Return response giving the file locations that
199+
* implement the symbol found in file at location line, col.
200+
*/
201+
export interface ImplementationRequest extends FileLocationRequest {
202+
}
203+
196204
/**
197205
* Location in source code expressed as (one-based) line and character offset.
198206
*/
@@ -240,6 +248,13 @@ declare namespace ts.server.protocol {
240248
body?: FileSpan[];
241249
}
242250

251+
/**
252+
* Implementation response message. Gives text range for implementations.
253+
*/
254+
export interface ImplementationResponse extends Response {
255+
body?: FileSpan[];
256+
}
257+
243258
/**
244259
* Get occurrences request; value of command field is
245260
* "occurrences". Return response giving spans that are relevant

Diff for: src/server/session.ts

+27
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ namespace ts.server {
111111
export const Formatonkey = "formatonkey";
112112
export const Geterr = "geterr";
113113
export const GeterrForProject = "geterrForProject";
114+
export const Implementation = "implementation";
114115
export const SemanticDiagnosticsSync = "semanticDiagnosticsSync";
115116
export const SyntacticDiagnosticsSync = "syntacticDiagnosticsSync";
116117
export const NavBar = "navbar";
@@ -363,6 +364,28 @@ namespace ts.server {
363364
}));
364365
}
365366

367+
private getImplementation(line: number, offset: number, fileName: string): protocol.FileSpan[] {
368+
const file = ts.normalizePath(fileName);
369+
const project = this.projectService.getProjectForFile(file);
370+
if (!project || project.languageServiceDiabled) {
371+
throw Errors.NoProject;
372+
}
373+
374+
const compilerService = project.compilerService;
375+
const implementations = compilerService.languageService.getImplementationAtPosition(file,
376+
compilerService.host.lineOffsetToPosition(file, line, offset));
377+
378+
if (!implementations) {
379+
return undefined;
380+
}
381+
382+
return implementations.map(impl => ({
383+
file: impl.fileName,
384+
start: compilerService.host.positionToLineOffset(impl.fileName, impl.textSpan.start),
385+
end: compilerService.host.positionToLineOffset(impl.fileName, ts.textSpanEnd(impl.textSpan))
386+
}));
387+
}
388+
366389
private getOccurrences(line: number, offset: number, fileName: string): protocol.OccurrencesResponseItem[] {
367390
fileName = ts.normalizePath(fileName);
368391
const project = this.projectService.getProjectForFile(fileName);
@@ -1090,6 +1113,10 @@ namespace ts.server {
10901113
const defArgs = <protocol.FileLocationRequestArgs>request.arguments;
10911114
return { response: this.getTypeDefinition(defArgs.line, defArgs.offset, defArgs.file), responseRequired: true };
10921115
},
1116+
[CommandNames.Implementation]: (request: protocol.Request) => {
1117+
const implArgs = <protocol.FileLocationRequestArgs>request.arguments;
1118+
return { response: this.getImplementation(implArgs.line, implArgs.offset, implArgs.file), responseRequired: true };
1119+
},
10931120
[CommandNames.References]: (request: protocol.Request) => {
10941121
const defArgs = <protocol.FileLocationRequestArgs>request.arguments;
10951122
return { response: this.getReferences(defArgs.line, defArgs.offset, defArgs.file), responseRequired: true };

Diff for: src/services/documentHighlights.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ namespace ts.DocumentHighlights {
2727
node.kind === SyntaxKind.StringLiteral ||
2828
isLiteralNameOfPropertyDeclarationOrIndexAccess(node)) {
2929

30-
const referencedSymbols = FindAllReferences.getReferencedSymbolsForNode(typeChecker, cancellationToken, node, sourceFilesToSearch, /*findInStrings*/ false, /*findInComments*/ false);
30+
const referencedSymbols = FindAllReferences.getReferencedSymbolsForNode(typeChecker, cancellationToken, node, sourceFilesToSearch, /*findInStrings*/ false, /*findInComments*/ false, /*implementations*/false);
3131
return convertReferencedSymbols(referencedSymbols);
3232

3333
}

0 commit comments

Comments
 (0)