diff --git a/packages/language-service/src/references_and_rename.ts b/packages/language-service/src/references_and_rename.ts index babb1557120..b100df918e8 100644 --- a/packages/language-service/src/references_and_rename.ts +++ b/packages/language-service/src/references_and_rename.ts @@ -5,12 +5,17 @@ * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ -import {AST, TmplAstNode} from '@angular/compiler'; +import {AST, TmplAstComponent, TmplAstDirective, TmplAstNode} from '@angular/compiler'; import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core'; import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; import {MetaKind, PipeMeta} from '@angular/compiler-cli/src/ngtsc/metadata'; import {PerfPhase} from '@angular/compiler-cli/src/ngtsc/perf'; -import {SymbolKind, TemplateTypeChecker} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; +import { + SelectorlessComponentSymbol, + SelectorlessDirectiveSymbol, + SymbolKind, + TemplateTypeChecker, +} from '@angular/compiler-cli/src/ngtsc/typecheck/api'; import ts from 'typescript'; import { @@ -18,6 +23,7 @@ import { FilePosition, getParentClassMeta, getRenameTextAndSpanAtPosition, + getSelectorlessTemplateSpanFromTcbLocations, getTargetDetailsAtTemplatePosition, TemplateLocationDetails, } from './references_and_rename_utils'; @@ -96,6 +102,7 @@ enum RequestKind { DirectFromTypeScript, PipeName, Selector, + SelectorlessIdentifier, } /** The context needed to perform a rename of a pipe name. */ @@ -127,6 +134,20 @@ interface SelectorRenameContext { renamePosition: FilePosition; } +/** The context needed to perform a rename of a selectorless component/directive. */ +interface SelectorlessIdentifierRenameContext { + type: RequestKind.SelectorlessIdentifier; + + /** Node defining the component/directive. */ + templateNode: TmplAstComponent | TmplAstDirective; + + /** Identifier of the class defining the class. */ + identifier: ts.Identifier; + + /** Location used for querying the TypeScript language service. */ + renamePosition: FilePosition; +} + interface DirectFromTypescriptRenameContext { type: RequestKind.DirectFromTypeScript; @@ -151,14 +172,19 @@ type IndirectRenameContext = PipeRenameContext | SelectorRenameContext; type RenameRequest = | IndirectRenameContext | DirectFromTemplateRenameContext - | DirectFromTypescriptRenameContext; + | DirectFromTypescriptRenameContext + | SelectorlessIdentifierRenameContext; function isDirectRenameContext( context: RenameRequest, -): context is DirectFromTemplateRenameContext | DirectFromTypescriptRenameContext { +): context is + | DirectFromTemplateRenameContext + | DirectFromTypescriptRenameContext + | SelectorlessIdentifierRenameContext { return ( context.type === RequestKind.DirectFromTemplate || - context.type === RequestKind.DirectFromTypeScript + context.type === RequestKind.DirectFromTypeScript || + context.type === RequestKind.SelectorlessIdentifier ); } @@ -200,6 +226,16 @@ export class RenameBuilder { start: renameRequest.pipeNameExpr.getStart() + 1, }, }; + } else if (renameRequest.type === RequestKind.SelectorlessIdentifier) { + return { + canRename: true, + displayName: renameRequest.identifier.text, + fullDisplayName: renameRequest.identifier.text, + triggerSpan: { + length: renameRequest.identifier.text.length, + start: renameRequest.identifier.getStart(), + }, + }; } else { // TODO(atscott): Add support for other special indirect renames from typescript files. return this.tsLS.getRenameInfo(filePath, position); @@ -299,18 +335,30 @@ export class RenameBuilder { for (const location of locations) { if (this.ttc.isTrackedTypeCheckFile(absoluteFrom(location.fileName))) { - const entry = convertToTemplateDocumentSpan( - location, - this.ttc, - this.compiler.getCurrentProgram(), - expectedRenameText, - ); - // There is no template node whose text matches the original rename request. Bail on - // renaming completely rather than providing incomplete results. - if (entry === null) { - return null; + if (renameRequest.type === RequestKind.SelectorlessIdentifier) { + const selectorlessEntries = getSelectorlessTemplateSpanFromTcbLocations( + location, + this.ttc, + this.compiler.getCurrentProgram(), + renameRequest.templateNode, + ); + if (selectorlessEntries !== null) { + entries.push(...selectorlessEntries); + } + } else { + const entry = convertToTemplateDocumentSpan( + location, + this.ttc, + this.compiler.getCurrentProgram(), + expectedRenameText, + ); + // There is no template node whose text matches the original rename request. Bail on + // renaming completely rather than providing incomplete results. + if (entry === null) { + return null; + } + entries.push(entry); } - entries.push(entry); } else { if (!isDirectRenameContext(renameRequest)) { // Discard any non-template results for non-direct renames. We should only rename @@ -358,6 +406,17 @@ export class RenameBuilder { return null; } renameRequests.push(renameRequest); + } else if ( + targetDetails.symbol.kind === SymbolKind.SelectorlessComponent || + targetDetails.symbol.kind === SymbolKind.SelectorlessDirective + ) { + const renameRequest = this.buildSelectorlessRenameRequestFromTemplate( + targetDetails.symbol, + ); + if (renameRequest === null) { + return null; + } + renameRequests.push(renameRequest); } else { const renameRequest: RenameRequest = { type: RequestKind.DirectFromTemplate, @@ -413,6 +472,40 @@ export class RenameBuilder { }, }; } + + private buildSelectorlessRenameRequestFromTemplate( + symbol: SelectorlessComponentSymbol | SelectorlessDirectiveSymbol, + ): SelectorlessIdentifierRenameContext | null { + if (symbol.tsSymbol === null || symbol.tsSymbol.valueDeclaration === undefined) { + return null; + } + + const meta = this.compiler.getMeta(symbol.tsSymbol.valueDeclaration); + if (meta === null || meta.kind !== MetaKind.Directive) { + return null; + } + + const nameNode = meta.ref.node.name; + const templateName = + symbol.kind === SymbolKind.SelectorlessComponent + ? symbol.templateNode.componentName + : symbol.templateNode.name; + + // Do not rename aliased references. + if (templateName !== nameNode.text) { + return null; + } + + return { + type: RequestKind.SelectorlessIdentifier, + templateNode: symbol.templateNode, + identifier: nameNode, + renamePosition: { + fileName: meta.ref.node.getSourceFile().fileName, + position: nameNode.getStart(), + }, + }; + } } /** @@ -444,6 +537,14 @@ function getExpectedRenameTextAndInitialRenameEntries( textSpan: {start: pipeNameExpr.getStart() + 1, length: pipeNameExpr.getText().length - 2}, }; entries.push(entry); + } else if (renameRequest.type === RequestKind.SelectorlessIdentifier) { + const {identifier} = renameRequest; + expectedRenameText = identifier.text; + const entry: ts.RenameLocation = { + fileName: identifier.getSourceFile().fileName, + textSpan: {start: identifier.getStart(), length: identifier.getWidth()}, + }; + entries.push(entry); } else { // TODO(atscott): Implement other types of special renames return null; diff --git a/packages/language-service/src/references_and_rename_utils.ts b/packages/language-service/src/references_and_rename_utils.ts index 717d053674e..ed286bb04be 100644 --- a/packages/language-service/src/references_and_rename_utils.ts +++ b/packages/language-service/src/references_and_rename_utils.ts @@ -20,12 +20,16 @@ import { TmplAstReference, TmplAstTextAttribute, TmplAstVariable, + TmplAstComponent, + TmplAstDirective, } from '@angular/compiler'; import {NgCompiler} from '@angular/compiler-cli/src/ngtsc/core'; import {absoluteFrom} from '@angular/compiler-cli/src/ngtsc/file_system'; import {DirectiveMeta, PipeMeta} from '@angular/compiler-cli/src/ngtsc/metadata'; import { DirectiveSymbol, + SelectorlessComponentSymbol, + SelectorlessDirectiveSymbol, Symbol, SymbolKind, TcbLocation, @@ -216,6 +220,17 @@ export function getTargetDetailsAtTemplatePosition( }); break; } + case SymbolKind.SelectorlessDirective: + case SymbolKind.SelectorlessComponent: + const dirPosition = getPositionForDirective(symbol); + if (dirPosition !== null) { + details.push({ + typescriptLocations: [dirPosition], + templateTarget, + symbol, + }); + } + break; } } @@ -228,17 +243,31 @@ export function getTargetDetailsAtTemplatePosition( function getPositionsForDirectives(directives: Set): 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 position = getPositionForDirective(dir); + if (position !== null) { + allDirectives.push(position); } + } + return allDirectives; +} - const {fileName} = dirClass.getSourceFile(); - const position = dirClass.name.getStart(); - allDirectives.push({fileName, position}); +/** Gets the `FilePosition` for a single directive symbol. */ +function getPositionForDirective( + directive: DirectiveSymbol | SelectorlessComponentSymbol | SelectorlessDirectiveSymbol, +): FilePosition | null { + const declaration = directive.tsSymbol?.valueDeclaration; + + if ( + declaration !== undefined && + ts.isClassDeclaration(declaration) && + declaration.name !== undefined + ) { + const {fileName} = declaration.getSourceFile(); + const position = declaration.name.getStart(); + return {fileName, position}; } - return allDirectives; + return null; } /** @@ -358,8 +387,10 @@ export function getRenameTextAndSpanAtPosition( span.length -= 2; } return {text, span}; - } else if (node instanceof TmplAstElement) { + } else if (node instanceof TmplAstElement || node instanceof TmplAstDirective) { return {text: node.name, span: toTextSpan(node.startSourceSpan)}; + } else if (node instanceof TmplAstComponent) { + return {text: node.componentName, span: toTextSpan(node.startSourceSpan)}; } return null; @@ -380,3 +411,72 @@ export function getParentClassMeta( } return compiler.getMeta(parentClass); } + +/** + * Converts a given `ts.DocumentSpan` in a shim file into one or more spans in the template, + * representing a selectorless component or directive. There can be more than one return value + * when a component has a closing tag. + */ +export function getSelectorlessTemplateSpanFromTcbLocations( + shimDocumentSpan: ts.DocumentSpan, + templateTypeChecker: TemplateTypeChecker, + program: ts.Program, + node: TmplAstComponent | TmplAstDirective, +): ts.DocumentSpan[] | null { + const sf = program.getSourceFile(shimDocumentSpan.fileName); + if (sf === undefined) { + return null; + } + + let tcbNode = findTightestNode(sf, shimDocumentSpan.textSpan.start); + if (tcbNode === undefined) { + return null; + } + + // Variables in the typecheck block are generated with the type on the right hand + // side: `var _t1 = null! as i1.DirA`. Finding references of DirA will return the type + // assertion and we need to map it back to the variable identifier _t1. + if (hasExpressionIdentifier(sf, tcbNode, ExpressionIdentifier.VARIABLE_AS_EXPRESSION)) { + while (tcbNode && !ts.isVariableDeclaration(tcbNode)) { + tcbNode = tcbNode.parent; + } + } + + const mapping = getTemplateLocationFromTcbLocation( + templateTypeChecker, + absoluteFrom(shimDocumentSpan.fileName), + /* tcbIsShim */ true, + tcbNode.getStart(), + ); + + if (mapping === null) { + return null; + } + + const fileName = mapping.templateUrl; + const {length} = node instanceof TmplAstComponent ? node.componentName : node.name; + const spans: ts.DocumentSpan[] = [ + { + fileName, + textSpan: { + // +1 because of the opening `<` or `@`. + start: node.startSourceSpan.start.offset + 1, + length, + }, + }, + ]; + + // If it's not a self-closing template tag, we need to rename the end tag too. + if (node instanceof TmplAstComponent && node.endSourceSpan?.toString().startsWith(' { @@ -2109,6 +2110,446 @@ describe('find references and rename locations', () => { }); }); + describe('selectorless components', () => { + let project: Project; + + beforeEach(() => { + initMockFileSystem('Native'); + env = LanguageServiceTestEnv.setup(); + project = env.addProject( + 'test', + { + 'app.ts': ` + import {Component} from '@angular/core'; + import {TestComponent} from './test-component'; + + @Component({templateUrl: './app.html'}) + export class AppCmp { + stringValue = 'hello'; + handleEvent() {} + } + `, + 'test-component.ts': ` + import {Component, EventEmitter, Input, Output} from '@angular/core'; + + @Component({template: ''}) + export class TestComponent { + @Input() name!: string; + @Output() testEvent = new EventEmitter(); + } + `, + 'app.html': 'Will be overridden', + }, + {_enableSelectorless: true}, + ); + }); + + describe('references', () => { + it('should find references to selectorless component from source file', () => { + const file = project.openFile('test-component.ts'); + const template = project.openFile('app.html'); + template.contents = ''; + file.moveCursorToText('export class TestComp¦onent {'); + const refs = getReferencesAtPosition(file)!; + expect(refs.length).toBe(3); + assertTextSpans(refs, ['TestComponent', '']); + assertFileNames(refs, ['test-component.ts', 'app.html', 'app.ts']); + }); + + it('should find references to selectorless component from template', () => { + const template = project.openFile('app.html'); + template.contents = ''; + template.moveCursorToText(''); + const refs = getReferencesAtPosition(template)!; + expect(refs.length).toBe(3); + assertTextSpans(refs, ['TestComponent', '']); + assertFileNames(refs, ['test-component.ts', 'app.html', 'app.ts']); + }); + + it('should find references to selectorless component inputs from source file', () => { + const file = project.openFile('test-component.ts'); + const template = project.openFile('app.html'); + template.contents = ''; + file.moveCursorToText('@Input() na¦me!: string;'); + const refs = getReferencesAtPosition(file)!; + expect(refs.length).toBe(2); + assertTextSpans(refs, ['name']); + assertFileNames(refs, ['test-component.ts', 'app.html']); + }); + + it('should find references to selectorless component inputs from template', () => { + const template = project.openFile('app.html'); + template.contents = ''; + template.moveCursorToText('[na¦me]'); + const refs = getReferencesAtPosition(template)!; + expect(refs.length).toBe(2); + assertTextSpans(refs, ['name']); + assertFileNames(refs, ['test-component.ts', 'app.html']); + }); + + it('should find references to selectorless component outputs from source file', () => { + const file = project.openFile('test-component.ts'); + const template = project.openFile('app.html'); + template.contents = ''; + file.moveCursorToText('@Output() test¦Event = new EventEmitter();'); + const refs = getReferencesAtPosition(file)!; + expect(refs.length).toBe(2); + assertTextSpans(refs, ['testEvent']); + assertFileNames(refs, ['test-component.ts', 'app.html']); + }); + + it('should find references to selectorless component outputs from template', () => { + const template = project.openFile('app.html'); + template.contents = ''; + template.moveCursorToText('(tes¦tEvent)'); + const refs = getReferencesAtPosition(template)!; + expect(refs.length).toBe(2); + assertTextSpans(refs, ['testEvent']); + assertFileNames(refs, ['test-component.ts', 'app.html']); + }); + }); + + describe('rename locations', () => { + it('should find rename locations of selectorless component from template', () => { + const template = project.openFile('app.html'); + template.contents = ''; + template.moveCursorToText(''); + const renameLocations = getRenameLocationsAtPosition(template)!; + + // There are 3 locations that need to be renamed: + // - Source file where the component is defined. + // - Self-closing tag in the template. + // - Import in the app component. + expect(renameLocations.length).toBe(3); + assertTextSpans(renameLocations, ['TestComponent']); + assertFileNames(renameLocations, ['test-component.ts', 'app.html', 'app.ts']); + }); + + it('should find rename locations for complex selectorless component', () => { + const template = project.openFile('app.html'); + template.contents = 'Hello'; + template.moveCursorToText(' { + const file = project.openFile('test-component.ts'); + const template = project.openFile('app.html'); + template.contents = ''; + file.moveCursorToText('@Input() na¦me!: string;'); + const refs = getRenameLocationsAtPosition(file)!; + expect(refs.length).toBe(2); + assertTextSpans(refs, ['name']); + assertFileNames(refs, ['test-component.ts', 'app.html']); + }); + + it('should find rename locations to selectorless component inputs from template', () => { + const template = project.openFile('app.html'); + template.contents = ''; + template.moveCursorToText('[na¦me]'); + const refs = getRenameLocationsAtPosition(template)!; + expect(refs.length).toBe(2); + assertTextSpans(refs, ['name']); + assertFileNames(refs, ['test-component.ts', 'app.html']); + }); + + it('should find rename locations to selectorless component outputs from source file', () => { + const file = project.openFile('test-component.ts'); + const template = project.openFile('app.html'); + template.contents = ''; + file.moveCursorToText('@Output() test¦Event = new EventEmitter();'); + const refs = getRenameLocationsAtPosition(file)!; + expect(refs.length).toBe(2); + assertTextSpans(refs, ['testEvent']); + assertFileNames(refs, ['test-component.ts', 'app.html']); + }); + + it('should find rename locations to selectorless component outputs from template', () => { + const template = project.openFile('app.html'); + template.contents = ''; + template.moveCursorToText('(tes¦tEvent)'); + const refs = getRenameLocationsAtPosition(template)!; + expect(refs.length).toBe(2); + assertTextSpans(refs, ['testEvent']); + assertFileNames(refs, ['test-component.ts', 'app.html']); + }); + + it('should handle rename request for selectorless component from source file', () => { + const file = project.openFile('test-component.ts'); + const template = project.openFile('app.html'); + template.contents = ''; + file.moveCursorToText('export class TestCom¦ponent {'); + const renameLocations = getRenameLocationsAtPosition(file)!; + + expect(renameLocations).toBeUndefined(); + + // TODO(crisbeto): investigate if we can make this work. + // It would involve looking for all the component references and renaming them. + // expect(renameLocations.length).toBe(3); + // assertTextSpans(renameLocations, ['TestComponent']); + // assertFileNames(renameLocations, ['test-component.ts', 'app.html', 'app.ts']); + }); + }); + }); + + describe('selectorless directives', () => { + let project: Project; + + beforeEach(() => { + initMockFileSystem('Native'); + env = LanguageServiceTestEnv.setup(); + project = env.addProject( + 'test', + { + 'app.ts': ` + import {Component} from '@angular/core'; + import {TestDirective} from './test-directive'; + + @Component({templateUrl: './app.html'}) + export class AppCmp { + numberValue!: number; + handleEvent() {} + } + `, + 'test-directive.ts': ` + import {Directive, EventEmitter, Input, Output} from '@angular/core'; + + @Directive() + export class TestDirective { + @Input() value!: number; + @Output() testEvent = new EventEmitter(); + } + `, + 'app.html': 'Will be overridden', + }, + {_enableSelectorless: true}, + ); + }); + + describe('references', () => { + it('should find references to selectorless directive from source file', () => { + const file = project.openFile('test-directive.ts'); + const template = project.openFile('app.html'); + template.contents = '
'; + file.moveCursorToText('export class TestDir¦ective {'); + const refs = getReferencesAtPosition(file)!; + expect(refs.length).toBe(3); + assertTextSpans(refs, ['TestDirective', '@TestDirective']); + assertFileNames(refs, ['test-directive.ts', 'app.html', 'app.ts']); + }); + + it('should find references to selectorless directive from template', () => { + const template = project.openFile('app.html'); + template.contents = '
'; + template.moveCursorToText('
'); + const refs = getReferencesAtPosition(template)!; + expect(refs.length).toBe(3); + assertTextSpans(refs, ['TestDirective', '@TestDirective']); + assertFileNames(refs, ['test-directive.ts', 'app.html', 'app.ts']); + }); + + it('should find references to selectorless directive inputs from source file', () => { + const file = project.openFile('test-directive.ts'); + const template = project.openFile('app.html'); + template.contents = '
'; + file.moveCursorToText('@Input() val¦ue!: number;'); + const refs = getReferencesAtPosition(file)!; + expect(refs.length).toBe(2); + assertTextSpans(refs, ['value']); + assertFileNames(refs, ['test-directive.ts', 'app.html']); + }); + + it('should find references to selectorless directive inputs from template', () => { + const template = project.openFile('app.html'); + template.contents = '
'; + template.moveCursorToText('[val¦ue]'); + const refs = getReferencesAtPosition(template)!; + expect(refs.length).toBe(2); + assertTextSpans(refs, ['value']); + assertFileNames(refs, ['test-directive.ts', 'app.html']); + }); + + it('should find references to selectorless directive outputs from source file', () => { + const file = project.openFile('test-directive.ts'); + const template = project.openFile('app.html'); + template.contents = '
'; + file.moveCursorToText('@Output() test¦Event = new EventEmitter();'); + const refs = getReferencesAtPosition(file)!; + expect(refs.length).toBe(2); + assertTextSpans(refs, ['testEvent']); + assertFileNames(refs, ['test-directive.ts', 'app.html']); + }); + + it('should find references to selectorless directive outputs from template', () => { + const template = project.openFile('app.html'); + template.contents = '
'; + template.moveCursorToText('(tes¦tEvent)'); + const refs = getReferencesAtPosition(template)!; + expect(refs.length).toBe(2); + assertTextSpans(refs, ['testEvent']); + assertFileNames(refs, ['test-directive.ts', 'app.html']); + }); + }); + + describe('rename locations', () => { + it('should find rename locations of selectorless directive from template', () => { + const template = project.openFile('app.html'); + template.contents = '
'; + template.moveCursorToText('@TestDir¦ective'); + const renameLocations = getRenameLocationsAtPosition(template)!; + + // There are 3 locations that need to be renamed: + // - Source file where the directive is defined. + // - Reference on the `div` node. + // - Import in the app component. + expect(renameLocations.length).toBe(3); + assertTextSpans(renameLocations, ['TestDirective']); + assertFileNames(renameLocations, ['test-directive.ts', 'app.html', 'app.ts']); + }); + + it('should find rename locations for selectorless directive with bindings', () => { + const template = project.openFile('app.html'); + template.contents = '
Hello
'; + template.moveCursorToText('@TestDir¦ective('); + const renameLocations = getRenameLocationsAtPosition(template)!; + + // There are 3 locations that need to be renamed: + // - Source file where the directive is defined. + // - Reference on the `div` node. + // - Import in the app component. + expect(renameLocations.length).toBe(3); + assertTextSpans(renameLocations, ['TestDirective']); + assertFileNames(renameLocations, ['test-directive.ts', 'app.html', 'app.ts']); + }); + + it('should find rename locations to selectorless directive inputs from source file', () => { + const file = project.openFile('test-directive.ts'); + const template = project.openFile('app.html'); + template.contents = '
'; + file.moveCursorToText('@Input() val¦ue!: number;'); + const refs = getRenameLocationsAtPosition(file)!; + expect(refs.length).toBe(2); + assertTextSpans(refs, ['value']); + assertFileNames(refs, ['test-directive.ts', 'app.html']); + }); + + it('should find rename locations to selectorless directive inputs from template', () => { + const template = project.openFile('app.html'); + template.contents = '
'; + template.moveCursorToText('[val¦ue]'); + const refs = getRenameLocationsAtPosition(template)!; + expect(refs.length).toBe(2); + assertTextSpans(refs, ['value']); + assertFileNames(refs, ['test-directive.ts', 'app.html']); + }); + + it('should find rename locations to selectorless directive outputs from source file', () => { + const file = project.openFile('test-directive.ts'); + const template = project.openFile('app.html'); + template.contents = '
'; + file.moveCursorToText('@Output() test¦Event = new EventEmitter();'); + const refs = getRenameLocationsAtPosition(file)!; + expect(refs.length).toBe(2); + assertTextSpans(refs, ['testEvent']); + assertFileNames(refs, ['test-directive.ts', 'app.html']); + }); + + it('should find rename locations to selectorless directive outputs from template', () => { + const template = project.openFile('app.html'); + template.contents = '
'; + template.moveCursorToText('(tes¦tEvent)'); + const refs = getRenameLocationsAtPosition(template)!; + expect(refs.length).toBe(2); + assertTextSpans(refs, ['testEvent']); + assertFileNames(refs, ['test-directive.ts', 'app.html']); + }); + + it('should handle rename request for selectorless component from source file', () => { + const file = project.openFile('test-directive.ts'); + const template = project.openFile('app.html'); + template.contents = '
'; + file.moveCursorToText('export class TestDir¦ective {'); + const renameLocations = getRenameLocationsAtPosition(file)!; + + expect(renameLocations).toBeUndefined(); + + // TODO(crisbeto): investigate if we can make this work. + // It would involve looking for all the component references and renaming them. + // expect(renameLocations.length).toBe(3); + // assertTextSpans(renameLocations, ['TestDirective']); + // assertFileNames(renameLocations, ['test-directive.ts', 'app.html', 'app.ts']); + }); + }); + }); + + describe('aliased selectorless', () => { + let project: Project; + + beforeEach(() => { + initMockFileSystem('Native'); + env = LanguageServiceTestEnv.setup(); + project = env.addProject( + 'test', + { + 'app.ts': ` + import {Component} from '@angular/core'; + import {TestComponent as AliasedComponent} from './test-component'; + import {TestDirective as AliasedDirective} from './test-directive'; + + @Component({templateUrl: './app.html'}) + export class AppCmp { + numberValue!: number; + handleEvent() {} + } + `, + 'test-component.ts': ` + import {Component, EventEmitter, Input, Output} from '@angular/core'; + + @Component({template: ''}) + export class TestComponent { + @Input() name!: string; + @Output() testEvent = new EventEmitter(); + } + `, + 'test-directive.ts': ` + import {Directive, EventEmitter, Input, Output} from '@angular/core'; + + @Directive() + export class TestDirective { + @Input() value!: number; + @Output() testEvent = new EventEmitter(); + } + `, + 'app.html': 'Will be overridden', + }, + {_enableSelectorless: true}, + ); + }); + + it('should not rename aliased selectorless component references', () => { + const template = project.openFile('app.html'); + template.contents = ''; + template.moveCursorToText(''); + expect(getRenameLocationsAtPosition(template)).toBeUndefined(); + }); + + it('should not rename aliased selectorless directive references', () => { + const template = project.openFile('app.html'); + template.contents = '
'; + template.moveCursorToText('@AliasedDir¦ective'); + expect(getRenameLocationsAtPosition(template)).toBeUndefined(); + }); + }); + function getReferencesAtPosition(file: OpenBuffer) { env.expectNoSourceDiagnostics(); const result = file.getReferencesAtPosition();