Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(language-service): Do not show HTML elements and attrs for ext template #33388

Closed
wants to merge 1 commit into from
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
116 changes: 61 additions & 55 deletions packages/language-service/src/completions.ts
Expand Up @@ -12,23 +12,38 @@ import {getExpressionScope} from '@angular/compiler-cli/src/language_services';
import {AstResult} from './common';
import {getExpressionCompletions} from './expressions';
import {attributeNames, elementNames, eventNames, propertyNames} from './html_info';
import {InlineTemplate} from './template';
import * as ng from './types';
import {diagnosticInfoFromTemplateInfo, findTemplateAstAt, getSelectors, hasTemplateReference, inSpan, spanOf} from './utils';

const TEMPLATE_ATTR_PREFIX = '*';

const hiddenHtmlElements = {
html: true,
script: true,
noscript: true,
base: true,
body: true,
title: true,
head: true,
link: true,
};

const ANGULAR_ELEMENTS: ReadonlyArray<string> = ['ng-container', 'ng-content', 'ng-template'];
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',
},
];

export function getTemplateCompletions(
templateInfo: AstResult, position: number): ng.CompletionEntry[] {
Expand All @@ -39,7 +54,7 @@ export function getTemplateCompletions(
const path = findNode(htmlAst, templatePosition);
const mostSpecific = path.tail;
if (path.empty || !mostSpecific) {
result = elementCompletions(templateInfo, path);
result = elementCompletions(templateInfo);
} else {
const astPosition = templatePosition - mostSpecific.sourceSpan.start.offset;
mostSpecific.visit(
Expand All @@ -50,7 +65,7 @@ export function getTemplateCompletions(
// + 1 for the opening angle bracket
if (templatePosition <= startTagSpan.start + tagLen + 1) {
// If we are in the tag then return the element completions.
result = elementCompletions(templateInfo, path);
result = elementCompletions(templateInfo);
} else if (templatePosition < startTagSpan.end) {
// We are in the attribute section of the element (but not in an attribute).
// Return the attribute completions.
Expand Down Expand Up @@ -78,14 +93,14 @@ export function getTemplateCompletions(
result = voidElementAttributeCompletions(templateInfo, path);
if (!result.length) {
// If the element can hold content, show element completions.
result = elementCompletions(templateInfo, path);
result = elementCompletions(templateInfo);
}
}
} else {
// If no element container, implies parsable data so show elements.
result = voidElementAttributeCompletions(templateInfo, path);
if (!result.length) {
result = elementCompletions(templateInfo, path);
result = elementCompletions(templateInfo);
}
}
},
Expand All @@ -110,13 +125,15 @@ function attributeCompletionsForElement(
info: AstResult, elementName: string): ng.CompletionEntry[] {
const results: ng.CompletionEntry[] = [];

// Add html attributes
for (const name of attributeNames(elementName)) {
results.push({
name,
kind: ng.CompletionKind.HTML_ATTRIBUTE,
sortText: name,
});
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,
});
}
}

// Add html properties
Expand Down Expand Up @@ -166,38 +183,29 @@ function attributeValueCompletions(
return visitor.result || [];
}

function elementCompletions(info: AstResult, path: AstPath<HtmlAst>): ng.CompletionEntry[] {
const htmlNames = elementNames().filter(name => !(name in hiddenHtmlElements));
function elementCompletions(info: AstResult): ng.CompletionEntry[] {
const results: ng.CompletionEntry[] = [...ANGULAR_ELEMENTS];

// Collect the elements referenced by the selectors
const directiveElements = getSelectors(info)
.selectors.map(selector => selector.element)
.filter(name => !!name) as string[];
if (info.template instanceof InlineTemplate) {
// Provide HTML elements completion only for inline templates
results.push(...HTML_ELEMENTS);
}

const components = directiveElements.map(name => {
return {
name,
kind: ng.CompletionKind.COMPONENT,
sortText: name,
};
});
const htmlElements = htmlNames.map(name => {
return {
name,
kind: ng.CompletionKind.ELEMENT,
sortText: name,
};
});
const angularElements = ANGULAR_ELEMENTS.map(name => {
return {
name,
kind: ng.CompletionKind.ANGULAR_ELEMENT,
sortText: name,
};
});
// Collect the elements referenced by the selectors
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,
});
}
}

// Return components and html elements
return uniqueByName([...htmlElements, ...components, ...angularElements]);
return results;
}

/**
Expand Down Expand Up @@ -420,8 +428,6 @@ function getSourceText(template: ng.TemplateSource, span: ng.Span): string {
return template.source.substring(span.start, span.end);
}

const templateAttr = /^(\w+:)?(template$|^\*)/;

function lowerName(name: string): string {
return name && (name[0].toLowerCase() + name.substr(1));
}
Expand Down
1 change: 1 addition & 0 deletions packages/language-service/src/types.ts
Expand Up @@ -268,6 +268,7 @@ export enum CompletionKind {
ELEMENT = 'element',
ENTITY = 'entity',
HTML_ATTRIBUTE = 'html attribute',
HTML_ELEMENT = 'html element',
KEY = 'key',
METHOD = 'method',
PIPE = 'pipe',
Expand Down
33 changes: 19 additions & 14 deletions packages/language-service/test/completions_spec.ts
Expand Up @@ -36,7 +36,7 @@ describe('completions', () => {
for (const location of locations) {
const marker = mockHost.getLocationMarkerFor(APP_COMPONENT, location);
const completions = ngLS.getCompletionsAt(APP_COMPONENT, marker.start);
expectContain(completions, CompletionKind.ELEMENT, ['div', 'h1', 'h2', 'span']);
expectContain(completions, CompletionKind.HTML_ELEMENT, ['div', 'h1', 'h2', 'span']);
}
});

Expand Down Expand Up @@ -124,11 +124,11 @@ describe('completions', () => {
it('should be able to return attributes of an incomplete element', () => {
const m1 = mockHost.getLocationMarkerFor(PARSING_CASES, 'incomplete-open-lt');
const c1 = ngLS.getCompletionsAt(PARSING_CASES, m1.start);
expectContain(c1, CompletionKind.ELEMENT, ['a', 'div', 'p', 'span']);
expectContain(c1, CompletionKind.HTML_ELEMENT, ['a', 'div', 'p', 'span']);

const m2 = mockHost.getLocationMarkerFor(PARSING_CASES, 'incomplete-open-a');
const c2 = ngLS.getCompletionsAt(PARSING_CASES, m2.start);
expectContain(c2, CompletionKind.ELEMENT, ['a', 'div', 'p', 'span']);
expectContain(c2, CompletionKind.HTML_ELEMENT, ['a', 'div', 'p', 'span']);

const m3 = mockHost.getLocationMarkerFor(PARSING_CASES, 'incomplete-open-attr');
const c3 = ngLS.getCompletionsAt(PARSING_CASES, m3.start);
Expand All @@ -138,7 +138,7 @@ describe('completions', () => {
it('should be able to return completions with a missing closing tag', () => {
const marker = mockHost.getLocationMarkerFor(PARSING_CASES, 'missing-closing');
const completions = ngLS.getCompletionsAt(PARSING_CASES, marker.start);
expectContain(completions, CompletionKind.ELEMENT, ['a', 'div', 'p', 'span', 'h1', 'h2']);
expectContain(completions, CompletionKind.HTML_ELEMENT, ['a', 'div', 'p', 'span', 'h1', 'h2']);
});

it('should be able to return common attributes of an unknown tag', () => {
Expand Down Expand Up @@ -166,16 +166,21 @@ describe('completions', () => {
expectContain(completions, CompletionKind.ENTITY, ['&amp;', '&gt;', '&lt;', '&iota;']);
});

it('should be able to return html elements', () => {
it('should not return html elements', () => {
const locations = ['empty', 'start-tag-h1', 'h1-content', 'start-tag', 'start-tag-after-h'];
for (const location of locations) {
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, location);
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.ELEMENT, ['div', 'h1', 'h2', 'span']);
expect(completions).toBeDefined();
const {entries} = completions !;
expect(entries).not.toContain(jasmine.objectContaining({name: 'div'}));
expect(entries).not.toContain(jasmine.objectContaining({name: 'h1'}));
expect(entries).not.toContain(jasmine.objectContaining({name: 'h2'}));
expect(entries).not.toContain(jasmine.objectContaining({name: 'span'}));
}
});

it('should be able to return element diretives', () => {
it('should be able to return element directives', () => {
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'empty');
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.COMPONENT, [
Expand All @@ -186,15 +191,15 @@ describe('completions', () => {
]);
});

it('should be able to return h1 attributes', () => {
it('should not return html attributes', () => {
const marker = mockHost.getLocationMarkerFor(TEST_TEMPLATE, 'h1-after-space');
const completions = ngLS.getCompletionsAt(TEST_TEMPLATE, marker.start);
expectContain(completions, CompletionKind.HTML_ATTRIBUTE, [
'class',
'id',
'onclick',
'onmouseup',
]);
expect(completions).toBeDefined();
const {entries} = completions !;
expect(entries).not.toContain(jasmine.objectContaining({name: 'class'}));
expect(entries).not.toContain(jasmine.objectContaining({name: 'id'}));
expect(entries).not.toContain(jasmine.objectContaining({name: 'onclick'}));
expect(entries).not.toContain(jasmine.objectContaining({name: 'onmouseup'}));
});

it('should be able to find common angular attributes', () => {
Expand Down