From bf62e22ce823e3b6e71e908250e3ede2cf602181 Mon Sep 17 00:00:00 2001 From: Aleksandr Kanunnikov Date: Sat, 26 Feb 2022 18:23:19 +0300 Subject: [PATCH 1/4] feat: ...attributes autocomplete --- .../core/template-completion-provider.ts | 20 +++++++++++++++++++ src/utils/ast-helpers.ts | 4 ++++ test/integration-test.ts | 20 +++++++++++++++++++ 3 files changed, 44 insertions(+) diff --git a/src/builtin-addons/core/template-completion-provider.ts b/src/builtin-addons/core/template-completion-provider.ts index 0b117ca0..aa33d065 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, @@ -427,6 +428,25 @@ export default class TemplateCompletionProvider { logDebugInfo(candidates, scopedValues); completions.push(...uniqBy([...candidates, ...scopedValues], 'label')); + } else if (isElementAttribute(focusPath)) { + if ((focusPath.node as ASTv1.AttrNode).name.startsWith('.')) { + completions.push({ + label: '...attributes', + detail: ` + 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. + ` + .split('\n') + .map((e) => e.trim()) + .join('\n'), + kind: CompletionItemKind.Property, + }); + } } else if (isComponentArgumentName(focusPath)) { // diff --git a/src/utils/ast-helpers.ts b/src/utils/ast-helpers.ts index b0e14458..fe31a112 100644 --- a/src/utils/ast-helpers.ts +++ b/src/utils/ast-helpers.ts @@ -460,6 +460,10 @@ export function isComponentArgumentName(path: any): boolean { return hasNodeType(path.node, 'AttrNode') && path.node.name.startsWith('@'); } +export function isElementAttribute(path: any): boolean { + return hasNodeType(path.node, 'AttrNode'); +} + export function isLinkComponentRouteTarget(path: any): boolean { return hasNodeType(path.node, 'TextNode') && hasNodeType(path.parent, 'AttrNode') && path.parent.name === '@route'; } diff --git a/test/integration-test.ts b/test/integration-test.ts index eaeb39c9..ce576187 100644 --- a/test/integration-test.ts +++ b/test/integration-test.ts @@ -684,6 +684,26 @@ describe('integration', function () { }); }); + describe('Ablet 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: 1, character: 8 } + ); + + expect(result).toMatchSnapshot(); + }); + }); + describe('Able to provide autocomplete information for local scoped params', () => { it('support tag blocks', async () => { const result = await getResult( From ea9aa836332c5b50c998cdac97971317526747c4 Mon Sep 17 00:00:00 2001 From: Aleksandr Kanunnikov Date: Sat, 26 Feb 2022 18:46:05 +0300 Subject: [PATCH 2/4] feat: ...attributes autocomplete --- .../core/template-completion-provider.ts | 36 +++++++++---------- src/utils/ast-helpers.ts | 2 +- test/integration-test.ts | 2 +- 3 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/builtin-addons/core/template-completion-provider.ts b/src/builtin-addons/core/template-completion-provider.ts index aa33d065..1c71c389 100644 --- a/src/builtin-addons/core/template-completion-provider.ts +++ b/src/builtin-addons/core/template-completion-provider.ts @@ -428,25 +428,23 @@ export default class TemplateCompletionProvider { logDebugInfo(candidates, scopedValues); completions.push(...uniqBy([...candidates, ...scopedValues], 'label')); - } else if (isElementAttribute(focusPath)) { - if ((focusPath.node as ASTv1.AttrNode).name.startsWith('.')) { - completions.push({ - label: '...attributes', - detail: ` - 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. - ` - .split('\n') - .map((e) => e.trim()) - .join('\n'), - kind: CompletionItemKind.Property, - }); - } + } else if (isElementAttribute(focusPath) && (focusPath.node as ASTv1.AttrNode).name.startsWith('.')) { + completions.push({ + label: '...attributes', + detail: ` + 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. + ` + .split('\n') + .map((e) => e.trim()) + .join('\n'), + kind: CompletionItemKind.Property, + }); } else if (isComponentArgumentName(focusPath)) { // diff --git a/src/utils/ast-helpers.ts b/src/utils/ast-helpers.ts index fe31a112..1de11128 100644 --- a/src/utils/ast-helpers.ts +++ b/src/utils/ast-helpers.ts @@ -457,7 +457,7 @@ 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 { diff --git a/test/integration-test.ts b/test/integration-test.ts index ce576187..e1178bdb 100644 --- a/test/integration-test.ts +++ b/test/integration-test.ts @@ -684,7 +684,7 @@ describe('integration', function () { }); }); - describe('Ablet to provide autocomplete information for element attributes', () => { + describe('Able to provide autocomplete information for element attributes', () => { it('support ...attributes autocomplete', async () => { const result = await getResult( CompletionRequest.method, From acd25e2dcfd292433b7051e9ceca60417da1b6a8 Mon Sep 17 00:00:00 2001 From: Aleksandr Kanunnikov Date: Mon, 28 Mar 2022 15:16:36 +0400 Subject: [PATCH 3/4] feat: ...attributes autocomplete --- test/__snapshots__/integration-test.ts.snap | 82 +++++++++++++++++++++ test/integration-test.ts | 2 +- 2 files changed, 83 insertions(+), 1 deletion(-) diff --git a/test/__snapshots__/integration-test.ts.snap b/test/__snapshots__/integration-test.ts.snap index f265e17b..c562e7df 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 { + "detail": " +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 { + "detail": " +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 e1178bdb..86f66264 100644 --- a/test/integration-test.ts +++ b/test/integration-test.ts @@ -697,7 +697,7 @@ describe('integration', function () { }, }, 'app/components/foo.hbs', - { line: 1, character: 8 } + { line: 0, character: 8 } ); expect(result).toMatchSnapshot(); From bb604a8c9c3dc8ba0e3b5bd10e11dafe17680457 Mon Sep 17 00:00:00 2001 From: Aleksandr Kanunnikov Date: Mon, 28 Mar 2022 15:50:18 +0400 Subject: [PATCH 4/4] feat: ...attributes autocomplete --- .../core/template-completion-provider.ts | 26 ++++++-------- src/builtin-addons/doc/autocomplete.ts | 34 +++++++++++++++++++ test/__snapshots__/integration-test.ts.snap | 4 +-- test/integration-test.ts | 17 ++++++++++ 4 files changed, 63 insertions(+), 18 deletions(-) create mode 100644 src/builtin-addons/doc/autocomplete.ts diff --git a/src/builtin-addons/core/template-completion-provider.ts b/src/builtin-addons/core/template-completion-provider.ts index 1c71c389..920a4b9c 100644 --- a/src/builtin-addons/core/template-completion-provider.ts +++ b/src/builtin-addons/core/template-completion-provider.ts @@ -48,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 @@ -429,22 +430,15 @@ export default class TemplateCompletionProvider { logDebugInfo(candidates, scopedValues); completions.push(...uniqBy([...candidates, ...scopedValues], 'label')); } else if (isElementAttribute(focusPath) && (focusPath.node as ASTv1.AttrNode).name.startsWith('.')) { - completions.push({ - label: '...attributes', - detail: ` - 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. - ` - .split('\n') - .map((e) => e.trim()) - .join('\n'), - kind: CompletionItemKind.Property, - }); + 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/test/__snapshots__/integration-test.ts.snap b/test/__snapshots__/integration-test.ts.snap index c562e7df..c5fcd633 100644 --- a/test/__snapshots__/integration-test.ts.snap +++ b/test/__snapshots__/integration-test.ts.snap @@ -364,7 +364,7 @@ Object { }, "response": Array [ Object { - "detail": " + "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. @@ -3481,7 +3481,7 @@ Object { }, "response": Array [ Object { - "detail": " + "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. diff --git a/test/integration-test.ts b/test/integration-test.ts index 86f66264..a7e35a56 100644 --- a/test/integration-test.ts +++ b/test/integration-test.ts @@ -702,6 +702,23 @@ describe('integration', function () { 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', () => {