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.dev/license
77 */
8- import { AST , TmplAstNode } from '@angular/compiler' ;
8+ import { AST , TmplAstComponent , TmplAstDirective , TmplAstNode } from '@angular/compiler' ;
99import { NgCompiler } from '@angular/compiler-cli/src/ngtsc/core' ;
1010import { absoluteFrom } from '@angular/compiler-cli/src/ngtsc/file_system' ;
1111import { MetaKind , PipeMeta } from '@angular/compiler-cli/src/ngtsc/metadata' ;
1212import { PerfPhase } from '@angular/compiler-cli/src/ngtsc/perf' ;
13- import { SymbolKind , TemplateTypeChecker } from '@angular/compiler-cli/src/ngtsc/typecheck/api' ;
13+ import {
14+ SelectorlessComponentSymbol ,
15+ SelectorlessDirectiveSymbol ,
16+ SymbolKind ,
17+ TemplateTypeChecker ,
18+ } from '@angular/compiler-cli/src/ngtsc/typecheck/api' ;
1419import ts from 'typescript' ;
1520
1621import {
1722 convertToTemplateDocumentSpan ,
1823 FilePosition ,
1924 getParentClassMeta ,
2025 getRenameTextAndSpanAtPosition ,
26+ getSelectorlessTemplateSpanFromTcbLocations ,
2127 getTargetDetailsAtTemplatePosition ,
2228 TemplateLocationDetails ,
2329} from './references_and_rename_utils' ;
@@ -96,6 +102,7 @@ enum RequestKind {
96102 DirectFromTypeScript ,
97103 PipeName ,
98104 Selector ,
105+ SelectorlessIdentifier ,
99106}
100107
101108/** The context needed to perform a rename of a pipe name. */
@@ -127,6 +134,20 @@ interface SelectorRenameContext {
127134 renamePosition : FilePosition ;
128135}
129136
137+ /** The context needed to perform a rename of a selectorless component/directive. */
138+ interface SelectorlessIdentifierRenameContext {
139+ type : RequestKind . SelectorlessIdentifier ;
140+
141+ /** Node defining the component/directive. */
142+ templateNode : TmplAstComponent | TmplAstDirective ;
143+
144+ /** Identifier of the class defining the class. */
145+ identifier : ts . Identifier ;
146+
147+ /** Location used for querying the TypeScript language service. */
148+ renamePosition : FilePosition ;
149+ }
150+
130151interface DirectFromTypescriptRenameContext {
131152 type : RequestKind . DirectFromTypeScript ;
132153
@@ -151,14 +172,19 @@ type IndirectRenameContext = PipeRenameContext | SelectorRenameContext;
151172type RenameRequest =
152173 | IndirectRenameContext
153174 | DirectFromTemplateRenameContext
154- | DirectFromTypescriptRenameContext ;
175+ | DirectFromTypescriptRenameContext
176+ | SelectorlessIdentifierRenameContext ;
155177
156178function isDirectRenameContext (
157179 context : RenameRequest ,
158- ) : context is DirectFromTemplateRenameContext | DirectFromTypescriptRenameContext {
180+ ) : context is
181+ | DirectFromTemplateRenameContext
182+ | DirectFromTypescriptRenameContext
183+ | SelectorlessIdentifierRenameContext {
159184 return (
160185 context . type === RequestKind . DirectFromTemplate ||
161- context . type === RequestKind . DirectFromTypeScript
186+ context . type === RequestKind . DirectFromTypeScript ||
187+ context . type === RequestKind . SelectorlessIdentifier
162188 ) ;
163189}
164190
@@ -200,6 +226,16 @@ export class RenameBuilder {
200226 start : renameRequest . pipeNameExpr . getStart ( ) + 1 ,
201227 } ,
202228 } ;
229+ } else if ( renameRequest . type === RequestKind . SelectorlessIdentifier ) {
230+ return {
231+ canRename : true ,
232+ displayName : renameRequest . identifier . text ,
233+ fullDisplayName : renameRequest . identifier . text ,
234+ triggerSpan : {
235+ length : renameRequest . identifier . text . length ,
236+ start : renameRequest . identifier . getStart ( ) ,
237+ } ,
238+ } ;
203239 } else {
204240 // TODO(atscott): Add support for other special indirect renames from typescript files.
205241 return this . tsLS . getRenameInfo ( filePath , position ) ;
@@ -299,18 +335,30 @@ export class RenameBuilder {
299335
300336 for ( const location of locations ) {
301337 if ( this . ttc . isTrackedTypeCheckFile ( absoluteFrom ( location . fileName ) ) ) {
302- const entry = convertToTemplateDocumentSpan (
303- location ,
304- this . ttc ,
305- this . compiler . getCurrentProgram ( ) ,
306- expectedRenameText ,
307- ) ;
308- // There is no template node whose text matches the original rename request. Bail on
309- // renaming completely rather than providing incomplete results.
310- if ( entry === null ) {
311- return null ;
338+ if ( renameRequest . type === RequestKind . SelectorlessIdentifier ) {
339+ const selectorlessEntries = getSelectorlessTemplateSpanFromTcbLocations (
340+ location ,
341+ this . ttc ,
342+ this . compiler . getCurrentProgram ( ) ,
343+ renameRequest . templateNode ,
344+ ) ;
345+ if ( selectorlessEntries !== null ) {
346+ entries . push ( ...selectorlessEntries ) ;
347+ }
348+ } else {
349+ const entry = convertToTemplateDocumentSpan (
350+ location ,
351+ this . ttc ,
352+ this . compiler . getCurrentProgram ( ) ,
353+ expectedRenameText ,
354+ ) ;
355+ // There is no template node whose text matches the original rename request. Bail on
356+ // renaming completely rather than providing incomplete results.
357+ if ( entry === null ) {
358+ return null ;
359+ }
360+ entries . push ( entry ) ;
312361 }
313- entries . push ( entry ) ;
314362 } else {
315363 if ( ! isDirectRenameContext ( renameRequest ) ) {
316364 // Discard any non-template results for non-direct renames. We should only rename
@@ -358,6 +406,17 @@ export class RenameBuilder {
358406 return null ;
359407 }
360408 renameRequests . push ( renameRequest ) ;
409+ } else if (
410+ targetDetails . symbol . kind === SymbolKind . SelectorlessComponent ||
411+ targetDetails . symbol . kind === SymbolKind . SelectorlessDirective
412+ ) {
413+ const renameRequest = this . buildSelectorlessRenameRequestFromTemplate (
414+ targetDetails . symbol ,
415+ ) ;
416+ if ( renameRequest === null ) {
417+ return null ;
418+ }
419+ renameRequests . push ( renameRequest ) ;
361420 } else {
362421 const renameRequest : RenameRequest = {
363422 type : RequestKind . DirectFromTemplate ,
@@ -413,6 +472,40 @@ export class RenameBuilder {
413472 } ,
414473 } ;
415474 }
475+
476+ private buildSelectorlessRenameRequestFromTemplate (
477+ symbol : SelectorlessComponentSymbol | SelectorlessDirectiveSymbol ,
478+ ) : SelectorlessIdentifierRenameContext | null {
479+ if ( symbol . tsSymbol === null || symbol . tsSymbol . valueDeclaration === undefined ) {
480+ return null ;
481+ }
482+
483+ const meta = this . compiler . getMeta ( symbol . tsSymbol . valueDeclaration ) ;
484+ if ( meta === null || meta . kind !== MetaKind . Directive ) {
485+ return null ;
486+ }
487+
488+ const nameNode = meta . ref . node . name ;
489+ const templateName =
490+ symbol . kind === SymbolKind . SelectorlessComponent
491+ ? symbol . templateNode . componentName
492+ : symbol . templateNode . name ;
493+
494+ // Do not rename aliased references.
495+ if ( templateName !== nameNode . text ) {
496+ return null ;
497+ }
498+
499+ return {
500+ type : RequestKind . SelectorlessIdentifier ,
501+ templateNode : symbol . templateNode ,
502+ identifier : nameNode ,
503+ renamePosition : {
504+ fileName : meta . ref . node . getSourceFile ( ) . fileName ,
505+ position : nameNode . getStart ( ) ,
506+ } ,
507+ } ;
508+ }
416509}
417510
418511/**
@@ -444,6 +537,14 @@ function getExpectedRenameTextAndInitialRenameEntries(
444537 textSpan : { start : pipeNameExpr . getStart ( ) + 1 , length : pipeNameExpr . getText ( ) . length - 2 } ,
445538 } ;
446539 entries . push ( entry ) ;
540+ } else if ( renameRequest . type === RequestKind . SelectorlessIdentifier ) {
541+ const { identifier} = renameRequest ;
542+ expectedRenameText = identifier . text ;
543+ const entry : ts . RenameLocation = {
544+ fileName : identifier . getSourceFile ( ) . fileName ,
545+ textSpan : { start : identifier . getStart ( ) , length : identifier . getWidth ( ) } ,
546+ } ;
547+ entries . push ( entry ) ;
447548 } else {
448549 // TODO(atscott): Implement other types of special renames
449550 return null ;
0 commit comments