refactor(language-service): extract utility functions for reference and rename (#40523)
This commit extracts utility functions and separates them from the core logic of the references and rename builder. PR Close #40523
This commit is contained in:
parent
b8bd3c3dd2
commit
9bc8b343ea
@ -5,27 +5,17 @@
|
|||||||
* Use of this source code is governed by an MIT-style license that can be
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
import {AbsoluteSourceSpan, AST, BindingPipe, LiteralPrimitive, MethodCall, ParseSourceSpan, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstNode, TmplAstReference, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
|
import {AST, TmplAstNode} from '@angular/compiler';
|
||||||
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
||||||
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
|
import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||||
import {PerfPhase} from '@angular/compiler-cli/src/ngtsc/perf';
|
import {PerfPhase} from '@angular/compiler-cli/src/ngtsc/perf';
|
||||||
import {ProgramDriver} from '@angular/compiler-cli/src/ngtsc/program_driver';
|
import {ProgramDriver} from '@angular/compiler-cli/src/ngtsc/program_driver';
|
||||||
import {DirectiveSymbol, ShimLocation, SymbolKind, TemplateTypeChecker} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
|
||||||
import {ExpressionIdentifier, hasExpressionIdentifier} from '@angular/compiler-cli/src/ngtsc/typecheck/src/comments';
|
|
||||||
import * as ts from 'typescript';
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
import {getTargetAtPosition, TargetNodeKind} from './template_target';
|
import {convertToTemplateDocumentSpan, createLocationKey, getRenameTextAndSpanAtPosition, getTargetDetailsAtTemplatePosition} from './references_and_rename_utils';
|
||||||
import {findTightestNode} from './ts_utils';
|
import {findTightestNode} from './ts_utils';
|
||||||
import {getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, getTemplateLocationFromShimLocation, isWithin, TemplateInfo, toTextSpan} from './utils';
|
import {getTemplateInfoAtPosition, TemplateInfo} from './utils';
|
||||||
|
|
||||||
interface FilePosition {
|
|
||||||
fileName: string;
|
|
||||||
position: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
function toFilePosition(shimLocation: ShimLocation): FilePosition {
|
|
||||||
return {fileName: shimLocation.shimPath, position: shimLocation.positionInShimFile};
|
|
||||||
}
|
|
||||||
|
|
||||||
enum RequestKind {
|
enum RequestKind {
|
||||||
Template,
|
Template,
|
||||||
@ -45,20 +35,6 @@ interface TypeScriptRequest {
|
|||||||
|
|
||||||
type RequestOrigin = TemplateRequest|TypeScriptRequest;
|
type RequestOrigin = TemplateRequest|TypeScriptRequest;
|
||||||
|
|
||||||
interface TemplateLocationDetails {
|
|
||||||
/**
|
|
||||||
* A target node in a template.
|
|
||||||
*/
|
|
||||||
templateTarget: TmplAstNode|AST;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TypeScript locations which the template node maps to. A given template node might map to
|
|
||||||
* several TS nodes. For example, a template node for an attribute might resolve to several
|
|
||||||
* directives or a directive and one of its inputs.
|
|
||||||
*/
|
|
||||||
typescriptLocations: FilePosition[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ReferencesAndRenameBuilder {
|
export class ReferencesAndRenameBuilder {
|
||||||
private readonly ttc = this.compiler.getTemplateTypeChecker();
|
private readonly ttc = this.compiler.getTemplateTypeChecker();
|
||||||
|
|
||||||
@ -76,15 +52,18 @@ export class ReferencesAndRenameBuilder {
|
|||||||
return this.tsLS.getRenameInfo(filePath, position);
|
return this.tsLS.getRenameInfo(filePath, position);
|
||||||
}
|
}
|
||||||
|
|
||||||
const allTargetDetails = this.getTargetDetailsAtTemplatePosition(templateInfo, position);
|
const allTargetDetails = getTargetDetailsAtTemplatePosition(templateInfo, position, this.ttc);
|
||||||
if (allTargetDetails === null) {
|
if (allTargetDetails === null) {
|
||||||
return {
|
return {
|
||||||
canRename: false,
|
canRename: false,
|
||||||
localizedErrorMessage: 'Could not find template node at position.',
|
localizedErrorMessage: 'Could not find template node at position.'
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const {templateTarget} = allTargetDetails[0];
|
const {templateTarget} = allTargetDetails[0];
|
||||||
const templateTextAndSpan = getRenameTextAndSpanAtPosition(templateTarget, position);
|
const templateTextAndSpan = getRenameTextAndSpanAtPosition(
|
||||||
|
templateTarget,
|
||||||
|
position,
|
||||||
|
);
|
||||||
if (templateTextAndSpan === null) {
|
if (templateTextAndSpan === null) {
|
||||||
return {canRename: false, localizedErrorMessage: 'Could not determine template node text.'};
|
return {canRename: false, localizedErrorMessage: 'Could not determine template node text.'};
|
||||||
}
|
}
|
||||||
@ -119,7 +98,7 @@ export class ReferencesAndRenameBuilder {
|
|||||||
|
|
||||||
private findRenameLocationsAtTemplatePosition(templateInfo: TemplateInfo, position: number):
|
private findRenameLocationsAtTemplatePosition(templateInfo: TemplateInfo, position: number):
|
||||||
readonly ts.RenameLocation[]|undefined {
|
readonly ts.RenameLocation[]|undefined {
|
||||||
const allTargetDetails = this.getTargetDetailsAtTemplatePosition(templateInfo, position);
|
const allTargetDetails = getTargetDetailsAtTemplatePosition(templateInfo, position, this.ttc);
|
||||||
if (allTargetDetails === null) {
|
if (allTargetDetails === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -181,7 +160,8 @@ export class ReferencesAndRenameBuilder {
|
|||||||
// TODO(atscott): Determine if a file is a shim file in a more robust way and make the API
|
// TODO(atscott): Determine if a file is a shim file in a more robust way and make the API
|
||||||
// available in an appropriate location.
|
// available in an appropriate location.
|
||||||
if (this.ttc.isTrackedTypeCheckFile(absoluteFrom(location.fileName))) {
|
if (this.ttc.isTrackedTypeCheckFile(absoluteFrom(location.fileName))) {
|
||||||
const entry = this.convertToTemplateDocumentSpan(location, this.ttc, originalNodeText);
|
const entry = convertToTemplateDocumentSpan(
|
||||||
|
location, this.ttc, this.driver.getProgram(), originalNodeText);
|
||||||
// There is no template node whose text matches the original rename request. Bail on
|
// There is no template node whose text matches the original rename request. Bail on
|
||||||
// renaming completely rather than providing incomplete results.
|
// renaming completely rather than providing incomplete results.
|
||||||
if (entry === null) {
|
if (entry === null) {
|
||||||
@ -215,7 +195,7 @@ export class ReferencesAndRenameBuilder {
|
|||||||
|
|
||||||
private getReferencesAtTemplatePosition(templateInfo: TemplateInfo, position: number):
|
private getReferencesAtTemplatePosition(templateInfo: TemplateInfo, position: number):
|
||||||
ts.ReferenceEntry[]|undefined {
|
ts.ReferenceEntry[]|undefined {
|
||||||
const allTargetDetails = this.getTargetDetailsAtTemplatePosition(templateInfo, position);
|
const allTargetDetails = getTargetDetailsAtTemplatePosition(templateInfo, position, this.ttc);
|
||||||
if (allTargetDetails === null) {
|
if (allTargetDetails === null) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
@ -231,126 +211,6 @@ export class ReferencesAndRenameBuilder {
|
|||||||
return allReferences.length > 0 ? allReferences : undefined;
|
return allReferences.length > 0 ? allReferences : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getTargetDetailsAtTemplatePosition({template, component}: TemplateInfo, position: number):
|
|
||||||
TemplateLocationDetails[]|null {
|
|
||||||
// Find the AST node in the template at the position.
|
|
||||||
const positionDetails = getTargetAtPosition(template, position);
|
|
||||||
if (positionDetails === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nodes = positionDetails.context.kind === TargetNodeKind.TwoWayBindingContext ?
|
|
||||||
positionDetails.context.nodes :
|
|
||||||
[positionDetails.context.node];
|
|
||||||
|
|
||||||
const details: TemplateLocationDetails[] = [];
|
|
||||||
|
|
||||||
for (const node of nodes) {
|
|
||||||
// Get the information about the TCB at the template position.
|
|
||||||
const symbol = this.ttc.getSymbolOfNode(node, component);
|
|
||||||
if (symbol === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const templateTarget = node;
|
|
||||||
switch (symbol.kind) {
|
|
||||||
case SymbolKind.Directive:
|
|
||||||
case SymbolKind.Template:
|
|
||||||
// References to elements, templates, and directives will be through template references
|
|
||||||
// (#ref). They shouldn't be used directly for a Language Service reference request.
|
|
||||||
break;
|
|
||||||
case SymbolKind.Element: {
|
|
||||||
const matches = getDirectiveMatchesForElementTag(symbol.templateNode, symbol.directives);
|
|
||||||
details.push(
|
|
||||||
{typescriptLocations: this.getPositionsForDirectives(matches), templateTarget});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SymbolKind.DomBinding: {
|
|
||||||
// Dom bindings aren't currently type-checked (see `checkTypeOfDomBindings`) so they don't
|
|
||||||
// have a shim location. This means we can't match dom bindings to their lib.dom
|
|
||||||
// reference, but we can still see if they match to a directive.
|
|
||||||
if (!(node instanceof TmplAstTextAttribute) && !(node instanceof TmplAstBoundAttribute)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const directives = getDirectiveMatchesForAttribute(
|
|
||||||
node.name, symbol.host.templateNode, symbol.host.directives);
|
|
||||||
details.push({
|
|
||||||
typescriptLocations: this.getPositionsForDirectives(directives),
|
|
||||||
templateTarget,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SymbolKind.Reference: {
|
|
||||||
details.push({
|
|
||||||
typescriptLocations: [toFilePosition(symbol.referenceVarLocation)],
|
|
||||||
templateTarget,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SymbolKind.Variable: {
|
|
||||||
if ((templateTarget instanceof TmplAstVariable)) {
|
|
||||||
if (templateTarget.valueSpan !== undefined &&
|
|
||||||
isWithin(position, templateTarget.valueSpan)) {
|
|
||||||
// In the valueSpan of the variable, we want to get the reference of the initializer.
|
|
||||||
details.push({
|
|
||||||
typescriptLocations: [toFilePosition(symbol.initializerLocation)],
|
|
||||||
templateTarget,
|
|
||||||
});
|
|
||||||
} else if (isWithin(position, templateTarget.keySpan)) {
|
|
||||||
// In the keySpan of the variable, we want to get the reference of the local variable.
|
|
||||||
details.push({
|
|
||||||
typescriptLocations: [toFilePosition(symbol.localVarLocation)],
|
|
||||||
templateTarget,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If the templateNode is not the `TmplAstVariable`, it must be a usage of the
|
|
||||||
// variable somewhere in the template.
|
|
||||||
details.push({
|
|
||||||
typescriptLocations: [toFilePosition(symbol.localVarLocation)],
|
|
||||||
templateTarget,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SymbolKind.Input:
|
|
||||||
case SymbolKind.Output: {
|
|
||||||
details.push({
|
|
||||||
typescriptLocations:
|
|
||||||
symbol.bindings.map(binding => toFilePosition(binding.shimLocation)),
|
|
||||||
templateTarget,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SymbolKind.Pipe:
|
|
||||||
case SymbolKind.Expression: {
|
|
||||||
details.push(
|
|
||||||
{typescriptLocations: [toFilePosition(symbol.shimLocation)], templateTarget});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return details.length > 0 ? details : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getPositionsForDirectives(directives: Set<DirectiveSymbol>): FilePosition[] {
|
|
||||||
const allDirectives: FilePosition[] = [];
|
|
||||||
for (const dir of directives.values()) {
|
|
||||||
const dirClass = dir.tsSymbol.valueDeclaration;
|
|
||||||
if (dirClass === undefined || !ts.isClassDeclaration(dirClass) ||
|
|
||||||
dirClass.name === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {fileName} = dirClass.getSourceFile();
|
|
||||||
const position = dirClass.name.getStart();
|
|
||||||
allDirectives.push({fileName, position});
|
|
||||||
}
|
|
||||||
|
|
||||||
return allDirectives;
|
|
||||||
}
|
|
||||||
|
|
||||||
private getReferencesAtTypescriptPosition(fileName: string, position: number):
|
private getReferencesAtTypescriptPosition(fileName: string, position: number):
|
||||||
ts.ReferenceEntry[]|undefined {
|
ts.ReferenceEntry[]|undefined {
|
||||||
const refs = this.tsLS.getReferencesAtPosition(fileName, position);
|
const refs = this.tsLS.getReferencesAtPosition(fileName, position);
|
||||||
@ -361,7 +221,7 @@ export class ReferencesAndRenameBuilder {
|
|||||||
const entries: Map<string, ts.ReferenceEntry> = new Map();
|
const entries: Map<string, ts.ReferenceEntry> = new Map();
|
||||||
for (const ref of refs) {
|
for (const ref of refs) {
|
||||||
if (this.ttc.isTrackedTypeCheckFile(absoluteFrom(ref.fileName))) {
|
if (this.ttc.isTrackedTypeCheckFile(absoluteFrom(ref.fileName))) {
|
||||||
const entry = this.convertToTemplateDocumentSpan(ref, this.ttc);
|
const entry = convertToTemplateDocumentSpan(ref, this.ttc, this.driver.getProgram());
|
||||||
if (entry !== null) {
|
if (entry !== null) {
|
||||||
entries.set(createLocationKey(entry), entry);
|
entries.set(createLocationKey(entry), entry);
|
||||||
}
|
}
|
||||||
@ -371,93 +231,4 @@ export class ReferencesAndRenameBuilder {
|
|||||||
}
|
}
|
||||||
return Array.from(entries.values());
|
return Array.from(entries.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
private convertToTemplateDocumentSpan<T extends ts.DocumentSpan>(
|
|
||||||
shimDocumentSpan: T, templateTypeChecker: TemplateTypeChecker, requiredNodeText?: string): T
|
|
||||||
|null {
|
|
||||||
const sf = this.driver.getProgram().getSourceFile(shimDocumentSpan.fileName);
|
|
||||||
if (sf === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const tcbNode = findTightestNode(sf, shimDocumentSpan.textSpan.start);
|
|
||||||
if (tcbNode === undefined ||
|
|
||||||
hasExpressionIdentifier(sf, tcbNode, ExpressionIdentifier.EVENT_PARAMETER)) {
|
|
||||||
// If the reference result is the $event parameter in the subscribe/addEventListener
|
|
||||||
// function in the TCB, we want to filter this result out of the references. We really only
|
|
||||||
// want to return references to the parameter in the template itself.
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// 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 = getTemplateLocationFromShimLocation(
|
|
||||||
templateTypeChecker, absoluteFrom(shimDocumentSpan.fileName),
|
|
||||||
shimDocumentSpan.textSpan.start);
|
|
||||||
if (mapping === null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const {span, templateUrl} = mapping;
|
|
||||||
if (requiredNodeText !== undefined && span.toString() !== requiredNodeText) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...shimDocumentSpan,
|
|
||||||
fileName: templateUrl,
|
|
||||||
textSpan: toTextSpan(span),
|
|
||||||
// Specifically clear other text span values because we do not have enough knowledge to
|
|
||||||
// convert these to spans in the template.
|
|
||||||
contextSpan: undefined,
|
|
||||||
originalContextSpan: undefined,
|
|
||||||
originalTextSpan: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRenameTextAndSpanAtPosition(
|
|
||||||
node: TmplAstNode|AST, position: number): {text: string, span: ts.TextSpan}|null {
|
|
||||||
if (node instanceof TmplAstBoundAttribute || node instanceof TmplAstTextAttribute ||
|
|
||||||
node instanceof TmplAstBoundEvent) {
|
|
||||||
if (node.keySpan === undefined) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {text: node.name, span: toTextSpan(node.keySpan)};
|
|
||||||
} else if (node instanceof TmplAstVariable || node instanceof TmplAstReference) {
|
|
||||||
if (isWithin(position, node.keySpan)) {
|
|
||||||
return {text: node.keySpan.toString(), span: toTextSpan(node.keySpan)};
|
|
||||||
} else if (node.valueSpan && isWithin(position, node.valueSpan)) {
|
|
||||||
return {text: node.valueSpan.toString(), span: toTextSpan(node.valueSpan)};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (node instanceof BindingPipe) {
|
|
||||||
// TODO(atscott): Add support for renaming pipes
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (node instanceof PropertyRead || node instanceof MethodCall || node instanceof PropertyWrite ||
|
|
||||||
node instanceof SafePropertyRead || node instanceof SafeMethodCall) {
|
|
||||||
return {text: node.name, span: toTextSpan(node.nameSpan)};
|
|
||||||
} else if (node instanceof LiteralPrimitive) {
|
|
||||||
const span = toTextSpan(node.sourceSpan);
|
|
||||||
const text = node.value;
|
|
||||||
if (typeof text === 'string') {
|
|
||||||
// The span of a string literal includes the quotes but they should be removed for renaming.
|
|
||||||
span.start += 1;
|
|
||||||
span.length -= 2;
|
|
||||||
}
|
|
||||||
return {text, span};
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a "key" for a rename/reference location by concatenating file name, span start, and span
|
|
||||||
* length. This allows us to de-duplicate template results when an item may appear several times
|
|
||||||
* in the TCB but map back to the same template location.
|
|
||||||
*/
|
|
||||||
function createLocationKey(ds: ts.DocumentSpan) {
|
|
||||||
return ds.fileName + ds.textSpan.start + ds.textSpan.length;
|
|
||||||
}
|
}
|
244
packages/language-service/ivy/references_and_rename_utils.ts
Normal file
244
packages/language-service/ivy/references_and_rename_utils.ts
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Use of this source code is governed by an MIT-style license that can be
|
||||||
|
* found in the LICENSE file at https://angular.io/license
|
||||||
|
*/
|
||||||
|
import {AST, BindingPipe, LiteralPrimitive, MethodCall, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstNode, TmplAstReference, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
|
||||||
|
import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||||
|
import {DirectiveSymbol, ShimLocation, SymbolKind, TemplateTypeChecker} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
||||||
|
import {ExpressionIdentifier, hasExpressionIdentifier} from '@angular/compiler-cli/src/ngtsc/typecheck/src/comments';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
import {getTargetAtPosition, TargetNodeKind} from './template_target';
|
||||||
|
import {findTightestNode} from './ts_utils';
|
||||||
|
import {getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateLocationFromShimLocation, isWithin, TemplateInfo, toTextSpan} from './utils';
|
||||||
|
|
||||||
|
interface FilePosition {
|
||||||
|
fileName: string;
|
||||||
|
position: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toFilePosition(shimLocation: ShimLocation): FilePosition {
|
||||||
|
return {fileName: shimLocation.shimPath, position: shimLocation.positionInShimFile};
|
||||||
|
}
|
||||||
|
export interface TemplateLocationDetails {
|
||||||
|
/**
|
||||||
|
* A target node in a template.
|
||||||
|
*/
|
||||||
|
templateTarget: TmplAstNode|AST;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TypeScript locations which the template node maps to. A given template node might map to
|
||||||
|
* several TS nodes. For example, a template node for an attribute might resolve to several
|
||||||
|
* directives or a directive and one of its inputs.
|
||||||
|
*/
|
||||||
|
typescriptLocations: FilePosition[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function getTargetDetailsAtTemplatePosition(
|
||||||
|
{template, component}: TemplateInfo, position: number,
|
||||||
|
templateTypeChecker: TemplateTypeChecker): TemplateLocationDetails[]|null {
|
||||||
|
// Find the AST node in the template at the position.
|
||||||
|
const positionDetails = getTargetAtPosition(template, position);
|
||||||
|
if (positionDetails === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nodes = positionDetails.context.kind === TargetNodeKind.TwoWayBindingContext ?
|
||||||
|
positionDetails.context.nodes :
|
||||||
|
[positionDetails.context.node];
|
||||||
|
|
||||||
|
const details: TemplateLocationDetails[] = [];
|
||||||
|
|
||||||
|
for (const node of nodes) {
|
||||||
|
// Get the information about the TCB at the template position.
|
||||||
|
const symbol = templateTypeChecker.getSymbolOfNode(node, component);
|
||||||
|
if (symbol === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const templateTarget = node;
|
||||||
|
switch (symbol.kind) {
|
||||||
|
case SymbolKind.Directive:
|
||||||
|
case SymbolKind.Template:
|
||||||
|
// References to elements, templates, and directives will be through template references
|
||||||
|
// (#ref). They shouldn't be used directly for a Language Service reference request.
|
||||||
|
break;
|
||||||
|
case SymbolKind.Element: {
|
||||||
|
const matches = getDirectiveMatchesForElementTag(symbol.templateNode, symbol.directives);
|
||||||
|
details.push({typescriptLocations: getPositionsForDirectives(matches), templateTarget});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SymbolKind.DomBinding: {
|
||||||
|
// Dom bindings aren't currently type-checked (see `checkTypeOfDomBindings`) so they don't
|
||||||
|
// have a shim location. This means we can't match dom bindings to their lib.dom
|
||||||
|
// reference, but we can still see if they match to a directive.
|
||||||
|
if (!(node instanceof TmplAstTextAttribute) && !(node instanceof TmplAstBoundAttribute)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const directives = getDirectiveMatchesForAttribute(
|
||||||
|
node.name, symbol.host.templateNode, symbol.host.directives);
|
||||||
|
details.push({
|
||||||
|
typescriptLocations: getPositionsForDirectives(directives),
|
||||||
|
templateTarget,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SymbolKind.Reference: {
|
||||||
|
details.push({
|
||||||
|
typescriptLocations: [toFilePosition(symbol.referenceVarLocation)],
|
||||||
|
templateTarget,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SymbolKind.Variable: {
|
||||||
|
if ((templateTarget instanceof TmplAstVariable)) {
|
||||||
|
if (templateTarget.valueSpan !== undefined &&
|
||||||
|
isWithin(position, templateTarget.valueSpan)) {
|
||||||
|
// In the valueSpan of the variable, we want to get the reference of the initializer.
|
||||||
|
details.push({
|
||||||
|
typescriptLocations: [toFilePosition(symbol.initializerLocation)],
|
||||||
|
templateTarget,
|
||||||
|
});
|
||||||
|
} else if (isWithin(position, templateTarget.keySpan)) {
|
||||||
|
// In the keySpan of the variable, we want to get the reference of the local variable.
|
||||||
|
details.push({
|
||||||
|
typescriptLocations: [toFilePosition(symbol.localVarLocation)],
|
||||||
|
templateTarget,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If the templateNode is not the `TmplAstVariable`, it must be a usage of the
|
||||||
|
// variable somewhere in the template.
|
||||||
|
details.push({
|
||||||
|
typescriptLocations: [toFilePosition(symbol.localVarLocation)],
|
||||||
|
templateTarget,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SymbolKind.Input:
|
||||||
|
case SymbolKind.Output: {
|
||||||
|
details.push({
|
||||||
|
typescriptLocations: symbol.bindings.map(binding => toFilePosition(binding.shimLocation)),
|
||||||
|
templateTarget,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case SymbolKind.Pipe:
|
||||||
|
case SymbolKind.Expression: {
|
||||||
|
details.push({typescriptLocations: [toFilePosition(symbol.shimLocation)], templateTarget});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return details.length > 0 ? details : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getPositionsForDirectives(directives: Set<DirectiveSymbol>): FilePosition[] {
|
||||||
|
const allDirectives: FilePosition[] = [];
|
||||||
|
for (const dir of directives.values()) {
|
||||||
|
const dirClass = dir.tsSymbol.valueDeclaration;
|
||||||
|
if (dirClass === undefined || !ts.isClassDeclaration(dirClass) || dirClass.name === undefined) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {fileName} = dirClass.getSourceFile();
|
||||||
|
const position = dirClass.name.getStart();
|
||||||
|
allDirectives.push({fileName, position});
|
||||||
|
}
|
||||||
|
|
||||||
|
return allDirectives;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a "key" for a rename/reference location by concatenating file name, span start, and span
|
||||||
|
* length. This allows us to de-duplicate template results when an item may appear several times
|
||||||
|
* in the TCB but map back to the same template location.
|
||||||
|
*/
|
||||||
|
export function createLocationKey(ds: ts.DocumentSpan) {
|
||||||
|
return ds.fileName + ds.textSpan.start + ds.textSpan.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function convertToTemplateDocumentSpan<T extends ts.DocumentSpan>(
|
||||||
|
shimDocumentSpan: T, templateTypeChecker: TemplateTypeChecker, program: ts.Program,
|
||||||
|
requiredNodeText?: string): T|null {
|
||||||
|
const sf = program.getSourceFile(shimDocumentSpan.fileName);
|
||||||
|
if (sf === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const tcbNode = findTightestNode(sf, shimDocumentSpan.textSpan.start);
|
||||||
|
if (tcbNode === undefined ||
|
||||||
|
hasExpressionIdentifier(sf, tcbNode, ExpressionIdentifier.EVENT_PARAMETER)) {
|
||||||
|
// If the reference result is the $event parameter in the subscribe/addEventListener
|
||||||
|
// function in the TCB, we want to filter this result out of the references. We really only
|
||||||
|
// want to return references to the parameter in the template itself.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// 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 = getTemplateLocationFromShimLocation(
|
||||||
|
templateTypeChecker, absoluteFrom(shimDocumentSpan.fileName),
|
||||||
|
shimDocumentSpan.textSpan.start);
|
||||||
|
if (mapping === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {span, templateUrl} = mapping;
|
||||||
|
if (requiredNodeText !== undefined && span.toString() !== requiredNodeText) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...shimDocumentSpan,
|
||||||
|
fileName: templateUrl,
|
||||||
|
textSpan: toTextSpan(span),
|
||||||
|
// Specifically clear other text span values because we do not have enough knowledge to
|
||||||
|
// convert these to spans in the template.
|
||||||
|
contextSpan: undefined,
|
||||||
|
originalContextSpan: undefined,
|
||||||
|
originalTextSpan: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRenameTextAndSpanAtPosition(
|
||||||
|
node: TmplAstNode|AST, position: number): {text: string, span: ts.TextSpan}|null {
|
||||||
|
if (node instanceof TmplAstBoundAttribute || node instanceof TmplAstTextAttribute ||
|
||||||
|
node instanceof TmplAstBoundEvent) {
|
||||||
|
if (node.keySpan === undefined) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {text: node.name, span: toTextSpan(node.keySpan)};
|
||||||
|
} else if (node instanceof TmplAstVariable || node instanceof TmplAstReference) {
|
||||||
|
if (isWithin(position, node.keySpan)) {
|
||||||
|
return {text: node.keySpan.toString(), span: toTextSpan(node.keySpan)};
|
||||||
|
} else if (node.valueSpan && isWithin(position, node.valueSpan)) {
|
||||||
|
return {text: node.valueSpan.toString(), span: toTextSpan(node.valueSpan)};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (node instanceof BindingPipe) {
|
||||||
|
// TODO(atscott): Add support for renaming pipes
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (node instanceof PropertyRead || node instanceof MethodCall || node instanceof PropertyWrite ||
|
||||||
|
node instanceof SafePropertyRead || node instanceof SafeMethodCall) {
|
||||||
|
return {text: node.name, span: toTextSpan(node.nameSpan)};
|
||||||
|
} else if (node instanceof LiteralPrimitive) {
|
||||||
|
const span = toTextSpan(node.sourceSpan);
|
||||||
|
const text = node.value;
|
||||||
|
if (typeof text === 'string') {
|
||||||
|
// The span of a string literal includes the quotes but they should be removed for renaming.
|
||||||
|
span.start += 1;
|
||||||
|
span.length -= 2;
|
||||||
|
}
|
||||||
|
return {text, span};
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user