From 4001e9d80852101a30b1adb1e501438b5846c463 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Fri, 11 Jun 2021 13:05:42 -0700 Subject: [PATCH] fix(language-service): 'go to defininition' for objects defined in template (#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 --- packages/language-service/ivy/definitions.ts | 37 ++++++++++++++++++- .../language-service/ivy/language_service.ts | 4 +- .../ivy/test/definitions_spec.ts | 29 +++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) diff --git a/packages/language-service/ivy/definitions.ts b/packages/language-service/ivy/definitions.ts index 087ec372ed666..257bd85cca823 100644 --- a/packages/language-service/ivy/definitions.ts +++ b/packages/language-service/ivy/definitions.ts @@ -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'; @@ -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 { @@ -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); diff --git a/packages/language-service/ivy/language_service.ts b/packages/language-service/ivy/language_service.ts index 8e913c144d4f6..673128a1abdd4 100644 --- a/packages/language-service/ivy/language_service.ts +++ b/packages/language-service/ivy/language_service.ts @@ -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); }); } @@ -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); }); } diff --git a/packages/language-service/ivy/test/definitions_spec.ts b/packages/language-service/ivy/test/definitions_spec.ts index b1ed3ef3fdc2c..145d8a3d1fb32 100644 --- a/packages/language-service/ivy/test/definitions_spec.ts +++ b/packages/language-service/ivy/test/definitions_spec.ts @@ -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': ` + + {{myVar.prop.name}} + + `, + '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();