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(