2016-11-22 12:10:23 -05:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
|
|
*
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
*/
|
|
|
|
|
2019-10-24 20:30:23 -04:00
|
|
|
import {AST, AstPath, Attribute, BoundDirectivePropertyAst, BoundElementPropertyAst, BoundEventAst, BoundTextAst, CssSelector, Element, ElementAst, ImplicitReceiver, NAMED_ENTITIES, Node as HtmlAst, NullTemplateVisitor, ParseSpan, PropertyRead, TagContentType, Text, findNode, getHtmlTagDefinition} from '@angular/compiler';
|
2019-03-04 09:46:18 -05:00
|
|
|
import {getExpressionScope} from '@angular/compiler-cli/src/language_services';
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-10-24 14:48:09 -04:00
|
|
|
import {AstResult} from './common';
|
2017-05-09 19:16:50 -04:00
|
|
|
import {getExpressionCompletions} from './expressions';
|
2016-11-22 12:10:23 -05:00
|
|
|
import {attributeNames, elementNames, eventNames, propertyNames} from './html_info';
|
2019-10-24 17:48:43 -04:00
|
|
|
import {InlineTemplate} from './template';
|
2019-10-24 14:35:03 -04:00
|
|
|
import * as ng from './types';
|
2019-10-21 21:49:32 -04:00
|
|
|
import {diagnosticInfoFromTemplateInfo, findTemplateAstAt, getSelectors, hasTemplateReference, inSpan, spanOf} from './utils';
|
2016-11-22 12:10:23 -05:00
|
|
|
|
|
|
|
const TEMPLATE_ATTR_PREFIX = '*';
|
2019-10-24 17:48:43 -04:00
|
|
|
const HIDDEN_HTML_ELEMENTS: ReadonlySet<string> =
|
|
|
|
new Set(['html', 'script', 'noscript', 'base', 'body', 'title', 'head', 'link']);
|
|
|
|
const HTML_ELEMENTS: ReadonlyArray<ng.CompletionEntry> =
|
|
|
|
elementNames().filter(name => !HIDDEN_HTML_ELEMENTS.has(name)).map(name => {
|
|
|
|
return {
|
|
|
|
name,
|
|
|
|
kind: ng.CompletionKind.HTML_ELEMENT,
|
|
|
|
sortText: name,
|
|
|
|
};
|
|
|
|
});
|
|
|
|
const ANGULAR_ELEMENTS: ReadonlyArray<ng.CompletionEntry> = [
|
|
|
|
{
|
|
|
|
name: 'ng-container',
|
|
|
|
kind: ng.CompletionKind.ANGULAR_ELEMENT,
|
|
|
|
sortText: 'ng-container',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'ng-content',
|
|
|
|
kind: ng.CompletionKind.ANGULAR_ELEMENT,
|
|
|
|
sortText: 'ng-content',
|
|
|
|
},
|
|
|
|
{
|
|
|
|
name: 'ng-template',
|
|
|
|
kind: ng.CompletionKind.ANGULAR_ELEMENT,
|
|
|
|
sortText: 'ng-template',
|
|
|
|
},
|
|
|
|
];
|
2019-09-19 02:13:30 -04:00
|
|
|
|
2019-08-28 13:18:18 -04:00
|
|
|
export function getTemplateCompletions(
|
2019-10-24 14:35:03 -04:00
|
|
|
templateInfo: AstResult, position: number): ng.CompletionEntry[] {
|
|
|
|
let result: ng.CompletionEntry[] = [];
|
2019-09-13 10:25:28 -04:00
|
|
|
const {htmlAst, template} = templateInfo;
|
2016-11-22 12:10:23 -05:00
|
|
|
// The templateNode starts at the delimiter character so we add 1 to skip it.
|
2019-09-13 10:25:28 -04:00
|
|
|
const templatePosition = position - template.span.start;
|
|
|
|
const path = findNode(htmlAst, templatePosition);
|
|
|
|
const mostSpecific = path.tail;
|
2019-08-21 17:36:00 -04:00
|
|
|
if (path.empty || !mostSpecific) {
|
2019-10-24 17:48:43 -04:00
|
|
|
result = elementCompletions(templateInfo);
|
2019-08-21 17:36:00 -04:00
|
|
|
} else {
|
2019-09-13 10:25:28 -04:00
|
|
|
const astPosition = templatePosition - mostSpecific.sourceSpan.start.offset;
|
2019-08-21 17:36:00 -04:00
|
|
|
mostSpecific.visit(
|
|
|
|
{
|
|
|
|
visitElement(ast) {
|
2019-09-13 10:25:28 -04:00
|
|
|
const startTagSpan = spanOf(ast.sourceSpan);
|
|
|
|
const tagLen = ast.name.length;
|
2019-08-28 13:18:18 -04:00
|
|
|
// + 1 for the opening angle bracket
|
|
|
|
if (templatePosition <= startTagSpan.start + tagLen + 1) {
|
2019-08-21 17:36:00 -04:00
|
|
|
// If we are in the tag then return the element completions.
|
2019-10-24 17:48:43 -04:00
|
|
|
result = elementCompletions(templateInfo);
|
2019-08-21 17:36:00 -04:00
|
|
|
} else if (templatePosition < startTagSpan.end) {
|
|
|
|
// We are in the attribute section of the element (but not in an attribute).
|
|
|
|
// Return the attribute completions.
|
|
|
|
result = attributeCompletions(templateInfo, path);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
visitAttribute(ast) {
|
|
|
|
if (!ast.valueSpan || !inSpan(templatePosition, spanOf(ast.valueSpan))) {
|
|
|
|
// We are in the name of an attribute. Show attribute completions.
|
|
|
|
result = attributeCompletions(templateInfo, path);
|
|
|
|
} else if (ast.valueSpan && inSpan(templatePosition, spanOf(ast.valueSpan))) {
|
|
|
|
result = attributeValueCompletions(templateInfo, templatePosition, ast);
|
|
|
|
}
|
|
|
|
},
|
|
|
|
visitText(ast) {
|
|
|
|
// Check if we are in a entity.
|
|
|
|
result = entityCompletions(getSourceText(template, spanOf(ast)), astPosition);
|
2019-08-28 13:18:18 -04:00
|
|
|
if (result.length) return result;
|
2019-08-21 17:36:00 -04:00
|
|
|
result = interpolationCompletions(templateInfo, templatePosition);
|
2019-08-28 13:18:18 -04:00
|
|
|
if (result.length) return result;
|
2019-09-13 10:25:28 -04:00
|
|
|
const element = path.first(Element);
|
2019-08-21 17:36:00 -04:00
|
|
|
if (element) {
|
2019-09-13 10:25:28 -04:00
|
|
|
const definition = getHtmlTagDefinition(element.name);
|
2019-08-21 17:36:00 -04:00
|
|
|
if (definition.contentType === TagContentType.PARSABLE_DATA) {
|
2016-11-22 12:10:23 -05:00
|
|
|
result = voidElementAttributeCompletions(templateInfo, path);
|
2019-08-28 13:18:18 -04:00
|
|
|
if (!result.length) {
|
2019-08-21 17:36:00 -04:00
|
|
|
// If the element can hold content, show element completions.
|
2019-10-24 17:48:43 -04:00
|
|
|
result = elementCompletions(templateInfo);
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
}
|
2019-08-21 17:36:00 -04:00
|
|
|
} else {
|
|
|
|
// If no element container, implies parsable data so show elements.
|
|
|
|
result = voidElementAttributeCompletions(templateInfo, path);
|
2019-08-28 13:18:18 -04:00
|
|
|
if (!result.length) {
|
2019-10-24 17:48:43 -04:00
|
|
|
result = elementCompletions(templateInfo);
|
2019-08-21 17:36:00 -04:00
|
|
|
}
|
|
|
|
}
|
2016-11-22 12:10:23 -05:00
|
|
|
},
|
2019-08-21 17:36:00 -04:00
|
|
|
visitComment(ast) {},
|
|
|
|
visitExpansion(ast) {},
|
|
|
|
visitExpansionCase(ast) {}
|
|
|
|
},
|
|
|
|
null);
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2019-10-24 14:35:03 -04:00
|
|
|
function attributeCompletions(info: AstResult, path: AstPath<HtmlAst>): ng.CompletionEntry[] {
|
2019-09-13 10:25:28 -04:00
|
|
|
const item = path.tail instanceof Element ? path.tail : path.parentOf(path.tail);
|
2016-11-22 12:10:23 -05:00
|
|
|
if (item instanceof Element) {
|
2019-10-21 21:49:32 -04:00
|
|
|
return attributeCompletionsForElement(info, item.name);
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
2019-08-28 13:18:18 -04:00
|
|
|
return [];
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
function attributeCompletionsForElement(
|
2019-10-24 14:35:03 -04:00
|
|
|
info: AstResult, elementName: string): ng.CompletionEntry[] {
|
|
|
|
const results: ng.CompletionEntry[] = [];
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-10-24 17:48:43 -04:00
|
|
|
if (info.template instanceof InlineTemplate) {
|
|
|
|
// Provide HTML attributes completion only for inline templates
|
|
|
|
for (const name of attributeNames(elementName)) {
|
|
|
|
results.push({
|
|
|
|
name,
|
|
|
|
kind: ng.CompletionKind.HTML_ATTRIBUTE,
|
|
|
|
sortText: name,
|
|
|
|
});
|
|
|
|
}
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Add html properties
|
2019-10-21 21:49:32 -04:00
|
|
|
for (const name of propertyNames(elementName)) {
|
|
|
|
results.push({
|
|
|
|
name: `[${name}]`,
|
2019-10-24 14:35:03 -04:00
|
|
|
kind: ng.CompletionKind.ATTRIBUTE,
|
2019-10-21 21:49:32 -04:00
|
|
|
sortText: name,
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// Add html events
|
2019-10-21 21:49:32 -04:00
|
|
|
for (const name of eventNames(elementName)) {
|
|
|
|
results.push({
|
|
|
|
name: `(${name})`,
|
2019-10-24 14:35:03 -04:00
|
|
|
kind: ng.CompletionKind.ATTRIBUTE,
|
2019-10-21 21:49:32 -04:00
|
|
|
sortText: name,
|
2016-11-22 12:10:23 -05:00
|
|
|
});
|
2019-10-21 21:49:32 -04:00
|
|
|
}
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-10-21 21:49:32 -04:00
|
|
|
// Add Angular attributes
|
|
|
|
results.push(...angularAttributes(info, elementName));
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-10-21 21:49:32 -04:00
|
|
|
return results;
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
|
2019-08-28 13:18:18 -04:00
|
|
|
function attributeValueCompletions(
|
2019-10-24 14:35:03 -04:00
|
|
|
info: AstResult, position: number, attr: Attribute): ng.CompletionEntry[] {
|
2017-05-09 19:16:50 -04:00
|
|
|
const path = findTemplateAstAt(info.templateAst, position);
|
2019-08-28 13:18:18 -04:00
|
|
|
if (!path.tail) {
|
|
|
|
return [];
|
|
|
|
}
|
2017-05-09 19:16:50 -04:00
|
|
|
const dinfo = diagnosticInfoFromTemplateInfo(info);
|
2019-08-28 13:18:18 -04:00
|
|
|
const visitor =
|
2019-10-24 20:30:23 -04:00
|
|
|
new ExpressionVisitor(info, position, () => getExpressionScope(dinfo, path, false), attr);
|
2019-08-28 13:18:18 -04:00
|
|
|
path.tail.visit(visitor, null);
|
2019-10-24 20:30:23 -04:00
|
|
|
const {results} = visitor;
|
|
|
|
if (results.length) {
|
|
|
|
return results;
|
|
|
|
}
|
|
|
|
// Try allowing widening the path
|
|
|
|
const widerPath = findTemplateAstAt(info.templateAst, position, /* allowWidening */ true);
|
|
|
|
if (widerPath.tail) {
|
|
|
|
const widerVisitor = new ExpressionVisitor(
|
|
|
|
info, position, () => getExpressionScope(dinfo, widerPath, false), attr);
|
|
|
|
widerPath.tail.visit(widerVisitor, null);
|
|
|
|
return widerVisitor.results;
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
2019-10-24 20:30:23 -04:00
|
|
|
return results;
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
|
2019-10-24 17:48:43 -04:00
|
|
|
function elementCompletions(info: AstResult): ng.CompletionEntry[] {
|
|
|
|
const results: ng.CompletionEntry[] = [...ANGULAR_ELEMENTS];
|
|
|
|
|
|
|
|
if (info.template instanceof InlineTemplate) {
|
|
|
|
// Provide HTML elements completion only for inline templates
|
|
|
|
results.push(...HTML_ELEMENTS);
|
|
|
|
}
|
2016-11-22 12:10:23 -05:00
|
|
|
|
|
|
|
// Collect the elements referenced by the selectors
|
2019-10-24 17:48:43 -04:00
|
|
|
const components = new Set<string>();
|
|
|
|
for (const selector of getSelectors(info).selectors) {
|
|
|
|
const name = selector.element;
|
|
|
|
if (name && !components.has(name)) {
|
|
|
|
components.add(name);
|
|
|
|
results.push({
|
|
|
|
name,
|
|
|
|
kind: ng.CompletionKind.COMPONENT,
|
|
|
|
sortText: name,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-10-24 17:48:43 -04:00
|
|
|
return results;
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
|
2019-10-24 14:35:03 -04:00
|
|
|
function entityCompletions(value: string, position: number): ng.CompletionEntry[] {
|
2016-11-22 12:10:23 -05:00
|
|
|
// Look for entity completions
|
|
|
|
const re = /&[A-Za-z]*;?(?!\d)/g;
|
|
|
|
let found: RegExpExecArray|null;
|
2019-10-24 14:35:03 -04:00
|
|
|
let result: ng.CompletionEntry[] = [];
|
2016-11-22 12:10:23 -05:00
|
|
|
while (found = re.exec(value)) {
|
|
|
|
let len = found[0].length;
|
|
|
|
if (position >= found.index && position < (found.index + len)) {
|
2019-08-28 13:18:18 -04:00
|
|
|
result = Object.keys(NAMED_ENTITIES).map(name => {
|
|
|
|
return {
|
|
|
|
name: `&${name};`,
|
2019-10-24 14:35:03 -04:00
|
|
|
kind: ng.CompletionKind.ENTITY,
|
2019-08-28 13:18:18 -04:00
|
|
|
sortText: name,
|
|
|
|
};
|
|
|
|
});
|
2016-11-22 12:10:23 -05:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2019-10-24 14:35:03 -04:00
|
|
|
function interpolationCompletions(info: AstResult, position: number): ng.CompletionEntry[] {
|
2016-11-22 12:10:23 -05:00
|
|
|
// Look for an interpolation in at the position.
|
2017-05-09 19:16:50 -04:00
|
|
|
const templatePath = findTemplateAstAt(info.templateAst, position);
|
2019-08-28 13:18:18 -04:00
|
|
|
if (!templatePath.tail) {
|
|
|
|
return [];
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
2019-09-13 10:25:28 -04:00
|
|
|
const visitor = new ExpressionVisitor(
|
2019-10-24 20:30:23 -04:00
|
|
|
info, position,
|
2019-08-28 13:18:18 -04:00
|
|
|
() => getExpressionScope(diagnosticInfoFromTemplateInfo(info), templatePath, false));
|
|
|
|
templatePath.tail.visit(visitor, null);
|
2019-10-24 20:30:23 -04:00
|
|
|
return visitor.results;
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
// There is a special case of HTML where text that contains a unclosed tag is treated as
|
|
|
|
// text. For exaple '<h1> Some <a text </h1>' produces a text nodes inside of the H1
|
|
|
|
// element "Some <a text". We, however, want to treat this as if the user was requesting
|
|
|
|
// the attributes of an "a" element, not requesting completion in the a text element. This
|
|
|
|
// code checks for this case and returns element completions if it is detected or undefined
|
|
|
|
// if it is not.
|
2019-08-28 13:18:18 -04:00
|
|
|
function voidElementAttributeCompletions(
|
2019-10-24 14:35:03 -04:00
|
|
|
info: AstResult, path: AstPath<HtmlAst>): ng.CompletionEntry[] {
|
2019-09-13 10:25:28 -04:00
|
|
|
const tail = path.tail;
|
2016-11-22 12:10:23 -05:00
|
|
|
if (tail instanceof Text) {
|
2019-09-13 10:25:28 -04:00
|
|
|
const match = tail.value.match(/<(\w(\w|\d|-)*:)?(\w(\w|\d|-)*)\s/);
|
2016-11-22 12:10:23 -05:00
|
|
|
// The position must be after the match, otherwise we are still in a place where elements
|
|
|
|
// are expected (such as `<|a` or `<a|`; we only want attributes for `<a |` or after).
|
2017-04-28 18:10:30 -04:00
|
|
|
if (match &&
|
|
|
|
path.position >= (match.index || 0) + match[0].length + tail.sourceSpan.start.offset) {
|
2016-11-22 12:10:23 -05:00
|
|
|
return attributeCompletionsForElement(info, match[3]);
|
|
|
|
}
|
|
|
|
}
|
2019-08-28 13:18:18 -04:00
|
|
|
return [];
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
class ExpressionVisitor extends NullTemplateVisitor {
|
2019-10-24 20:30:23 -04:00
|
|
|
private readonly completions = new Map<string, ng.CompletionEntry>();
|
2016-11-22 12:10:23 -05:00
|
|
|
|
|
|
|
constructor(
|
2019-10-24 20:30:23 -04:00
|
|
|
private readonly info: AstResult, private readonly position: number,
|
|
|
|
private readonly getExpressionScope: () => ng.SymbolTable,
|
|
|
|
private readonly attr?: Attribute) {
|
2016-11-22 12:10:23 -05:00
|
|
|
super();
|
|
|
|
}
|
|
|
|
|
2019-10-24 20:30:23 -04:00
|
|
|
get results(): ng.CompletionEntry[] { return Array.from(this.completions.values()); }
|
|
|
|
|
2016-11-22 12:10:23 -05:00
|
|
|
visitDirectiveProperty(ast: BoundDirectivePropertyAst): void {
|
2019-10-24 20:30:23 -04:00
|
|
|
this.addAttributeValuesToCompletions(ast.value);
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
visitElementProperty(ast: BoundElementPropertyAst): void {
|
2019-10-24 20:30:23 -04:00
|
|
|
this.addAttributeValuesToCompletions(ast.value);
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
|
2019-10-24 20:30:23 -04:00
|
|
|
visitEvent(ast: BoundEventAst): void { this.addAttributeValuesToCompletions(ast.handler); }
|
2016-11-22 12:10:23 -05:00
|
|
|
|
|
|
|
visitElement(ast: ElementAst): void {
|
2019-10-24 20:30:23 -04:00
|
|
|
if (!this.attr || !this.attr.valueSpan || !this.attr.name.startsWith(TEMPLATE_ATTR_PREFIX)) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
// The value is a template expression but the expression AST was not produced when the
|
|
|
|
// TemplateAst was produce so do that now.
|
|
|
|
const key = this.attr.name.substr(TEMPLATE_ATTR_PREFIX.length);
|
|
|
|
// Find the selector
|
|
|
|
const selectorInfo = getSelectors(this.info);
|
|
|
|
const selectors = selectorInfo.selectors;
|
|
|
|
const selector =
|
|
|
|
selectors.filter(s => s.attrs.some((attr, i) => i % 2 === 0 && attr === key))[0];
|
|
|
|
if (!selector) {
|
|
|
|
return;
|
|
|
|
}
|
2016-11-22 12:10:23 -05:00
|
|
|
|
2019-10-24 20:30:23 -04:00
|
|
|
const templateBindingResult =
|
|
|
|
this.info.expressionParser.parseTemplateBindings(key, this.attr.value, null, 0);
|
|
|
|
|
|
|
|
// find the template binding that contains the position
|
|
|
|
const valueRelativePosition = this.position - this.attr.valueSpan.start.offset;
|
|
|
|
const bindings = templateBindingResult.templateBindings;
|
|
|
|
const binding =
|
|
|
|
bindings.find(
|
|
|
|
binding => inSpan(valueRelativePosition, binding.span, /* exclusive */ true)) ||
|
|
|
|
bindings.find(binding => inSpan(valueRelativePosition, binding.span));
|
|
|
|
|
|
|
|
if (binding) {
|
|
|
|
if (binding.keyIsVar) {
|
2016-11-22 12:10:23 -05:00
|
|
|
const equalLocation = this.attr.value.indexOf('=');
|
|
|
|
if (equalLocation >= 0 && valueRelativePosition >= equalLocation) {
|
|
|
|
// We are after the '=' in a let clause. The valid values here are the members of the
|
|
|
|
// template reference's type parameter.
|
|
|
|
const directiveMetadata = selectorInfo.map.get(selector);
|
2017-04-28 18:10:30 -04:00
|
|
|
if (directiveMetadata) {
|
|
|
|
const contextTable =
|
|
|
|
this.info.template.query.getTemplateContext(directiveMetadata.type.reference);
|
|
|
|
if (contextTable) {
|
2019-10-24 20:30:23 -04:00
|
|
|
this.addSymbolsToCompletions(contextTable.values());
|
|
|
|
return;
|
2017-04-28 18:10:30 -04:00
|
|
|
}
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
}
|
2019-10-24 20:30:23 -04:00
|
|
|
}
|
|
|
|
if ((binding.expression && inSpan(valueRelativePosition, binding.expression.ast.span)) ||
|
|
|
|
// If the position is in the expression or after the key or there is no key, return the
|
|
|
|
// expression completions
|
|
|
|
valueRelativePosition > binding.span.start + binding.key.length - key.length) {
|
|
|
|
const span = new ParseSpan(0, this.attr.value.length);
|
|
|
|
const offset = ast.sourceSpan.start.offset;
|
|
|
|
let expressionAst: AST;
|
|
|
|
if (binding.expression) {
|
|
|
|
expressionAst = binding.expression.ast;
|
2016-11-22 12:10:23 -05:00
|
|
|
} else {
|
2019-10-24 20:30:23 -04:00
|
|
|
const receiver = new ImplicitReceiver(span, span.toAbsolute(offset));
|
|
|
|
expressionAst = new PropertyRead(span, span.toAbsolute(offset), receiver, '');
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
2019-10-24 20:30:23 -04:00
|
|
|
this.addAttributeValuesToCompletions(expressionAst, this.position);
|
|
|
|
return;
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
}
|
2019-10-24 20:30:23 -04:00
|
|
|
|
|
|
|
this.addKeysToCompletions(selector, key);
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
visitBoundText(ast: BoundTextAst) {
|
2019-10-24 19:07:04 -04:00
|
|
|
if (inSpan(this.position, ast.value.sourceSpan)) {
|
2016-11-22 12:10:23 -05:00
|
|
|
const completions = getExpressionCompletions(
|
2019-10-24 19:07:04 -04:00
|
|
|
this.getExpressionScope(), ast.value, this.position, this.info.template.query);
|
2016-11-22 12:10:23 -05:00
|
|
|
if (completions) {
|
2019-10-24 20:30:23 -04:00
|
|
|
this.addSymbolsToCompletions(completions);
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-24 20:30:23 -04:00
|
|
|
private addAttributeValuesToCompletions(value: AST, position?: number) {
|
2016-11-22 12:10:23 -05:00
|
|
|
const symbols = getExpressionCompletions(
|
2019-10-11 16:36:42 -04:00
|
|
|
this.getExpressionScope(), value,
|
|
|
|
position === undefined ? this.attributeValuePosition : position, this.info.template.query);
|
2016-11-22 12:10:23 -05:00
|
|
|
if (symbols) {
|
2019-10-24 20:30:23 -04:00
|
|
|
this.addSymbolsToCompletions(symbols);
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-24 20:30:23 -04:00
|
|
|
private addKeysToCompletions(selector: CssSelector, key: string) {
|
|
|
|
if (key !== 'ngFor') {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
this.completions.set('let', {
|
|
|
|
name: 'let',
|
|
|
|
kind: ng.CompletionKind.KEY,
|
|
|
|
sortText: 'let',
|
2019-08-28 13:18:18 -04:00
|
|
|
});
|
2019-10-24 20:30:23 -04:00
|
|
|
if (selector.attrs.some(attr => attr === 'ngForOf')) {
|
|
|
|
this.completions.set('of', {
|
|
|
|
name: 'of',
|
|
|
|
kind: ng.CompletionKind.KEY,
|
|
|
|
sortText: 'of',
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private addSymbolsToCompletions(symbols: ng.Symbol[]) {
|
|
|
|
for (const s of symbols) {
|
|
|
|
if (s.name.startsWith('__') || !s.public || this.completions.has(s.name)) {
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
this.completions.set(s.name, {
|
|
|
|
name: s.name,
|
|
|
|
kind: s.kind as ng.CompletionKind,
|
|
|
|
sortText: s.name,
|
|
|
|
});
|
|
|
|
}
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
private get attributeValuePosition() {
|
2017-04-28 18:10:30 -04:00
|
|
|
if (this.attr && this.attr.valueSpan) {
|
2019-10-24 19:07:04 -04:00
|
|
|
return this.position;
|
2017-04-28 18:10:30 -04:00
|
|
|
}
|
|
|
|
return 0;
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-24 14:35:03 -04:00
|
|
|
function getSourceText(template: ng.TemplateSource, span: ng.Span): string {
|
2016-11-22 12:10:23 -05:00
|
|
|
return template.source.substring(span.start, span.end);
|
|
|
|
}
|
|
|
|
|
2019-10-24 14:35:03 -04:00
|
|
|
function angularAttributes(info: AstResult, elementName: string): ng.CompletionEntry[] {
|
2019-10-21 21:49:32 -04:00
|
|
|
const {selectors, map: selectorMap} = getSelectors(info);
|
|
|
|
const templateRefs = new Set<string>();
|
|
|
|
const inputs = new Set<string>();
|
|
|
|
const outputs = new Set<string>();
|
|
|
|
const others = new Set<string>();
|
|
|
|
for (const selector of selectors) {
|
|
|
|
if (selector.element && selector.element !== elementName) {
|
|
|
|
continue;
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
2019-10-21 21:49:32 -04:00
|
|
|
const summary = selectorMap.get(selector) !;
|
|
|
|
for (const attr of selector.attrs) {
|
|
|
|
if (attr) {
|
|
|
|
if (hasTemplateReference(summary.type)) {
|
|
|
|
templateRefs.add(attr);
|
|
|
|
} else {
|
|
|
|
others.add(attr);
|
|
|
|
}
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
}
|
2019-10-21 21:49:32 -04:00
|
|
|
for (const input of Object.values(summary.inputs)) {
|
|
|
|
inputs.add(input);
|
|
|
|
}
|
|
|
|
for (const output of Object.values(summary.outputs)) {
|
|
|
|
outputs.add(output);
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-10-24 14:35:03 -04:00
|
|
|
const results: ng.CompletionEntry[] = [];
|
2019-10-21 21:49:32 -04:00
|
|
|
for (const name of templateRefs) {
|
|
|
|
results.push({
|
|
|
|
name: `*${name}`,
|
2019-10-24 14:35:03 -04:00
|
|
|
kind: ng.CompletionKind.ATTRIBUTE,
|
2019-10-21 21:49:32 -04:00
|
|
|
sortText: name,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
for (const name of inputs) {
|
|
|
|
results.push({
|
|
|
|
name: `[${name}]`,
|
2019-10-24 14:35:03 -04:00
|
|
|
kind: ng.CompletionKind.ATTRIBUTE,
|
2019-10-21 21:49:32 -04:00
|
|
|
sortText: name,
|
|
|
|
});
|
|
|
|
// Add banana-in-a-box syntax
|
|
|
|
// https://angular.io/guide/template-syntax#two-way-binding-
|
|
|
|
if (outputs.has(`${name}Change`)) {
|
|
|
|
results.push({
|
|
|
|
name: `[(${name})]`,
|
2019-10-24 14:35:03 -04:00
|
|
|
kind: ng.CompletionKind.ATTRIBUTE,
|
2019-10-21 21:49:32 -04:00
|
|
|
sortText: name,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for (const name of outputs) {
|
|
|
|
results.push({
|
|
|
|
name: `(${name})`,
|
2019-10-24 14:35:03 -04:00
|
|
|
kind: ng.CompletionKind.ATTRIBUTE,
|
2019-10-21 21:49:32 -04:00
|
|
|
sortText: name,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
for (const name of others) {
|
|
|
|
results.push({
|
|
|
|
name,
|
2019-10-24 14:35:03 -04:00
|
|
|
kind: ng.CompletionKind.ATTRIBUTE,
|
2019-10-21 21:49:32 -04:00
|
|
|
sortText: name,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return results;
|
2016-11-22 12:10:23 -05:00
|
|
|
}
|