diff --git a/packages/language-service/ivy/definitions.ts b/packages/language-service/ivy/definitions.ts index b748f78eec..e78068bcf7 100644 --- a/packages/language-service/ivy/definitions.ts +++ b/packages/language-service/ivy/definitions.ts @@ -14,7 +14,7 @@ import * as ts from 'typescript'; import {getTargetAtPosition, TargetNodeKind} from './template_target'; import {findTightestNode, getParentClassDeclaration} from './ts_utils'; -import {flatMap, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, getTextSpanOfNode, isDollarEvent, isTypeScriptFile, TemplateInfo, toTextSpan} from './utils'; +import {flatMap, getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, getTemplateLocationFromShimLocation, getTextSpanOfNode, isDollarEvent, isTypeScriptFile, TemplateInfo, toTextSpan} from './utils'; interface DefinitionMeta { node: AST|TmplAstNode; @@ -100,15 +100,22 @@ export class DefinitionBuilder { case SymbolKind.Reference: { const definitions: ts.DefinitionInfo[] = []; if (symbol.declaration !== node) { - definitions.push({ - name: symbol.declaration.name, - containerName: '', - containerKind: ts.ScriptElementKind.unknown, - kind: ts.ScriptElementKind.variableElement, - textSpan: getTextSpanOfNode(symbol.declaration), - contextSpan: toTextSpan(symbol.declaration.sourceSpan), - fileName: symbol.declaration.sourceSpan.start.file.url, - }); + const shimLocation = symbol.kind === SymbolKind.Variable ? symbol.localVarLocation : + symbol.referenceVarLocation; + const mapping = getTemplateLocationFromShimLocation( + this.compiler.getTemplateTypeChecker(), shimLocation.shimPath, + shimLocation.positionInShimFile); + if (mapping !== null) { + definitions.push({ + name: symbol.declaration.name, + containerName: '', + containerKind: ts.ScriptElementKind.unknown, + kind: ts.ScriptElementKind.variableElement, + textSpan: getTextSpanOfNode(symbol.declaration), + contextSpan: toTextSpan(symbol.declaration.sourceSpan), + fileName: mapping.templateUrl, + }); + } } if (symbol.kind === SymbolKind.Variable) { definitions.push( diff --git a/packages/language-service/ivy/references.ts b/packages/language-service/ivy/references.ts index 4e71360764..114385e5fd 100644 --- a/packages/language-service/ivy/references.ts +++ b/packages/language-service/ivy/references.ts @@ -14,7 +14,7 @@ import * as ts from 'typescript'; import {getTargetAtPosition, TargetNodeKind} from './template_target'; import {findTightestNode} from './ts_utils'; -import {getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, isWithin, TemplateInfo, toTextSpan} from './utils'; +import {getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, getTemplateLocationFromShimLocation, isWithin, TemplateInfo, toTextSpan} from './utils'; interface FilePosition { fileName: string; @@ -376,27 +376,14 @@ export class ReferencesAndRenameBuilder { // TODO(atscott): Determine how to consistently resolve paths. i.e. with the project // serverHost or LSParseConfigHost in the adapter. We should have a better defined way to // normalize paths. - const mapping = templateTypeChecker.getTemplateMappingAtShimLocation({ - shimPath: absoluteFrom(shimDocumentSpan.fileName), - positionInShimFile: shimDocumentSpan.textSpan.start, - }); + const mapping = getTemplateLocationFromShimLocation( + templateTypeChecker, absoluteFrom(shimDocumentSpan.fileName), + shimDocumentSpan.textSpan.start); if (mapping === null) { return null; } - const {templateSourceMapping, span} = mapping; - - let templateUrl: AbsoluteFsPath; - if (templateSourceMapping.type === 'direct') { - templateUrl = absoluteFromSourceFile(templateSourceMapping.node.getSourceFile()); - } else if (templateSourceMapping.type === 'external') { - templateUrl = absoluteFrom(templateSourceMapping.templateUrl); - } else { - // This includes indirect mappings, which are difficult to map directly to the code - // location. Diagnostics similarly return a synthetic template string for this case rather - // than a real location. - return null; - } + const {span, templateUrl} = mapping; if (requiredNodeText !== undefined && span.toString() !== requiredNodeText) { return null; } diff --git a/packages/language-service/ivy/test/definitions_spec.ts b/packages/language-service/ivy/test/definitions_spec.ts index 52f1f82983..5b0cd19aaa 100644 --- a/packages/language-service/ivy/test/definitions_spec.ts +++ b/packages/language-service/ivy/test/definitions_spec.ts @@ -13,6 +13,28 @@ import {extractCursorInfo, LanguageServiceTestEnvironment} from './env'; import {assertFileNames, createModuleWithDeclarations, humanizeDocumentSpanLike} from './test_utils'; describe('definitions', () => { + it('gets definition for template reference in overridden template', () => { + initMockFileSystem('Native'); + const templateFile = {contents: '', name: absoluteFrom('/app.html')}; + const appFile = { + name: absoluteFrom('/app.ts'), + contents: ` + import {Component} from '@angular/core'; + + @Component({templateUrl: '/app.html'}) + export class AppCmp {} + `, + }; + + const env = createModuleWithDeclarations([appFile], [templateFile]); + const {cursor} = env.overrideTemplateWithCursor( + absoluteFrom('/app.ts'), 'AppCmp', ' {{myIn¦put.value}}'); + env.expectNoSourceDiagnostics(); + const {definitions} = env.ngLS.getDefinitionAndBoundSpan(absoluteFrom('/app.html'), cursor)!; + expect(definitions![0].name).toEqual('myInput'); + assertFileNames(Array.from(definitions!), ['app.html']); + }); + it('returns the pipe class as definition when checkTypeOfPipes is false', () => { initMockFileSystem('Native'); const {cursor, text} = extractCursorInfo('{{"1/1/2020" | dat¦e}}'); diff --git a/packages/language-service/ivy/test/test_utils.ts b/packages/language-service/ivy/test/test_utils.ts index ccc0408862..4ddb57d80a 100644 --- a/packages/language-service/ivy/test/test_utils.ts +++ b/packages/language-service/ivy/test/test_utils.ts @@ -60,7 +60,7 @@ export function humanizeDocumentSpanLike( env.host.readFile(item.fileName)) ?? ''; if (!fileContents) { - throw new Error('Could not read file ${entry.fileName}'); + throw new Error(`Could not read file ${item.fileName}`); } return { ...item, diff --git a/packages/language-service/ivy/utils.ts b/packages/language-service/ivy/utils.ts index 07c4c15c28..a7ab8156b8 100644 --- a/packages/language-service/ivy/utils.ts +++ b/packages/language-service/ivy/utils.ts @@ -7,9 +7,10 @@ */ import {AbsoluteSourceSpan, CssSelector, ParseSourceSpan, SelectorMatcher, TmplAstBoundEvent} from '@angular/compiler'; import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core'; +import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system'; import {isExternalResource} from '@angular/compiler-cli/src/ngtsc/metadata'; import {DeclarationNode} from '@angular/compiler-cli/src/ngtsc/reflection'; -import {DirectiveSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; +import {DirectiveSymbol, TemplateTypeChecker} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST import * as ts from 'typescript'; @@ -346,3 +347,31 @@ export function isWithin(position: number, span: AbsoluteSourceSpan|ParseSourceS // like ¦start and end¦ where ¦ is the cursor. return start <= position && position <= end; } + +/** + * For a given location in a shim file, retrieves the corresponding file url for the template and + * the span in the template. + */ +export function getTemplateLocationFromShimLocation( + templateTypeChecker: TemplateTypeChecker, shimPath: AbsoluteFsPath, + positionInShimFile: number): {templateUrl: AbsoluteFsPath, span: ParseSourceSpan}|null { + const mapping = + templateTypeChecker.getTemplateMappingAtShimLocation({shimPath, positionInShimFile}); + if (mapping === null) { + return null; + } + const {templateSourceMapping, span} = mapping; + + let templateUrl: AbsoluteFsPath; + if (templateSourceMapping.type === 'direct') { + templateUrl = absoluteFromSourceFile(templateSourceMapping.node.getSourceFile()); + } else if (templateSourceMapping.type === 'external') { + templateUrl = absoluteFrom(templateSourceMapping.templateUrl); + } else { + // This includes indirect mappings, which are difficult to map directly to the code + // location. Diagnostics similarly return a synthetic template string for this case rather + // than a real location. + return null; + } + return {templateUrl, span}; +} \ No newline at end of file