Skip to content

Commit 4e8198d

Browse files
atscottAndrewKushnir
authored andcommitted
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
1 parent 40e0bfd commit 4e8198d

File tree

5 files changed

+167
-25
lines changed

5 files changed

+167
-25
lines changed

packages/language-service/ivy/language_service.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {AST, TmplAstBoundEvent, TmplAstNode} from '@angular/compiler';
1010
import {CompilerOptions, ConfigurationHost, readConfiguration} from '@angular/compiler-cli';
11-
import {absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
11+
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
1212
import {TypeCheckShimGenerator} from '@angular/compiler-cli/src/ngtsc/typecheck';
1313
import {OptimizeFor, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
1414
import * as ts from 'typescript/lib/tsserverlibrary';
@@ -113,6 +113,21 @@ export class LanguageService {
113113
return results;
114114
}
115115

116+
getRenameInfo(fileName: string, position: number): ts.RenameInfo {
117+
const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName);
118+
const renameInfo = new ReferencesAndRenameBuilder(this.strategy, this.tsLS, compiler)
119+
.getRenameInfo(absoluteFrom(fileName), position);
120+
if (!renameInfo.canRename) {
121+
return renameInfo;
122+
}
123+
124+
const quickInfo = this.getQuickInfoAtPosition(fileName, position) ??
125+
this.tsLS.getQuickInfoAtPosition(fileName, position);
126+
const kind = quickInfo?.kind ?? ts.ScriptElementKind.unknown;
127+
const kindModifiers = quickInfo?.kindModifiers ?? ts.ScriptElementKind.unknown;
128+
return {...renameInfo, kind, kindModifiers};
129+
}
130+
116131
findRenameLocations(fileName: string, position: number): readonly ts.RenameLocation[]|undefined {
117132
const compiler = this.compilerFactory.getOrCreateWithChangedFile(fileName);
118133
const results = new ReferencesAndRenameBuilder(this.strategy, this.tsLS, compiler)

packages/language-service/ivy/references.ts

Lines changed: 67 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import {AST, BindingPipe, LiteralPrimitive, MethodCall, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstNode, TmplAstReference, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
8+
import {AbsoluteSourceSpan, AST, BindingPipe, LiteralPrimitive, MethodCall, ParseSourceSpan, PropertyRead, PropertyWrite, SafeMethodCall, SafePropertyRead, TmplAstBoundAttribute, TmplAstBoundEvent, TmplAstNode, TmplAstReference, TmplAstTextAttribute, TmplAstVariable} from '@angular/compiler';
99
import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
1010
import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '@angular/compiler-cli/src/ngtsc/file_system';
1111
import {DirectiveSymbol, ShimLocation, SymbolKind, TemplateTypeChecker, TypeCheckingProgramStrategy} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
@@ -14,7 +14,7 @@ import * as ts from 'typescript';
1414

1515
import {getTargetAtPosition, TargetNodeKind} from './template_target';
1616
import {findTightestNode} from './ts_utils';
17-
import {getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, isTemplateNode, isWithin, TemplateInfo, toTextSpan} from './utils';
17+
import {getDirectiveMatchesForAttribute, getDirectiveMatchesForElementTag, getTemplateInfoAtPosition, isWithin, TemplateInfo, toTextSpan} from './utils';
1818

1919
interface FilePosition {
2020
fileName: string;
@@ -64,10 +64,37 @@ export class ReferencesAndRenameBuilder {
6464
private readonly strategy: TypeCheckingProgramStrategy,
6565
private readonly tsLS: ts.LanguageService, private readonly compiler: NgCompiler) {}
6666

67+
getRenameInfo(filePath: string, position: number):
68+
Omit<ts.RenameInfoSuccess, 'kind'|'kindModifiers'>|ts.RenameInfoFailure {
69+
const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler);
70+
// We could not get a template at position so we assume the request came from outside the
71+
// template.
72+
if (templateInfo === undefined) {
73+
return this.tsLS.getRenameInfo(filePath, position);
74+
}
75+
76+
const allTargetDetails = this.getTargetDetailsAtTemplatePosition(templateInfo, position);
77+
if (allTargetDetails === null) {
78+
return {canRename: false, localizedErrorMessage: 'Could not find template node at position.'};
79+
}
80+
const {templateTarget} = allTargetDetails[0];
81+
const templateTextAndSpan = getRenameTextAndSpanAtPosition(templateTarget, position);
82+
if (templateTextAndSpan === null) {
83+
return {canRename: false, localizedErrorMessage: 'Could not determine template node text.'};
84+
}
85+
const {text, span} = templateTextAndSpan;
86+
return {
87+
canRename: true,
88+
displayName: text,
89+
fullDisplayName: text,
90+
triggerSpan: toTextSpan(span),
91+
};
92+
}
93+
6794
findRenameLocations(filePath: string, position: number): readonly ts.RenameLocation[]|undefined {
6895
this.ttc.generateAllTypeCheckBlocks();
6996
const templateInfo = getTemplateInfoAtPosition(filePath, position, this.compiler);
70-
// We could not get a template at position so we assume the request is came from outside the
97+
// We could not get a template at position so we assume the request came from outside the
7198
// template.
7299
if (templateInfo === undefined) {
73100
const requestNode = this.getTsNodeAtPosition(filePath, position);
@@ -126,11 +153,11 @@ export class ReferencesAndRenameBuilder {
126153
originalNodeText = requestOrigin.requestNode.getText();
127154
} else {
128155
const templateNodeText =
129-
getTemplateNodeRenameTextAtPosition(requestOrigin.requestNode, requestOrigin.position);
156+
getRenameTextAndSpanAtPosition(requestOrigin.requestNode, requestOrigin.position);
130157
if (templateNodeText === null) {
131158
return undefined;
132159
}
133-
originalNodeText = templateNodeText;
160+
originalNodeText = templateNodeText.text;
134161
}
135162

136163
const locations = this.tsLS.findRenameLocations(
@@ -207,11 +234,11 @@ export class ReferencesAndRenameBuilder {
207234
for (const node of nodes) {
208235
// Get the information about the TCB at the template position.
209236
const symbol = this.ttc.getSymbolOfNode(node, component);
210-
const templateTarget = node;
211-
212237
if (symbol === null) {
213238
continue;
214239
}
240+
241+
const templateTarget = node;
215242
switch (symbol.kind) {
216243
case SymbolKind.Directive:
217244
case SymbolKind.Template:
@@ -233,13 +260,17 @@ export class ReferencesAndRenameBuilder {
233260
}
234261
const directives = getDirectiveMatchesForAttribute(
235262
node.name, symbol.host.templateNode, symbol.host.directives);
236-
details.push(
237-
{typescriptLocations: this.getPositionsForDirectives(directives), templateTarget});
263+
details.push({
264+
typescriptLocations: this.getPositionsForDirectives(directives),
265+
templateTarget,
266+
});
238267
break;
239268
}
240269
case SymbolKind.Reference: {
241-
details.push(
242-
{typescriptLocations: [toFilePosition(symbol.referenceVarLocation)], templateTarget});
270+
details.push({
271+
typescriptLocations: [toFilePosition(symbol.referenceVarLocation)],
272+
templateTarget,
273+
});
243274
break;
244275
}
245276
case SymbolKind.Variable: {
@@ -253,14 +284,18 @@ export class ReferencesAndRenameBuilder {
253284
});
254285
} else if (isWithin(position, templateTarget.keySpan)) {
255286
// In the keySpan of the variable, we want to get the reference of the local variable.
256-
details.push(
257-
{typescriptLocations: [toFilePosition(symbol.localVarLocation)], templateTarget});
287+
details.push({
288+
typescriptLocations: [toFilePosition(symbol.localVarLocation)],
289+
templateTarget,
290+
});
258291
}
259292
} else {
260293
// If the templateNode is not the `TmplAstVariable`, it must be a usage of the
261294
// variable somewhere in the template.
262-
details.push(
263-
{typescriptLocations: [toFilePosition(symbol.localVarLocation)], templateTarget});
295+
details.push({
296+
typescriptLocations: [toFilePosition(symbol.localVarLocation)],
297+
templateTarget,
298+
});
264299
}
265300
break;
266301
}
@@ -374,15 +409,19 @@ export class ReferencesAndRenameBuilder {
374409
}
375410
}
376411

377-
function getTemplateNodeRenameTextAtPosition(node: TmplAstNode|AST, position: number): string|null {
412+
function getRenameTextAndSpanAtPosition(node: TmplAstNode|AST, position: number):
413+
{text: string, span: ParseSourceSpan|AbsoluteSourceSpan}|null {
378414
if (node instanceof TmplAstBoundAttribute || node instanceof TmplAstTextAttribute ||
379415
node instanceof TmplAstBoundEvent) {
380-
return node.name;
416+
if (node.keySpan === undefined) {
417+
return null;
418+
}
419+
return {text: node.name, span: node.keySpan};
381420
} else if (node instanceof TmplAstVariable || node instanceof TmplAstReference) {
382421
if (isWithin(position, node.keySpan)) {
383-
return node.keySpan.toString();
422+
return {text: node.keySpan.toString(), span: node.keySpan};
384423
} else if (node.valueSpan && isWithin(position, node.valueSpan)) {
385-
return node.valueSpan.toString();
424+
return {text: node.valueSpan.toString(), span: node.valueSpan};
386425
}
387426
}
388427

@@ -392,9 +431,16 @@ function getTemplateNodeRenameTextAtPosition(node: TmplAstNode|AST, position: nu
392431
}
393432
if (node instanceof PropertyRead || node instanceof MethodCall || node instanceof PropertyWrite ||
394433
node instanceof SafePropertyRead || node instanceof SafeMethodCall) {
395-
return node.name;
434+
return {text: node.name, span: node.nameSpan};
396435
} else if (node instanceof LiteralPrimitive) {
397-
return node.value;
436+
const span = node.span;
437+
const text = node.value;
438+
if (typeof text === 'string') {
439+
// The span of a string literal includes the quotes but they should be removed for renaming.
440+
span.start += 1;
441+
span.end -= 1;
442+
}
443+
return {text, span};
398444
}
399445

400446
return null;

packages/language-service/ivy/test/references_spec.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1369,6 +1369,81 @@ describe('find references and rename locations', () => {
13691369
});
13701370
});
13711371

1372+
describe('get rename info', () => {
1373+
it('indicates inability to rename when cursor is outside template and in a string literal',
1374+
() => {
1375+
const {cursor, text} = extractCursorInfo(`
1376+
import {Component} from '@angular/core';
1377+
1378+
@Component({selector: 'my-comp', template: ''})
1379+
export class MyComp {
1380+
myProp = 'cannot rena¦me me';
1381+
}`);
1382+
env = createModuleWithDeclarations([{name: _('/my-comp.ts'), contents: text}]);
1383+
env.expectNoSourceDiagnostics();
1384+
const result = env.ngLS.getRenameInfo(_('/my-comp.ts'), cursor);
1385+
expect(result.canRename).toEqual(false);
1386+
});
1387+
1388+
it('gets rename info when cursor is outside template', () => {
1389+
const {cursor, text} = extractCursorInfo(`
1390+
import {Component, Input} from '@angular/core';
1391+
1392+
@Component({name: 'my-comp', template: ''})
1393+
export class MyComp {
1394+
@Input() m¦yProp!: string;
1395+
}`);
1396+
env = createModuleWithDeclarations([{name: _('/my-comp.ts'), contents: text}]);
1397+
env.expectNoSourceDiagnostics();
1398+
const result = env.ngLS.getRenameInfo(_('/my-comp.ts'), cursor) as ts.RenameInfoSuccess;
1399+
expect(result.canRename).toEqual(true);
1400+
expect(result.displayName).toEqual('myProp');
1401+
expect(result.kind).toEqual('property');
1402+
});
1403+
1404+
it('gets rename info on keyed read', () => {
1405+
const {cursor, text} = extractCursorInfo(`
1406+
import {Component} from '@angular/core';
1407+
1408+
@Component({name: 'my-comp', template: '{{ myObj["my¦Prop"] }}'})
1409+
export class MyComp {
1410+
readonly myObj = {'myProp': 'hello world'};
1411+
}`);
1412+
env = createModuleWithDeclarations([{name: _('/my-comp.ts'), contents: text}]);
1413+
env.expectNoSourceDiagnostics();
1414+
const result = env.ngLS.getRenameInfo(_('/my-comp.ts'), cursor) as ts.RenameInfoSuccess;
1415+
expect(result.canRename).toEqual(true);
1416+
expect(result.displayName).toEqual('myProp');
1417+
expect(result.kind).toEqual('property');
1418+
expect(result.triggerSpan.length).toEqual('myProp'.length);
1419+
});
1420+
1421+
it('gets rename info when cursor is on a directive input in a template', () => {
1422+
const dirFile = {
1423+
name: _('/dir.ts'),
1424+
contents: `
1425+
import {Directive, Input} from '@angular/core';
1426+
@Directive({selector: '[dir]'})
1427+
export class MyDir {
1428+
@Input() dir!: any;
1429+
}`
1430+
};
1431+
const {cursor, text} = extractCursorInfo(`
1432+
import {Component, Input} from '@angular/core';
1433+
1434+
@Component({name: 'my-comp', template: '<div di¦r="something"></div>'})
1435+
export class MyComp {
1436+
@Input() myProp!: string;
1437+
}`);
1438+
env = createModuleWithDeclarations([{name: _('/my-comp.ts'), contents: text}, dirFile]);
1439+
env.expectNoSourceDiagnostics();
1440+
const result = env.ngLS.getRenameInfo(_('/my-comp.ts'), cursor) as ts.RenameInfoSuccess;
1441+
expect(result.canRename).toEqual(true);
1442+
expect(result.displayName).toEqual('dir');
1443+
expect(result.kind).toEqual('property');
1444+
});
1445+
});
1446+
13721447
function getReferencesAtPosition(fileName: string, position: number) {
13731448
env.expectNoSourceDiagnostics();
13741449
const result = env.ngLS.getReferencesAtPosition(fileName, position);

packages/language-service/ivy/ts_plugin.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ export function create(info: ts.server.PluginCreateInfo): ts.LanguageService {
7171
return ngLS.findRenameLocations(fileName, position);
7272
}
7373

74+
function getRenameInfo(fileName: string, position: number): ts.RenameInfo {
75+
// See the comment in `findRenameLocations` explaining why we don't check the `angularOnly`
76+
// flag.
77+
return ngLS.getRenameInfo(fileName, position);
78+
}
79+
7480
function getCompletionsAtPosition(
7581
fileName: string, position: number,
7682
options: ts.GetCompletionsAtPositionOptions): ts.WithMetadata<ts.CompletionInfo>|undefined {
@@ -118,6 +124,7 @@ export function create(info: ts.server.PluginCreateInfo): ts.LanguageService {
118124
getDefinitionAndBoundSpan,
119125
getReferencesAtPosition,
120126
findRenameLocations,
127+
getRenameInfo,
121128
getCompletionsAtPosition,
122129
getCompletionEntryDetails,
123130
getCompletionEntrySymbol,

packages/language-service/ivy/utils.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core';
1010
import {isExternalResource} from '@angular/compiler-cli/src/ngtsc/metadata';
1111
import {DeclarationNode} from '@angular/compiler-cli/src/ngtsc/reflection';
1212
import {DirectiveSymbol} from '@angular/compiler-cli/src/ngtsc/typecheck/api';
13-
import {Diagnostic as ngDiagnostic, isNgDiagnostic} from '@angular/compiler-cli/src/transformers/api';
1413
import * as e from '@angular/compiler/src/expression_parser/ast'; // e for expression AST
1514
import * as t from '@angular/compiler/src/render3/r3_ast'; // t for template AST
1615
import * as ts from 'typescript';
@@ -33,9 +32,9 @@ export function getTextSpanOfNode(node: t.Node|e.AST): ts.TextSpan {
3332
}
3433
}
3534

36-
export function toTextSpan(span: AbsoluteSourceSpan|ParseSourceSpan): ts.TextSpan {
35+
export function toTextSpan(span: AbsoluteSourceSpan|ParseSourceSpan|e.ParseSpan): ts.TextSpan {
3736
let start: number, end: number;
38-
if (span instanceof AbsoluteSourceSpan) {
37+
if (span instanceof AbsoluteSourceSpan || span instanceof e.ParseSpan) {
3938
start = span.start;
4039
end = span.end;
4140
} else {

0 commit comments

Comments
 (0)