feat(language-service): Implement `getRenameInfo` (#40439)
The `getRenameInfo` action is used by consumers to 1. Determine if a location is a candidate for renames 2. Determine what text to use as the starting point for the rename PR Close #40439
This commit is contained in:
parent
40e0bfdc0d
commit
4e8198d60f
|
@ -8,7 +8,7 @@
|
|||
|
||||
import {AST, TmplAstBoundEvent, TmplAstNode} from '@angular/compiler';
|
||||
import {CompilerOptions, ConfigurationHost, readConfiguration} from '@angular/compiler-cli';
|
||||
import {absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
|
||||
import {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck';
|
||||
import {OptimizeFor, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
||||
import * as ts from 'typescript/lib/tsserverlibrary';
|
||||
|
@ -113,6 +113,21 @@ export class LanguageService {
|
|||
return results;
|
||||
}
|
||||
|
||||
getRenameInfo(fileName: string, position: number): ts.RenameInfo {
|
||||
const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName);
|
||||
const renameInfo = new ReferencesAndRenameBuilder(this.strategy, this.tsLS, compiler)
|
||||
.getRenameInfo(absoluteFrom(fileName), position);
|
||||
if (!renameInfo.canRename) {
|
||||
return renameInfo;
|
||||
}
|
||||
|
||||
const quickInfo = this.getQuickInfoAtPosition(fileName, position) ??
|
||||
this.tsLS.getQuickInfoAtPosition(fileName, position);
|
||||
const kind = quickInfo?.kind ?? ts.ScriptElementKind.unknown;
|
||||
const kindModifiers = quickInfo?.kindModifiers ?? ts.ScriptElementKind.unknown;
|
||||
return {...renameInfo, kind, kindModifiers};
|
||||
}
|
||||
|
||||
findRenameLocations(fileName: string, position: number): readonly ts.RenameLocation[]|undefined {
|
||||
const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName);
|
||||
const results = new ReferencesAndRenameBuilder(this.strategy, this.tsLS, compiler)
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* 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 {AbsoluteSourceSpan, AST, BindingPipe, LiteralPrimitive, MethodCall, ParseSourceSpan, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstNode, TmplAstReference, TmplAstTextAttribute, TmplAstVariable} 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 {DirectiveSymbol, ShimLocation, SymbolKind, TemplateTypeChecker, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
|
||||
|
@ -14,7 +14,7 @@ import * as ts from 'typescript';
|
|||
|
||||
import {getTargetAtPosition, TargetNodeKind} from './template_target';
|
||||
import {findTightestNode} from './ts_utils';
|
||||
import {getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, isTemplateNode, isWithin, TemplateInfo, toTextSpan} from './utils';
|
||||
import {getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, isWithin, TemplateInfo, toTextSpan} from './utils';
|
||||
|
||||
interface FilePosition {
|
||||
fileName: string;
|
||||
|
@ -64,10 +64,37 @@ export class ReferencesAndRenameBuilder {
|
|||
private readonly strategy: TypeCheckingProgramStrategy,
|
||||
private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler) {}
|
||||
|
||||
getRenameInfo(filePath: string, position: number):
|
||||
Omit<ts.RenameInfoSuccess, 'kind'|'kindModifiers'>|ts.RenameInfoFailure {
|
||||
const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler);
|
||||
// We could not get a template at position so we assume the request came from outside the
|
||||
// template.
|
||||
if (templateInfo === undefined) {
|
||||
return this.tsLS.getRenameInfo(filePath, position);
|
||||
}
|
||||
|
||||
const allTargetDetails = this.getTargetDetailsAtTemplatePosition(templateInfo, position);
|
||||
if (allTargetDetails === null) {
|
||||
return {canRename: false, localizedErrorMessage: 'Could not find template node at position.'};
|
||||
}
|
||||
const {templateTarget} = allTargetDetails[0];
|
||||
const templateTextAndSpan = getRenameTextAndSpanAtPosition(templateTarget, position);
|
||||
if (templateTextAndSpan === null) {
|
||||
return {canRename: false, localizedErrorMessage: 'Could not determine template node text.'};
|
||||
}
|
||||
const {text, span} = templateTextAndSpan;
|
||||
return {
|
||||
canRename: true,
|
||||
displayName: text,
|
||||
fullDisplayName: text,
|
||||
triggerSpan: toTextSpan(span),
|
||||
};
|
||||
}
|
||||
|
||||
findRenameLocations(filePath: string, position: number): readonly ts.RenameLocation[]|undefined {
|
||||
this.ttc.generateAllTypeCheckBlocks();
|
||||
const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler);
|
||||
// We could not get a template at position so we assume the request is came from outside the
|
||||
// We could not get a template at position so we assume the request came from outside the
|
||||
// template.
|
||||
if (templateInfo === undefined) {
|
||||
const requestNode = this.getTsNodeAtPosition(filePath, position);
|
||||
|
@ -126,11 +153,11 @@ export class ReferencesAndRenameBuilder {
|
|||
originalNodeText = requestOrigin.requestNode.getText();
|
||||
} else {
|
||||
const templateNodeText =
|
||||
getTemplateNodeRenameTextAtPosition(requestOrigin.requestNode, requestOrigin.position);
|
||||
getRenameTextAndSpanAtPosition(requestOrigin.requestNode, requestOrigin.position);
|
||||
if (templateNodeText === null) {
|
||||
return undefined;
|
||||
}
|
||||
originalNodeText = templateNodeText;
|
||||
originalNodeText = templateNodeText.text;
|
||||
}
|
||||
|
||||
const locations = this.tsLS.findRenameLocations(
|
||||
|
@ -207,11 +234,11 @@ export class ReferencesAndRenameBuilder {
|
|||
for (const node of nodes) {
|
||||
// Get the information about the TCB at the template position.
|
||||
const symbol = this.ttc.getSymbolOfNode(node, component);
|
||||
const templateTarget = node;
|
||||
|
||||
if (symbol === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const templateTarget = node;
|
||||
switch (symbol.kind) {
|
||||
case SymbolKind.Directive:
|
||||
case SymbolKind.Template:
|
||||
|
@ -233,13 +260,17 @@ export class ReferencesAndRenameBuilder {
|
|||
}
|
||||
const directives = getDirectiveMatchesForAttribute(
|
||||
node.name, symbol.host.templateNode, symbol.host.directives);
|
||||
details.push(
|
||||
{typescriptLocations: this.getPositionsForDirectives(directives), templateTarget});
|
||||
details.push({
|
||||
typescriptLocations: this.getPositionsForDirectives(directives),
|
||||
templateTarget,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case SymbolKind.Reference: {
|
||||
details.push(
|
||||
{typescriptLocations: [toFilePosition(symbol.referenceVarLocation)], templateTarget});
|
||||
details.push({
|
||||
typescriptLocations: [toFilePosition(symbol.referenceVarLocation)],
|
||||
templateTarget,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case SymbolKind.Variable: {
|
||||
|
@ -253,14 +284,18 @@ export class ReferencesAndRenameBuilder {
|
|||
});
|
||||
} 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});
|
||||
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});
|
||||
details.push({
|
||||
typescriptLocations: [toFilePosition(symbol.localVarLocation)],
|
||||
templateTarget,
|
||||
});
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -374,15 +409,19 @@ export class ReferencesAndRenameBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
function getTemplateNodeRenameTextAtPosition(node: TmplAstNode|AST, position: number): string|null {
|
||||
function getRenameTextAndSpanAtPosition(node: TmplAstNode|AST, position: number):
|
||||
{text: string, span: ParseSourceSpan|AbsoluteSourceSpan}|null {
|
||||
if (node instanceof TmplAstBoundAttribute || node instanceof TmplAstTextAttribute ||
|
||||
node instanceof TmplAstBoundEvent) {
|
||||
return node.name;
|
||||
if (node.keySpan === undefined) {
|
||||
return null;
|
||||
}
|
||||
return {text: node.name, span: node.keySpan};
|
||||
} else if (node instanceof TmplAstVariable || node instanceof TmplAstReference) {
|
||||
if (isWithin(position, node.keySpan)) {
|
||||
return node.keySpan.toString();
|
||||
return {text: node.keySpan.toString(), span: node.keySpan};
|
||||
} else if (node.valueSpan && isWithin(position, node.valueSpan)) {
|
||||
return node.valueSpan.toString();
|
||||
return {text: node.valueSpan.toString(), span: node.valueSpan};
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -392,9 +431,16 @@ function getTemplateNodeRenameTextAtPosition(node: TmplAstNode|AST, position: nu
|
|||
}
|
||||
if (node instanceof PropertyRead || node instanceof MethodCall || node instanceof PropertyWrite ||
|
||||
node instanceof SafePropertyRead || node instanceof SafeMethodCall) {
|
||||
return node.name;
|
||||
return {text: node.name, span: node.nameSpan};
|
||||
} else if (node instanceof LiteralPrimitive) {
|
||||
return node.value;
|
||||
const span = node.span;
|
||||
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.end -= 1;
|
||||
}
|
||||
return {text, span};
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -1369,6 +1369,81 @@ describe('find references and rename locations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('get rename info', () => {
|
||||
it('indicates inability to rename when cursor is outside template and in a string literal',
|
||||
() => {
|
||||
const {cursor, text} = extractCursorInfo(`
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({selector: 'my-comp', template: ''})
|
||||
export class MyComp {
|
||||
myProp = 'cannot rena¦me me';
|
||||
}`);
|
||||
env = createModuleWithDeclarations([{name: _('/my-comp.ts'), contents: text}]);
|
||||
env.expectNoSourceDiagnostics();
|
||||
const result = env.ngLS.getRenameInfo(_('/my-comp.ts'), cursor);
|
||||
expect(result.canRename).toEqual(false);
|
||||
});
|
||||
|
||||
it('gets rename info when cursor is outside template', () => {
|
||||
const {cursor, text} = extractCursorInfo(`
|
||||
import {Component, Input} from '@angular/core';
|
||||
|
||||
@Component({name: 'my-comp', template: ''})
|
||||
export class MyComp {
|
||||
@Input() m¦yProp!: string;
|
||||
}`);
|
||||
env = createModuleWithDeclarations([{name: _('/my-comp.ts'), contents: text}]);
|
||||
env.expectNoSourceDiagnostics();
|
||||
const result = env.ngLS.getRenameInfo(_('/my-comp.ts'), cursor) as ts.RenameInfoSuccess;
|
||||
expect(result.canRename).toEqual(true);
|
||||
expect(result.displayName).toEqual('myProp');
|
||||
expect(result.kind).toEqual('property');
|
||||
});
|
||||
|
||||
it('gets rename info on keyed read', () => {
|
||||
const {cursor, text} = extractCursorInfo(`
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({name: 'my-comp', template: '{{ myObj["my¦Prop"] }}'})
|
||||
export class MyComp {
|
||||
readonly myObj = {'myProp': 'hello world'};
|
||||
}`);
|
||||
env = createModuleWithDeclarations([{name: _('/my-comp.ts'), contents: text}]);
|
||||
env.expectNoSourceDiagnostics();
|
||||
const result = env.ngLS.getRenameInfo(_('/my-comp.ts'), cursor) as ts.RenameInfoSuccess;
|
||||
expect(result.canRename).toEqual(true);
|
||||
expect(result.displayName).toEqual('myProp');
|
||||
expect(result.kind).toEqual('property');
|
||||
expect(result.triggerSpan.length).toEqual('myProp'.length);
|
||||
});
|
||||
|
||||
it('gets rename info when cursor is on a directive input in a template', () => {
|
||||
const dirFile = {
|
||||
name: _('/dir.ts'),
|
||||
contents: `
|
||||
import {Directive, Input} from '@angular/core';
|
||||
@Directive({selector: '[dir]'})
|
||||
export class MyDir {
|
||||
@Input() dir!: any;
|
||||
}`
|
||||
};
|
||||
const {cursor, text} = extractCursorInfo(`
|
||||
import {Component, Input} from '@angular/core';
|
||||
|
||||
@Component({name: 'my-comp', template: '<div di¦r="something"></div>'})
|
||||
export class MyComp {
|
||||
@Input() myProp!: string;
|
||||
}`);
|
||||
env = createModuleWithDeclarations([{name: _('/my-comp.ts'), contents: text}, dirFile]);
|
||||
env.expectNoSourceDiagnostics();
|
||||
const result = env.ngLS.getRenameInfo(_('/my-comp.ts'), cursor) as ts.RenameInfoSuccess;
|
||||
expect(result.canRename).toEqual(true);
|
||||
expect(result.displayName).toEqual('dir');
|
||||
expect(result.kind).toEqual('property');
|
||||
});
|
||||
});
|
||||
|
||||
function getReferencesAtPosition(fileName: string, position: number) {
|
||||
env.expectNoSourceDiagnostics();
|
||||
const result = env.ngLS.getReferencesAtPosition(fileName, position);
|
||||
|
|
|
@ -71,6 +71,12 @@ export function create(info: ts.server.PluginCreateInfo): ts.LanguageService {
|
|||
return ngLS.findRenameLocations(fileName, position);
|
||||
}
|
||||
|
||||
function getRenameInfo(fileName: string, position: number): ts.RenameInfo {
|
||||
// See the comment in `findRenameLocations` explaining why we don't check the `angularOnly`
|
||||
// flag.
|
||||
return ngLS.getRenameInfo(fileName, position);
|
||||
}
|
||||
|
||||
function getCompletionsAtPosition(
|
||||
fileName: string, position: number,
|
||||
options: ts.GetCompletionsAtPositionOptions): ts.WithMetadata<ts.CompletionInfo>|undefined {
|
||||
|
@ -118,6 +124,7 @@ export function create(info: ts.server.PluginCreateInfo): ts.LanguageService {
|
|||
getDefinitionAndBoundSpan,
|
||||
getReferencesAtPosition,
|
||||
findRenameLocations,
|
||||
getRenameInfo,
|
||||
getCompletionsAtPosition,
|
||||
getCompletionEntryDetails,
|
||||
getCompletionEntrySymbol,
|
||||
|
|
|
@ -10,7 +10,6 @@ import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
|
|||
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 {Diagnostic as ngDiagnostic, isNgDiagnostic} from '@angular/compiler-cli/src/transformers/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';
|
||||
|
@ -33,9 +32,9 @@ export function getTextSpanOfNode(node: t.Node|e.AST): ts.TextSpan {
|
|||
}
|
||||
}
|
||||
|
||||
export function toTextSpan(span: AbsoluteSourceSpan|ParseSourceSpan): ts.TextSpan {
|
||||
export function toTextSpan(span: AbsoluteSourceSpan|ParseSourceSpan|e.ParseSpan): ts.TextSpan {
|
||||
let start: number, end: number;
|
||||
if (span instanceof AbsoluteSourceSpan) {
|
||||
if (span instanceof AbsoluteSourceSpan || span instanceof e.ParseSpan) {
|
||||
start = span.start;
|
||||
end = span.end;
|
||||
} else {
|
||||
|
|
Loading…
Reference in New Issue