Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions packages/language-service/src/completions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,10 @@ function attributeValueCompletions(
if (!path.tail) {
return [];
}
const includeEvents = !!path.first(BoundEventAst);
const dinfo = diagnosticInfoFromTemplateInfo(info);
const visitor =
new ExpressionVisitor(info, position, () => getExpressionScope(dinfo, path, false), attr);
const visitor = new ExpressionVisitor(
info, position, () => getExpressionScope(dinfo, path, includeEvents), attr);
path.tail.visit(visitor, null);
const {results} = visitor;
if (results.length) {
Expand Down
23 changes: 17 additions & 6 deletions packages/language-service/src/expression_diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import {AST, AstPath, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, CompileDirectiveSummary, CompileTypeMetadata, DirectiveAst, ElementAst, EmbeddedTemplateAst, Node, ParseSourceSpan, RecursiveTemplateAstVisitor, ReferenceAst, TemplateAst, TemplateAstPath, VariableAst, findNode, identifierName, templateVisitAll, tokenReference} from '@angular/compiler';

import {AstType, DiagnosticKind, ExpressionDiagnosticsContext, TypeDiagnostic} from './expression_type';
import {findOutputBinding} from './locate_symbol';
import {BuiltinType, Definition, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './symbols';

export interface DiagnosticTemplateInfo {
Expand Down Expand Up @@ -172,22 +173,32 @@ function refinedVariableType(
return query.getBuiltinType(BuiltinType.Any);
}

function getEventDeclaration(info: DiagnosticTemplateInfo, includeEvent?: boolean) {
function getEventDeclaration(
info: DiagnosticTemplateInfo, path: TemplateAstPath, includeEvent?: boolean) {
let result: SymbolDeclaration[] = [];
if (includeEvent) {
// TODO: Determine the type of the event parameter based on the Observable<T> or EventEmitter<T>
// of the event.
result = [{name: '$event', kind: 'variable', type: info.query.getBuiltinType(BuiltinType.Any)}];
let eventType = info.query.getBuiltinType(BuiltinType.Any);
if (path.tail instanceof BoundEventAst) {
const member = findOutputBinding(info.query, path, path.tail);
if (member && member.type) {
if (member.type.name == 'Observable' || member.type.name == 'EventEmitter') {
const typeArguments = info.query.getTypeArguments(member.type);
if (typeArguments && typeArguments.length) {
eventType = typeArguments[0];
}
}
}
result = [{name: '$event', kind: 'variable', type: eventType}];
}
}
return result;
}

export function getExpressionScope(
info: DiagnosticTemplateInfo, path: TemplateAstPath, includeEvent: boolean): SymbolTable {
let result = info.members;
const references = getReferences(info);
const variables = getVarDeclarations(info, path);
const events = getEventDeclaration(info, includeEvent);
const events = getEventDeclaration(info, path, includeEvent);
if (references.length || variables.length || events.length) {
const referenceTable = info.query.createSymbolTable(references);
const variableTable = info.query.createSymbolTable(variables);
Expand Down
17 changes: 9 additions & 8 deletions packages/language-service/src/locate_symbol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {AST, Attribute, BoundDirectivePropertyAst, BoundEventAst, CompileTypeSum
import {AstResult} from './common';
import {getExpressionScope} from './expression_diagnostics';
import {getExpressionSymbol} from './expressions';
import {Definition, DirectiveKind, Span, Symbol} from './types';
import {Definition, DirectiveKind, Span, Symbol, SymbolQuery} from './types';
import {diagnosticInfoFromTemplateInfo, findTemplateAstAt, inSpan, offsetSpan, spanOf} from './utils';

export interface SymbolInfo {
Expand Down Expand Up @@ -81,7 +81,7 @@ export function locateSymbol(info: AstResult, position: number): SymbolInfo|unde
visitVariable(ast) {},
visitEvent(ast) {
if (!attributeValueSymbol(ast.handler, /* inEvent */ true)) {
symbol = findOutputBinding(info, path, ast);
symbol = findOutputBinding(info.template.query, path, ast);
symbol = symbol && new OverrideKindSymbol(symbol, DirectiveKind.EVENT);
span = spanOf(ast);
}
Expand Down Expand Up @@ -130,7 +130,7 @@ export function locateSymbol(info: AstResult, position: number): SymbolInfo|unde
},
visitDirectiveProperty(ast) {
if (!attributeValueSymbol(ast.value)) {
symbol = findInputBinding(info, path, ast);
symbol = findInputBinding(info.template.query, path, ast);
span = spanOf(ast);
}
}
Expand All @@ -149,14 +149,15 @@ function findAttribute(info: AstResult, position: number): Attribute|undefined {
}

function findInputBinding(
info: AstResult, path: TemplateAstPath, binding: BoundDirectivePropertyAst): Symbol|undefined {
query: SymbolQuery, path: TemplateAstPath, binding: BoundDirectivePropertyAst): Symbol|
undefined {
const element = path.first(ElementAst);
if (element) {
for (const directive of element.directives) {
const invertedInput = invertMap(directive.directive.inputs);
const fieldName = invertedInput[binding.templateName];
if (fieldName) {
const classSymbol = info.template.query.getTypeSymbol(directive.directive.type.reference);
const classSymbol = query.getTypeSymbol(directive.directive.type.reference);
if (classSymbol) {
return classSymbol.members().get(fieldName);
}
Expand All @@ -165,15 +166,15 @@ function findInputBinding(
}
}

function findOutputBinding(info: AstResult, path: TemplateAstPath, binding: BoundEventAst): Symbol|
undefined {
export function findOutputBinding(
query: SymbolQuery, path: TemplateAstPath, binding: BoundEventAst): Symbol|undefined {
const element = path.first(ElementAst);
if (element) {
for (const directive of element.directives) {
const invertedOutputs = invertMap(directive.directive.outputs);
const fieldName = invertedOutputs[binding.name];
if (fieldName) {
const classSymbol = info.template.query.getTypeSymbol(directive.directive.type.reference);
const classSymbol = query.getTypeSymbol(directive.directive.type.reference);
if (classSymbol) {
return classSymbol.members().get(fieldName);
}
Expand Down
5 changes: 5 additions & 0 deletions packages/language-service/src/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,11 @@ export interface SymbolQuery {
*/
getElementType(type: Symbol): Symbol|undefined;

/**
* Return type arguments of generic type.
*/
getTypeArguments(type: Symbol): Symbol[];

/**
* Return a type that is the non-nullable version of the given type. If `type` is already
* non-nullable, return `type`.
Expand Down
13 changes: 13 additions & 0 deletions packages/language-service/src/typescript_symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,19 @@ class TypeScriptSymbolQuery implements SymbolQuery {
}
}

getTypeArguments(type: Symbol): Symbol[] {
const result: Symbol[] = [];
if (type && type instanceof TypeWrapper && type.tsType.symbol) {
const typeArguments: ts.Type[]|undefined = (type.tsType as any).typeArguments;
if (typeArguments && typeArguments.length) {
for (const parameter of typeArguments) {
result.push(new TypeWrapper(parameter, type.context));
}
}
}
return result;
}

getNonNullableType(symbol: Symbol): Symbol {
if (symbol instanceof TypeWrapper && (typeof this.checker.getNonNullableType == 'function')) {
const tsType = symbol.tsType;
Expand Down
20 changes: 20 additions & 0 deletions packages/language-service/test/completions_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,26 @@ describe('completions', () => {
// });
});

describe('custom event binding', () => {
it('should be able to get completions for event name', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'custom-event-name');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.ATTRIBUTE, ['(deleteRequest)']);
});

it('should be able to get completions for $event variable', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'custom-event-binding');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.VARIABLE, ['$event']);
});

it('should be able to get completions for event members', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'custom-event-members');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.PROPERTY, ['name', 'age', 'street']);
});
});

describe('replacement span', () => {
it('should not generate replacement entries for zero-length replacements', () => {
const fileName = mockHost.addCode(`
Expand Down
1 change: 1 addition & 0 deletions packages/language-service/test/project/app/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import * as ParsingCases from './parsing-cases';
ParsingCases.TemplateReference,
ParsingCases.TestComponent,
ParsingCases.TwoWayBinding,
ParsingCases.CustomEventComponent
]
})
export class AppModule {
Expand Down
14 changes: 14 additions & 0 deletions packages/language-service/test/project/app/parsing-cases.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,17 @@ export class EmptyInterpolation {
title = 'Some title';
subTitle = 'Some sub title';
}

@Component({
selector: 'custom-event-comp',
template: `
<custom-event-comp ~{custom-event-name}></custom-event-comp>
<custom-event-comp
(deleteRequest)="log(~{custom-event-binding}$event.~{custom-event-members}name)">
</custom-event-comp>
`
})
export class CustomEventComponent {
@Output() deleteRequest = new EventEmitter<Person>();
log(text: string): void {}
}
2 changes: 1 addition & 1 deletion packages/language-service/test/typescript_host_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ describe('TypeScriptServiceHost', () => {
const tsLS = ts.createLanguageService(tsLSHost);
const ngLSHost = new TypeScriptServiceHost(tsLSHost, tsLS);
const templates = ngLSHost.getTemplates('/app/parsing-cases.ts');
expect(templates.length).toBe(18);
expect(templates.length).toBe(19);
});

it('should be able to find external template', () => {
Expand Down