diff --git a/packages/language-service/src/definitions.ts b/packages/language-service/src/definitions.ts
index 6f0b649a369..ebc4442329e 100644
--- a/packages/language-service/src/definitions.ts
+++ b/packages/language-service/src/definitions.ts
@@ -22,6 +22,8 @@ import {
DirectiveSymbol,
DomBindingSymbol,
ElementSymbol,
+ SelectorlessComponentSymbol,
+ SelectorlessDirectiveSymbol,
Symbol,
SymbolKind,
TcbLocation,
@@ -118,6 +120,8 @@ export class DefinitionBuilder {
case SymbolKind.Element:
case SymbolKind.Template:
case SymbolKind.DomBinding:
+ case SymbolKind.SelectorlessComponent:
+ case SymbolKind.SelectorlessDirective:
// Though it is generally more appropriate for the above symbol definitions to be
// associated with "type definitions" since the location in the template is the
// actual definition location, the better user experience would be to allow
@@ -238,6 +242,8 @@ export class DefinitionBuilder {
case SymbolKind.DomBinding:
case SymbolKind.Element:
case SymbolKind.Template:
+ case SymbolKind.SelectorlessComponent:
+ case SymbolKind.SelectorlessDirective:
definitions.push(...this.getTypeDefinitionsForTemplateInstance(symbol, node));
break;
case SymbolKind.Output:
@@ -286,7 +292,13 @@ export class DefinitionBuilder {
}
private getTypeDefinitionsForTemplateInstance(
- symbol: TemplateSymbol | ElementSymbol | DomBindingSymbol | DirectiveSymbol,
+ symbol:
+ | TemplateSymbol
+ | ElementSymbol
+ | DomBindingSymbol
+ | DirectiveSymbol
+ | SelectorlessComponentSymbol
+ | SelectorlessDirectiveSymbol,
node: AST | TmplAstNode,
): ts.DefinitionInfo[] {
switch (symbol.kind) {
@@ -313,6 +325,8 @@ export class DefinitionBuilder {
);
return this.getTypeDefinitionsForSymbols(...dirs);
}
+ case SymbolKind.SelectorlessComponent:
+ case SymbolKind.SelectorlessDirective:
case SymbolKind.Directive:
return this.getTypeDefinitionsForSymbols(symbol);
}
diff --git a/packages/language-service/test/definitions_spec.ts b/packages/language-service/test/definitions_spec.ts
index e1c711334d4..130c9182add 100644
--- a/packages/language-service/test/definitions_spec.ts
+++ b/packages/language-service/test/definitions_spec.ts
@@ -13,6 +13,7 @@ import {
assertFileNames,
assertTextSpans,
createModuleAndProjectWithDeclarations,
+ createProjectWithStandaloneDeclarations,
humanizeDocumentSpanLike,
LanguageServiceTestEnv,
OpenBuffer,
@@ -472,6 +473,252 @@ describe('definitions', () => {
assertFileNames(Array.from(definitions), ['app.html']);
});
+ it('gets definition for selectorless component', () => {
+ initMockFileSystem('Native');
+ const files = {
+ 'app.html': '',
+ 'dep.ts': `
+ import {Component} from '@angular/core';
+
+ @Component({template: ''})
+ export class Dep {}
+ `,
+ 'app.ts': `
+ import {Component} from '@angular/core';
+ import {Dep} from './dep';
+
+ @Component({
+ templateUrl: '/app.html',
+ })
+ export class AppCmp {}
+ `,
+ };
+ const env = LanguageServiceTestEnv.setup();
+
+ const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
+ _enableSelectorless: true,
+ });
+ const template = project.openFile('app.html');
+ template.contents = '';
+ project.expectNoSourceDiagnostics();
+
+ template.moveCursorToText('');
+ const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
+ expect(definitions[0].name).toEqual('Dep');
+ expect(definitions[0].kind).toBe(ts.ScriptElementKind.classElement);
+ expect(definitions[0].textSpan).toBe('Dep');
+ assertFileNames(Array.from(definitions), ['dep.ts']);
+ });
+
+ it('gets definition for selectorless directive', () => {
+ initMockFileSystem('Native');
+ const files = {
+ 'app.html': '',
+ 'dep.ts': `
+ import {Directive} from '@angular/core';
+
+ @Directive()
+ export class Dep {}
+ `,
+ 'app.ts': `
+ import {Component} from '@angular/core';
+ import {Dep} from './dep';
+
+ @Component({
+ templateUrl: '/app.html',
+ })
+ export class AppCmp {}
+ `,
+ };
+ const env = LanguageServiceTestEnv.setup();
+
+ const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
+ _enableSelectorless: true,
+ });
+ const template = project.openFile('app.html');
+ template.contents = '
';
+ project.expectNoSourceDiagnostics();
+
+ template.moveCursorToText('@De¦p');
+ const {definitions} = getDefinitionsAndAssertBoundSpan(env, template);
+ expect(definitions[0].name).toEqual('Dep');
+ expect(definitions[0].kind).toBe(ts.ScriptElementKind.classElement);
+ expect(definitions[0].textSpan).toBe('Dep');
+ assertFileNames(Array.from(definitions), ['dep.ts']);
+ });
+
+ it('gets definition of selectorless component input', () => {
+ initMockFileSystem('Native');
+ const files = {
+ 'app.html': '',
+ 'dep.ts': `
+ import {Component, Input} from '@angular/core';
+
+ @Component({template: ''})
+ export class Dep {
+ @Input() someInput: any;
+ }
+ `,
+ 'app.ts': `
+ import {Component} from '@angular/core';
+ import {Dep} from './dep';
+
+ @Component({
+ templateUrl: '/app.html',
+ })
+ export class AppCmp {}
+ `,
+ };
+ const env = LanguageServiceTestEnv.setup();
+
+ const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
+ _enableSelectorless: true,
+ });
+ const template = project.openFile('app.html');
+ template.contents = '';
+ project.expectNoSourceDiagnostics();
+
+ template.moveCursorToText('[some¦Input]');
+ const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
+
+ expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
+ 'someInput',
+ );
+ expect(definitions.length).toBe(1);
+ expect(definitions[0].textSpan).toContain('someInput');
+ assertFileNames(definitions, ['dep.ts']);
+ });
+
+ it('gets definition of selectorless directive input', () => {
+ initMockFileSystem('Native');
+ const files = {
+ 'app.html': '',
+ 'dep.ts': `
+ import {Directive, Input} from '@angular/core';
+
+ @Directive()
+ export class Dep {
+ @Input() someInput: any;
+ }
+ `,
+ 'app.ts': `
+ import {Component} from '@angular/core';
+ import {Dep} from './dep';
+
+ @Component({
+ templateUrl: '/app.html',
+ })
+ export class AppCmp {}
+ `,
+ };
+ const env = LanguageServiceTestEnv.setup();
+
+ const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
+ _enableSelectorless: true,
+ });
+ const template = project.openFile('app.html');
+ template.contents = '';
+ project.expectNoSourceDiagnostics();
+
+ template.moveCursorToText('[some¦Input]');
+ const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
+
+ expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
+ 'someInput',
+ );
+ expect(definitions.length).toBe(1);
+ expect(definitions[0].textSpan).toContain('someInput');
+ assertFileNames(definitions, ['dep.ts']);
+ });
+
+ it('gets definition of selectorless component output', () => {
+ initMockFileSystem('Native');
+ const files = {
+ 'app.html': '',
+ 'dep.ts': `
+ import {Component, Output, EventEmitter} from '@angular/core';
+
+ @Component({template: ''})
+ export class Dep {
+ @Output() someEvent = new EventEmitter();
+ }
+ `,
+ 'app.ts': `
+ import {Component} from '@angular/core';
+ import {Dep} from './dep';
+
+ @Component({
+ templateUrl: '/app.html',
+ })
+ export class AppCmp {
+ handler() {}
+ }
+ `,
+ };
+ const env = LanguageServiceTestEnv.setup();
+
+ const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
+ _enableSelectorless: true,
+ });
+ const template = project.openFile('app.html');
+ template.contents = '';
+ project.expectNoSourceDiagnostics();
+
+ template.moveCursorToText('(some¦Event)');
+ const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
+
+ expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
+ 'someEvent',
+ );
+ expect(definitions.length).toBe(1);
+ expect(definitions[0].textSpan).toContain('someEvent');
+ assertFileNames(definitions, ['dep.ts']);
+ });
+
+ it('gets definition of selectorless directive output', () => {
+ initMockFileSystem('Native');
+ const files = {
+ 'app.html': '',
+ 'dep.ts': `
+ import {Directive, Output, EventEmitter} from '@angular/core';
+
+ @Directive()
+ export class Dep {
+ @Output() someEvent = new EventEmitter();
+ }
+ `,
+ 'app.ts': `
+ import {Component} from '@angular/core';
+ import {Dep} from './dep';
+
+ @Component({
+ templateUrl: '/app.html',
+ })
+ export class AppCmp {
+ handler() {}
+ }
+ `,
+ };
+ const env = LanguageServiceTestEnv.setup();
+
+ const project = createProjectWithStandaloneDeclarations(env, 'test', files, {
+ _enableSelectorless: true,
+ });
+ const template = project.openFile('app.html');
+ template.contents = '';
+ project.expectNoSourceDiagnostics();
+
+ template.moveCursorToText('(some¦Event)');
+ const {textSpan, definitions} = getDefinitionsAndAssertBoundSpan(env, template);
+
+ expect(template.contents.slice(textSpan.start, textSpan.start + textSpan.length)).toEqual(
+ 'someEvent',
+ );
+ expect(definitions.length).toBe(1);
+ expect(definitions[0].textSpan).toContain('someEvent');
+ assertFileNames(definitions, ['dep.ts']);
+ });
+
it('gets definition for a method in a void expression', () => {
initMockFileSystem('Native');
const files = {
diff --git a/packages/language-service/testing/src/project.ts b/packages/language-service/testing/src/project.ts
index ad9ca9daf95..614d75a2d8a 100644
--- a/packages/language-service/testing/src/project.ts
+++ b/packages/language-service/testing/src/project.ts
@@ -65,7 +65,10 @@ function writeTsconfig(
export type TestableOptions = TypeCheckingOptions &
InternalOptions &
- Pick;
+ Pick & {
+ // This already exists in `InternalOptions`, but it's `internal` so it's stripped away.
+ _enableSelectorless?: boolean;
+ };
export class Project {
private tsProject: ts.server.Project;
diff --git a/packages/language-service/testing/src/util.ts b/packages/language-service/testing/src/util.ts
index e32ae3d3977..ec05789e1af 100644
--- a/packages/language-service/testing/src/util.ts
+++ b/packages/language-service/testing/src/util.ts
@@ -111,6 +111,26 @@ export function createModuleAndProjectWithDeclarations(
return env.addProject(projectName, {...projectFiles, ...standaloneFiles}, angularCompilerOptions);
}
+export function createProjectWithStandaloneDeclarations(
+ env: LanguageServiceTestEnv,
+ projectName: string,
+ projectFiles: ProjectFiles,
+ angularCompilerOptions: TestableOptions = {},
+ standaloneFiles: ProjectFiles = {},
+): Project {
+ const externalClasses: string[] = [];
+ const externalImports: string[] = [];
+ for (const [fileName, fileContents] of Object.entries(projectFiles)) {
+ if (!fileName.endsWith('.ts')) {
+ continue;
+ }
+ const className = getFirstClassDeclaration(fileContents);
+ externalClasses.push(className);
+ externalImports.push(`import {${className}} from './${fileName.replace('.ts', '')}';`);
+ }
+ return env.addProject(projectName, {...projectFiles, ...standaloneFiles}, angularCompilerOptions);
+}
+
export function humanizeDocumentSpanLike(
item: T,
env: LanguageServiceTestEnv,