Skip to content

Commit

Permalink
fix(language-service): 'go to defininition' for objects defined in te…
Browse files Browse the repository at this point in the history
…mplate (#42559)

Previously, the "go to definition" action did no account for the
possibility that something may actually be defined in a template. This
change updates the logic in the definition builder to convert any
results that are locations in template typecheck files to their
corresponding locations in the template.

PR Close #42559
  • Loading branch information
atscott authored and alxhub committed Jun 14, 2021
1 parent 228beea commit 4001e9d
Show file tree
Hide file tree
Showing 3 changed files with 66 additions and 4 deletions.
37 changes: 35 additions & 2 deletions packages/language-service/ivy/definitions.ts
Expand Up @@ -8,10 +8,13 @@

import {AST, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstElement, TmplAstNode, TmplAstTemplate, TmplAstTextAttribute} from '@angular/compiler';
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
import {isExternalResource} from '@angular/compiler-cli/src/ngtsc/metadata';
import {ProgramDriver} from '@angular/compiler-cli/src/ngtsc/program_driver';
import {DirectiveSymbol, DomBindingSymbol, ElementSymbol, ShimLocation, Symbol, SymbolKind, TemplateSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
import * as ts from 'typescript';

import {convertToTemplateDocumentSpan} from './references_and_rename_utils';
import {getTargetAtPosition, TargetNodeKind} from './template_target';
import {findTightestNode, getParentClassDeclaration} from './ts_utils';
import {flatMap, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, getTemplateLocationFromShimLocation, getTextSpanOfNode, isDollarEvent, isTypeScriptFile, TemplateInfo, toTextSpan} from './utils';
Expand All @@ -27,7 +30,11 @@ interface HasShimLocation {
}

export class DefinitionBuilder {
constructor(private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler) {}
private readonly ttc = this.compiler.getTemplateTypeChecker();

constructor(
private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler,
private readonly driver: ProgramDriver) {}

getDefinitionAndBoundSpan(fileName: string, position: number): ts.DefinitionInfoAndBoundSpan
|undefined {
Expand Down Expand Up @@ -132,10 +139,36 @@ export class DefinitionBuilder {
private getDefinitionsForSymbols(...symbols: HasShimLocation[]): ts.DefinitionInfo[] {
return flatMap(symbols, ({shimLocation}) => {
const {shimPath, positionInShimFile} = shimLocation;
return this.tsLS.getDefinitionAtPosition(shimPath, positionInShimFile) ?? [];
const definitionInfos = this.tsLS.getDefinitionAtPosition(shimPath, positionInShimFile);
if (definitionInfos === undefined) {
return [];
}
return this.mapShimResultsToTemplates(definitionInfos);
});
}

/**
* Converts and definition info result that points to a template typecheck file to a reference to
* the corresponding location in the template.
*/
private mapShimResultsToTemplates(definitionInfos: readonly ts.DefinitionInfo[]):
readonly ts.DefinitionInfo[] {
const result: ts.DefinitionInfo[] = [];
for (const info of definitionInfos) {
if (this.ttc.isTrackedTypeCheckFile(absoluteFrom(info.fileName))) {
const templateDefinitionInfo =
convertToTemplateDocumentSpan(info, this.ttc, this.driver.getProgram());
if (templateDefinitionInfo === null) {
continue;
}
result.push(templateDefinitionInfo);
} else {
result.push(info);
}
}
return result;
}

getTypeDefinitionsAtPosition(fileName: string, position: number):
readonly ts.DefinitionInfo[]|undefined {
const templateInfo = getTemplateInfoAtPosition(fileName, position, this.compiler);
Expand Down
4 changes: 2 additions & 2 deletions packages/language-service/ivy/language_service.ts
Expand Up @@ -118,7 +118,7 @@ export class LanguageService {
if (!isInAngularContext(compiler.getCurrentProgram(), fileName, position)) {
return undefined;
}
return new DefinitionBuilder(this.tsLS, compiler)
return new DefinitionBuilder(this.tsLS, compiler, this.programDriver)
.getDefinitionAndBoundSpan(fileName, position);
});
}
Expand All @@ -129,7 +129,7 @@ export class LanguageService {
if (!isTemplateContext(compiler.getCurrentProgram(), fileName, position)) {
return undefined;
}
return new DefinitionBuilder(this.tsLS, compiler)
return new DefinitionBuilder(this.tsLS, compiler, this.programDriver)
.getTypeDefinitionsAtPosition(fileName, position);
});
}
Expand Down
29 changes: 29 additions & 0 deletions packages/language-service/ivy/test/definitions_spec.ts
Expand Up @@ -176,6 +176,35 @@ describe('definitions', () => {
assertFileNames(definitions, ['style.scss']);
});

it('gets definition for property of variable declared in template', () => {
initMockFileSystem('Native');
const files = {
'app.html': `
<ng-container *ngIf="{prop: myVal} as myVar">
{{myVar.prop.name}}
</ng-container>
`,
'app.ts': `
import {Component} from '@angular/core';
@Component({templateUrl: '/app.html'})
export class AppCmp {
myVal = {name: 'Andrew'};
}
`,
};
const env = LanguageServiceTestEnv.setup();

const project = createModuleAndProjectWithDeclarations(env, 'test', files);
const template = project.openFile('app.html');
project.expectNoSourceDiagnostics();

template.moveCursorToText('{{myVar.pro¦p.name}}');
const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
expect(definitions![0].name).toEqual('"prop"');
assertFileNames(Array.from(definitions!), ['app.html']);
});

function getDefinitionsAndAssertBoundSpan(env: LanguageServiceTestEnv, file: OpenBuffer) {
env.expectNoSourceDiagnostics();
const definitionAndBoundSpan = file.getDefinitionAndBoundSpan();
Expand Down

0 comments on commit 4001e9d

Please sign in to comment.