Skip to content

Commit

Permalink
Merge pull request #10482 from Microsoft/go_to_implementation_pr
Browse files Browse the repository at this point in the history
Go to Implementation
  • Loading branch information
riknoll authored Sep 15, 2016
2 parents 98a3fc5 + 29d85cd commit 50d243e
Show file tree
Hide file tree
Showing 67 changed files with 1,609 additions and 38 deletions.
3 changes: 2 additions & 1 deletion Jakefile.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ var servicesSources = [
"documentRegistry.ts",
"findAllReferences.ts",
"goToDefinition.ts",
"goToImplementation.ts",
"jsDoc.ts",
"jsTyping.ts",
"navigateTo.ts",
Expand Down Expand Up @@ -789,7 +790,7 @@ function cleanTestDirs() {

// used to pass data from jake command line directly to run.js
function writeTestConfigFile(tests, light, taskConfigsFolder, workerCount, stackTraceLimit) {
var testConfigContents = JSON.stringify({
var testConfigContents = JSON.stringify({
test: tests ? [tests] : undefined,
light: light,
workerCount: workerCount,
Expand Down
103 changes: 103 additions & 0 deletions src/harness/fourslash.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ namespace FourSlash {
marker?: Marker;
}

interface ImplementationLocationInformation extends ts.ImplementationLocation {
matched?: boolean;
}

export interface TextSpan {
start: number;
end: number;
Expand Down Expand Up @@ -1699,6 +1703,17 @@ namespace FourSlash {
assertFn(actualCount, expectedCount, this.messageAtLastKnownMarker("Type definitions Count"));
}

public verifyImplementationListIsEmpty(negative: boolean) {
const implementations = this.languageService.getImplementationAtPosition(this.activeFile.fileName, this.currentCaretPosition);

if (negative) {
assert.isTrue(implementations && implementations.length > 0, "Expected at least one implementation but got 0");
}
else {
assert.isUndefined(implementations, "Expected implementation list to be empty but implementations returned");
}
}

public verifyGoToDefinitionName(expectedName: string, expectedContainerName: string) {
const definitions = this.languageService.getDefinitionAtPosition(this.activeFile.fileName, this.currentCaretPosition);
const actualDefinitionName = definitions && definitions.length ? definitions[0].name : "";
Expand All @@ -1707,6 +1722,82 @@ namespace FourSlash {
assert.equal(actualDefinitionContainerName, expectedContainerName, this.messageAtLastKnownMarker("Definition Info Container Name"));
}

public goToImplementation() {
const implementations = this.languageService.getImplementationAtPosition(this.activeFile.fileName, this.currentCaretPosition);
if (!implementations || !implementations.length) {
this.raiseError("goToImplementation failed - expected to find at least one implementation location but got 0");
}
if (implementations.length > 1) {
this.raiseError(`goToImplementation failed - more than 1 implementation returned (${implementations.length})`);
}

const implementation = implementations[0];
this.openFile(implementation.fileName);
this.currentCaretPosition = implementation.textSpan.start;
}

public verifyRangesInImplementationList(markerName: string) {
this.goToMarker(markerName);
const implementations: ImplementationLocationInformation[] = this.languageService.getImplementationAtPosition(this.activeFile.fileName, this.currentCaretPosition);
if (!implementations || !implementations.length) {
this.raiseError("verifyRangesInImplementationList failed - expected to find at least one implementation location but got 0");
}

for (let i = 0; i < implementations.length; i++) {
for (let j = 0; j < implementations.length; j++) {
if (i !== j && implementationsAreEqual(implementations[i], implementations[j])) {
const { textSpan, fileName } = implementations[i];
const end = textSpan.start + textSpan.length;
this.raiseError(`Duplicate implementations returned for range (${textSpan.start}, ${end}) in ${fileName}`);
}
}
}

const ranges = this.getRanges();

if (!ranges || !ranges.length) {
this.raiseError("verifyRangesInImplementationList failed - expected to find at least one range in test source");
}

const unsatisfiedRanges: Range[] = [];

for (const range of ranges) {
const length = range.end - range.start;
const matchingImpl = ts.find(implementations, impl =>
range.fileName === impl.fileName && range.start === impl.textSpan.start && length === impl.textSpan.length);
if (matchingImpl) {
matchingImpl.matched = true;
}
else {
unsatisfiedRanges.push(range);
}
}

const unmatchedImplementations = implementations.filter(impl => !impl.matched);
if (unmatchedImplementations.length || unsatisfiedRanges.length) {
let error = "Not all ranges or implementations are satisfied";
if (unsatisfiedRanges.length) {
error += "\nUnsatisfied ranges:";
for (const range of unsatisfiedRanges) {
error += `\n (${range.start}, ${range.end}) in ${range.fileName}: ${this.rangeText(range)}`;
}
}

if (unmatchedImplementations.length) {
error += "\nUnmatched implementations:";
for (const impl of unmatchedImplementations) {
const end = impl.textSpan.start + impl.textSpan.length;
error += `\n (${impl.textSpan.start}, ${end}) in ${impl.fileName}: ${this.getFileContent(impl.fileName).slice(impl.textSpan.start, end)}`;
}
}
this.raiseError(error);
}

function implementationsAreEqual(a: ImplementationLocationInformation, b: ImplementationLocationInformation) {
return a.fileName === b.fileName && TestState.textSpansEqual(a.textSpan, b.textSpan);
}
}

public getMarkers(): Marker[] {
// Return a copy of the list
return this.testData.markers.slice(0);
Expand Down Expand Up @@ -2885,6 +2976,10 @@ namespace FourSlashInterface {
this.state.goToTypeDefinition(definitionIndex);
}

public implementation() {
this.state.goToImplementation();
}

public position(position: number, fileIndex?: number): void;
public position(position: number, fileName?: string): void;
public position(position: number, fileNameOrIndex?: any): void {
Expand Down Expand Up @@ -2985,6 +3080,10 @@ namespace FourSlashInterface {
this.state.verifyTypeDefinitionsCount(this.negative, expectedCount);
}

public implementationListIsEmpty() {
this.state.verifyImplementationListIsEmpty(this.negative);
}

public isValidBraceCompletionAtPosition(openingBrace: string) {
this.state.verifyBraceCompletionAtPosition(this.negative, openingBrace);
}
Expand Down Expand Up @@ -3253,6 +3352,10 @@ namespace FourSlashInterface {
public ProjectInfo(expected: string[]) {
this.state.verifyProjectInfo(expected);
}

public allRangesAppearInImplementationList(markerName: string) {
this.state.verifyRangesInImplementationList(markerName);
}
}

export class Edit {
Expand Down
3 changes: 3 additions & 0 deletions src/harness/harnessLanguageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,9 @@ namespace Harness.LanguageService {
getTypeDefinitionAtPosition(fileName: string, position: number): ts.DefinitionInfo[] {
return unwrapJSONCallResult(this.shim.getTypeDefinitionAtPosition(fileName, position));
}
getImplementationAtPosition(fileName: string, position: number): ts.ImplementationLocation[] {
return unwrapJSONCallResult(this.shim.getImplementationAtPosition(fileName, position));
}
getReferencesAtPosition(fileName: string, position: number): ts.ReferenceEntry[] {
return unwrapJSONCallResult(this.shim.getReferencesAtPosition(fileName, position));
}
Expand Down
22 changes: 22 additions & 0 deletions src/server/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,28 @@ namespace ts.server {
});
}

getImplementationAtPosition(fileName: string, position: number): ImplementationLocation[] {
const lineOffset = this.positionToOneBasedLineOffset(fileName, position);
const args: protocol.FileLocationRequestArgs = {
file: fileName,
line: lineOffset.line,
offset: lineOffset.offset,
};

const request = this.processRequest<protocol.ImplementationRequest>(CommandNames.Implementation, args);
const response = this.processResponse<protocol.ImplementationResponse>(request);

return response.body.map(entry => {
const fileName = entry.file;
const start = this.lineOffsetToPosition(fileName, entry.start);
const end = this.lineOffsetToPosition(fileName, entry.end);
return {
fileName,
textSpan: ts.createTextSpanFromBounds(start, end)
};
});
}

findReferences(fileName: string, position: number): ReferencedSymbol[] {
// Not yet implemented.
return [];
Expand Down
15 changes: 15 additions & 0 deletions src/server/protocol.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,14 @@ declare namespace ts.server.protocol {
export interface TypeDefinitionRequest extends FileLocationRequest {
}

/**
* Go to implementation request; value of command field is
* "implementation". Return response giving the file locations that
* implement the symbol found in file at location line, col.
*/
export interface ImplementationRequest extends FileLocationRequest {
}

/**
* Location in source code expressed as (one-based) line and character offset.
*/
Expand Down Expand Up @@ -240,6 +248,13 @@ declare namespace ts.server.protocol {
body?: FileSpan[];
}

/**
* Implementation response message. Gives text range for implementations.
*/
export interface ImplementationResponse extends Response {
body?: FileSpan[];
}

/**
* Get occurrences request; value of command field is
* "occurrences". Return response giving spans that are relevant
Expand Down
27 changes: 27 additions & 0 deletions src/server/session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ namespace ts.server {
export const Formatonkey = "formatonkey";
export const Geterr = "geterr";
export const GeterrForProject = "geterrForProject";
export const Implementation = "implementation";
export const SemanticDiagnosticsSync = "semanticDiagnosticsSync";
export const SyntacticDiagnosticsSync = "syntacticDiagnosticsSync";
export const NavBar = "navbar";
Expand Down Expand Up @@ -363,6 +364,28 @@ namespace ts.server {
}));
}

private getImplementation(line: number, offset: number, fileName: string): protocol.FileSpan[] {
const file = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(file);
if (!project || project.languageServiceDiabled) {
throw Errors.NoProject;
}

const compilerService = project.compilerService;
const implementations = compilerService.languageService.getImplementationAtPosition(file,
compilerService.host.lineOffsetToPosition(file, line, offset));

if (!implementations) {
return undefined;
}

return implementations.map(impl => ({
file: impl.fileName,
start: compilerService.host.positionToLineOffset(impl.fileName, impl.textSpan.start),
end: compilerService.host.positionToLineOffset(impl.fileName, ts.textSpanEnd(impl.textSpan))
}));
}

private getOccurrences(line: number, offset: number, fileName: string): protocol.OccurrencesResponseItem[] {
fileName = ts.normalizePath(fileName);
const project = this.projectService.getProjectForFile(fileName);
Expand Down Expand Up @@ -1090,6 +1113,10 @@ namespace ts.server {
const defArgs = <protocol.FileLocationRequestArgs>request.arguments;
return { response: this.getTypeDefinition(defArgs.line, defArgs.offset, defArgs.file), responseRequired: true };
},
[CommandNames.Implementation]: (request: protocol.Request) => {
const implArgs = <protocol.FileLocationRequestArgs>request.arguments;
return { response: this.getImplementation(implArgs.line, implArgs.offset, implArgs.file), responseRequired: true };
},
[CommandNames.References]: (request: protocol.Request) => {
const defArgs = <protocol.FileLocationRequestArgs>request.arguments;
return { response: this.getReferences(defArgs.line, defArgs.offset, defArgs.file), responseRequired: true };
Expand Down
2 changes: 1 addition & 1 deletion src/services/documentHighlights.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ namespace ts.DocumentHighlights {
node.kind === SyntaxKind.StringLiteral ||
isLiteralNameOfPropertyDeclarationOrIndexAccess(node)) {

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

}
Expand Down
Loading

0 comments on commit 50d243e

Please sign in to comment.