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
This commit is contained in:
parent
228beeabd1
commit
4001e9d808
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
Loading…
Reference in New Issue