mirror of
https://github.com/angular/angular
synced 2026-05-24 09:28:37 +00:00
refactor(language-service): initial reference and rename implementation for selectorless (#61240)
Adds an initial implementation for finding references and renaming to selectorless components/directives. Finding references should work everywhere, whereas renaming only currently works when initiated from the template. PR Close #61240
This commit is contained in:
parent
f074c30616
commit
fa27b76339
3 changed files with 666 additions and 24 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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<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 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('</')) {
|
||||
spans.push({
|
||||
fileName,
|
||||
textSpan: {
|
||||
// +2 because of the `</`.
|
||||
start: node.endSourceSpan.start.offset + 2,
|
||||
length,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return spans;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import {
|
|||
humanizeDocumentSpanLike,
|
||||
LanguageServiceTestEnv,
|
||||
OpenBuffer,
|
||||
Project,
|
||||
} from '../testing';
|
||||
|
||||
describe('find references and rename locations', () => {
|
||||
|
|
@ -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<string>();
|
||||
}
|
||||
`,
|
||||
'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 = '<TestComponent/>';
|
||||
file.moveCursorToText('export class TestComp¦onent {');
|
||||
const refs = getReferencesAtPosition(file)!;
|
||||
expect(refs.length).toBe(3);
|
||||
assertTextSpans(refs, ['TestComponent', '<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 = '<TestComponent/>';
|
||||
template.moveCursorToText('<TestCom¦ponent/>');
|
||||
const refs = getReferencesAtPosition(template)!;
|
||||
expect(refs.length).toBe(3);
|
||||
assertTextSpans(refs, ['TestComponent', '<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 = '<TestComponent [name]="stringValue"/>';
|
||||
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 = '<TestComponent [name]="stringValue"/>';
|
||||
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 = '<TestComponent (testEvent)="handleEvent()"/>';
|
||||
file.moveCursorToText('@Output() test¦Event = new EventEmitter<string>();');
|
||||
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 = '<TestComponent (testEvent)="handleEvent()"/>';
|
||||
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 = '<TestComponent/>';
|
||||
template.moveCursorToText('<TestCom¦ponent/>');
|
||||
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 = '<TestComponent:a hello="world">Hello</TestComponent:a>';
|
||||
template.moveCursorToText('<TestCom¦ponent:a');
|
||||
const renameLocations = getRenameLocationsAtPosition(template)!;
|
||||
|
||||
// There are 4 locations that need to be renamed:
|
||||
// - Source file where the component is defined.
|
||||
// - Opening tag in the template.
|
||||
// - Closing tag in the template.
|
||||
// - Import in the app component.
|
||||
expect(renameLocations.length).toBe(4);
|
||||
assertTextSpans(renameLocations, ['TestComponent']);
|
||||
assertFileNames(renameLocations, ['test-component.ts', 'app.html', 'app.html', 'app.ts']);
|
||||
});
|
||||
|
||||
it('should find rename locations to selectorless component inputs from source file', () => {
|
||||
const file = project.openFile('test-component.ts');
|
||||
const template = project.openFile('app.html');
|
||||
template.contents = '<TestComponent [name]="stringValue"/>';
|
||||
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 = '<TestComponent [name]="stringValue"/>';
|
||||
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 = '<TestComponent (testEvent)="handleEvent()"/>';
|
||||
file.moveCursorToText('@Output() test¦Event = new EventEmitter<string>();');
|
||||
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 = '<TestComponent (testEvent)="handleEvent()"/>';
|
||||
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 = '<TestComponent/>';
|
||||
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<number>();
|
||||
}
|
||||
`,
|
||||
'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 = '<div @TestDirective></div>';
|
||||
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 = '<div @TestDirective></div>';
|
||||
template.moveCursorToText('<div @TestDir¦ective></div>');
|
||||
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 = '<div @TestDirective([value]="numberValue")></div>';
|
||||
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 = '<div @TestDirective([value]="numberValue")></div>';
|
||||
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 = '<div @TestDirective((testEvent)="handleEvent()")></div>';
|
||||
file.moveCursorToText('@Output() test¦Event = new EventEmitter<number>();');
|
||||
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 = '<div @TestDirective((testEvent)="handleEvent()")></div>';
|
||||
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 = '<div @TestDirective></div>';
|
||||
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 = '<div @TestDirective([value]="123")>Hello</div>';
|
||||
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 = '<div @TestDirective([value]="numberValue")></div>';
|
||||
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 = '<div @TestDirective([value]="numberValue")></div>';
|
||||
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 = '<div @TestDirective((testEvent)="handleEvent()")></div>';
|
||||
file.moveCursorToText('@Output() test¦Event = new EventEmitter<number>();');
|
||||
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 = '<div @TestDirective((testEvent)="handleEvent()")></div>';
|
||||
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 = '<div @TestDirective></div>';
|
||||
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<string>();
|
||||
}
|
||||
`,
|
||||
'test-directive.ts': `
|
||||
import {Directive, EventEmitter, Input, Output} from '@angular/core';
|
||||
|
||||
@Directive()
|
||||
export class TestDirective {
|
||||
@Input() value!: number;
|
||||
@Output() testEvent = new EventEmitter<number>();
|
||||
}
|
||||
`,
|
||||
'app.html': 'Will be overridden',
|
||||
},
|
||||
{_enableSelectorless: true},
|
||||
);
|
||||
});
|
||||
|
||||
it('should not rename aliased selectorless component references', () => {
|
||||
const template = project.openFile('app.html');
|
||||
template.contents = '<AliasedComponent/>';
|
||||
template.moveCursorToText('<AliasedCom¦ponent/>');
|
||||
expect(getRenameLocationsAtPosition(template)).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should not rename aliased selectorless directive references', () => {
|
||||
const template = project.openFile('app.html');
|
||||
template.contents = '<div @AliasedDirective></div>';
|
||||
template.moveCursorToText('@AliasedDir¦ective');
|
||||
expect(getRenameLocationsAtPosition(template)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
function getReferencesAtPosition(file: OpenBuffer) {
|
||||
env.expectNoSourceDiagnostics();
|
||||
const result = file.getReferencesAtPosition();
|
||||
|
|
|
|||
Loading…
Reference in a new issue