diff --git a/src/builtin-addons/core/template-completion-provider.ts b/src/builtin-addons/core/template-completion-provider.ts
index 0b117ca0..920a4b9c 100644
--- a/src/builtin-addons/core/template-completion-provider.ts
+++ b/src/builtin-addons/core/template-completion-provider.ts
@@ -25,6 +25,7 @@ import {
isAngleComponentPath,
isModifierPath,
isNamedBlockName,
+ isElementAttribute,
} from '../../utils/ast-helpers';
import {
listComponents,
@@ -47,6 +48,7 @@ import { ASTv1 } from '@glimmer/syntax';
import { URI } from 'vscode-uri';
import { componentsContextData } from './template-context-provider';
import { IRegistry } from '../../utils/registry-api';
+import { docForAttribute } from '../doc/autocomplete';
const mListModifiers = memoize(listModifiers, { length: 1, maxAge: 60000 }); // 1 second
const mListComponents = memoize(listComponents, { length: 1, maxAge: 60000 }); // 1 second
@@ -427,6 +429,16 @@ export default class TemplateCompletionProvider {
logDebugInfo(candidates, scopedValues);
completions.push(...uniqBy([...candidates, ...scopedValues], 'label'));
+ } else if (isElementAttribute(focusPath) && (focusPath.node as ASTv1.AttrNode).name.startsWith('.')) {
+ const attrName = '...attributes';
+
+ if (!(focusPath.parent as ASTv1.ElementNode).attributes.find((attr) => attr.name === attrName)) {
+ completions.push({
+ label: attrName,
+ documentation: docForAttribute(attrName),
+ kind: CompletionItemKind.Property,
+ });
+ }
} else if (isComponentArgumentName(focusPath)) {
//
diff --git a/src/builtin-addons/doc/autocomplete.ts b/src/builtin-addons/doc/autocomplete.ts
new file mode 100644
index 00000000..5a7473b7
--- /dev/null
+++ b/src/builtin-addons/doc/autocomplete.ts
@@ -0,0 +1,34 @@
+const attributes: {
+ [key: string]: {
+ documentation: string;
+ };
+} = {
+ '...attributes': {
+ documentation: `
+ In general, you should place ...attributes after any attributes you specify to give people using your component an opportunity to override your attribute.
+ If ...attributes appears after an attribute, it overrides that attribute.
+ If it appears before an attribute, it does not.
+ Place ...attributes before your attributes only if you want to disallow tags from overriding your attributes.
+ This is likely to be unusual.
+ In addition, the class attribute is special, and will be merged with any existing classes on the element rather than overwriting them.
+ This allows you to progressively add CSS classes to your components, and makes them more flexible overall.
+ `,
+ },
+};
+
+function normalizeNewlines(text: string) {
+ return text
+ .split('\n')
+ .map((e) => e.trim())
+ .join('\n');
+}
+
+export function docForAttribute(name: string) {
+ if (name in attributes) {
+ if (attributes[name].documentation) {
+ return normalizeNewlines(attributes[name].documentation);
+ }
+ }
+
+ return '';
+}
diff --git a/src/utils/ast-helpers.ts b/src/utils/ast-helpers.ts
index b0e14458..1de11128 100644
--- a/src/utils/ast-helpers.ts
+++ b/src/utils/ast-helpers.ts
@@ -457,7 +457,11 @@ export function isScopedPathExpression(path: any): boolean {
}
export function isComponentArgumentName(path: any): boolean {
- return hasNodeType(path.node, 'AttrNode') && path.node.name.startsWith('@');
+ return isElementAttribute(path) && path.node.name.startsWith('@');
+}
+
+export function isElementAttribute(path: any): boolean {
+ return hasNodeType(path.node, 'AttrNode');
}
export function isLinkComponentRouteTarget(path: any): boolean {
diff --git a/test/__snapshots__/integration-test.ts.snap b/test/__snapshots__/integration-test.ts.snap
index f265e17b..c5fcd633 100644
--- a/test/__snapshots__/integration-test.ts.snap
+++ b/test/__snapshots__/integration-test.ts.snap
@@ -352,6 +352,47 @@ Object {
}
`;
+exports[`integration async fs enabled: false Able to provide autocomplete information for element attributes support ...attributes autocomplete 1`] = `
+Object {
+ "addonsMeta": Array [],
+ "registry": Object {
+ "component": Object {
+ "foo": Array [
+ "app/components/foo.hbs",
+ ],
+ },
+ },
+ "response": Array [
+ Object {
+ "documentation": "
+In general, you should place ...attributes after any attributes you specify to give people using your component an opportunity to override your attribute.
+If ...attributes appears after an attribute, it overrides that attribute.
+If it appears before an attribute, it does not.
+Place ...attributes before your attributes only if you want to disallow tags from overriding your attributes.
+This is likely to be unusual.
+In addition, the class attribute is special, and will be merged with any existing classes on the element rather than overwriting them.
+This allows you to progressively add CSS classes to your components, and makes them more flexible overall.
+",
+ "kind": 10,
+ "label": "...attributes",
+ "textEdit": Object {
+ "newText": "...attributes",
+ "range": Object {
+ "end": Object {
+ "character": 8,
+ "line": 0,
+ },
+ "start": Object {
+ "character": 7,
+ "line": 0,
+ },
+ },
+ },
+ },
+ ],
+}
+`;
+
exports[`integration async fs enabled: false Able to provide autocomplete information for local context access support child project addon calling parent project addon 1`] = `
Object {
"addonsMeta": Array [
@@ -3428,6 +3469,47 @@ Object {
}
`;
+exports[`integration async fs enabled: true Able to provide autocomplete information for element attributes support ...attributes autocomplete 1`] = `
+Object {
+ "addonsMeta": Array [],
+ "registry": Object {
+ "component": Object {
+ "foo": Array [
+ "app/components/foo.hbs",
+ ],
+ },
+ },
+ "response": Array [
+ Object {
+ "documentation": "
+In general, you should place ...attributes after any attributes you specify to give people using your component an opportunity to override your attribute.
+If ...attributes appears after an attribute, it overrides that attribute.
+If it appears before an attribute, it does not.
+Place ...attributes before your attributes only if you want to disallow tags from overriding your attributes.
+This is likely to be unusual.
+In addition, the class attribute is special, and will be merged with any existing classes on the element rather than overwriting them.
+This allows you to progressively add CSS classes to your components, and makes them more flexible overall.
+",
+ "kind": 10,
+ "label": "...attributes",
+ "textEdit": Object {
+ "newText": "...attributes",
+ "range": Object {
+ "end": Object {
+ "character": 8,
+ "line": 0,
+ },
+ "start": Object {
+ "character": 7,
+ "line": 0,
+ },
+ },
+ },
+ },
+ ],
+}
+`;
+
exports[`integration async fs enabled: true Able to provide autocomplete information for local context access support child project addon calling parent project addon 1`] = `
Object {
"addonsMeta": Array [
diff --git a/test/integration-test.ts b/test/integration-test.ts
index eaeb39c9..a7e35a56 100644
--- a/test/integration-test.ts
+++ b/test/integration-test.ts
@@ -684,6 +684,43 @@ describe('integration', function () {
});
});
+ describe('Able to provide autocomplete information for element attributes', () => {
+ it('support ...attributes autocomplete', async () => {
+ const result = await getResult(
+ CompletionRequest.method,
+ connection,
+ {
+ app: {
+ components: {
+ 'foo.hbs': '',
+ },
+ },
+ },
+ 'app/components/foo.hbs',
+ { line: 0, character: 8 }
+ );
+
+ expect(result).toMatchSnapshot();
+ });
+ it('does not complete attributes twice', async () => {
+ const result = await getResult(
+ CompletionRequest.method,
+ connection,
+ {
+ app: {
+ components: {
+ 'foo.hbs': '',
+ },
+ },
+ },
+ 'app/components/foo.hbs',
+ { line: 0, character: 22 }
+ );
+
+ expect(result.response.length).toBe(0);
+ });
+ });
+
describe('Able to provide autocomplete information for local scoped params', () => {
it('support tag blocks', async () => {
const result = await getResult(