From 584a1199d4c83fa9b2d41bda91ef1298021d0be7 Mon Sep 17 00:00:00 2001 From: Aleksandr Kanunnikov Date: Mon, 28 Mar 2022 17:35:41 +0400 Subject: [PATCH] feat: each key arguments autocomplete --- .../core/template-completion-provider.ts | 16 ++- src/utils/ast-helpers.ts | 2 +- test/__snapshots__/integration-test.ts.snap | 136 ++++++++++++++++++ test/integration-test.ts | 38 +++++ 4 files changed, 188 insertions(+), 4 deletions(-) diff --git a/src/builtin-addons/core/template-completion-provider.ts b/src/builtin-addons/core/template-completion-provider.ts index 808e90e2..0b9bb173 100644 --- a/src/builtin-addons/core/template-completion-provider.ts +++ b/src/builtin-addons/core/template-completion-provider.ts @@ -51,7 +51,6 @@ import { URI } from 'vscode-uri'; import { componentsContextData } from './template-context-provider'; import { IRegistry } from '../../utils/registry-api'; import { argumentsForBuiltinComponent, docForAttribute, valuesForBuiltinComponentArgument } from '../doc/autocomplete'; -import { isPathExpression } from '../../utils/ast-helpers'; const mListModifiers = memoize(listModifiers, { length: 1, maxAge: 60000 }); // 1 second const mListComponents = memoize(listComponents, { length: 1, maxAge: 60000 }); // 1 second @@ -433,6 +432,8 @@ export default class TemplateCompletionProvider { logDebugInfo(candidates, scopedValues); completions.push(...uniqBy([...candidates, ...scopedValues], 'label')); } else if (isElementAttribute(focusPath) && (focusPath.node as ASTv1.AttrNode).name.startsWith('.')) { + logDebugInfo('isElementAttribute'); + const attrName = '...attributes'; if (!(focusPath.parent as ASTv1.ElementNode).attributes.find((attr) => attr.name === attrName)) { @@ -443,6 +444,8 @@ export default class TemplateCompletionProvider { }); } } else if (isComponentArgumentName(focusPath)) { + logDebugInfo('isComponentArgumentName'); + // const maybeComponentName = focusPath.parent.tag; @@ -501,6 +504,7 @@ export default class TemplateCompletionProvider { completions.push(...uniqBy(candidates, 'label')); } else if (isArgumentPathExpression(focusPath)) { + logDebugInfo('isArgumentPathExpression'); // {{@ite..}} const rawCandidates = await this.getLocalPathExpressionCandidates(uri, originalText); const candidates = rawCandidates.filter((el) => { @@ -544,6 +548,8 @@ export default class TemplateCompletionProvider { completions.push(...uniqBy(candidates, 'label')); completions.push(...emberSubExpressionItems); } else if (isPathExpression(focusPath)) { + logDebugInfo('isPathExpression'); + if (isScopedPathExpression(focusPath)) { const scopedValues = this.getScopedValues(focusPath); @@ -622,7 +628,7 @@ export default class TemplateCompletionProvider { completions.push(...uniqBy([...emberModifierItems, ...resolvedModifiers, ...builtinModifiers()], 'label')); } else if (isHashPair(focusPath)) { - // {{#each key=value}} + // {{#each key=}} logDebugInfo('isHashPair'); const grandPaparentNode = focusPath.parentPath?.parentPath?.node as ASTv1.BlockStatement; @@ -643,9 +649,11 @@ export default class TemplateCompletionProvider { } } } else if (isHashPairValue(focusPath)) { + // {{#each key=value}} {{/each}} + logDebugInfo('isHashPairValue'); const grandPaparentNode = focusPath.parentPath?.parentPath?.parentPath?.node as ASTv1.BlockStatement; - if (isPathExpression(grandPaparentNode.path)) { + if (grandPaparentNode && grandPaparentNode.type === 'BlockStatement' && isPathExpression(grandPaparentNode.path)) { const name = (grandPaparentNode.path as ASTv1.PathExpression).original; completions.push( @@ -658,6 +666,8 @@ export default class TemplateCompletionProvider { }) ); } + } else { + logDebugInfo('no match'); } } catch (e) { logError(e); diff --git a/src/utils/ast-helpers.ts b/src/utils/ast-helpers.ts index 346d12f3..4811274b 100644 --- a/src/utils/ast-helpers.ts +++ b/src/utils/ast-helpers.ts @@ -274,7 +274,7 @@ export function isHashPair(path: ASTPath) { } export function isHashPairValue(path: ASTPath) { - return path.parent && isHashPair(path.parent) && path.parent.value === path.node; + return path.parent && isHashPair(path.parentPath as ASTPath) && path.parent.value === path.node; } export function isSubExpressionPath(path: ASTPath): boolean { diff --git a/test/__snapshots__/integration-test.ts.snap b/test/__snapshots__/integration-test.ts.snap index c5fcd633..0f9a8cc4 100644 --- a/test/__snapshots__/integration-test.ts.snap +++ b/test/__snapshots__/integration-test.ts.snap @@ -352,6 +352,74 @@ Object { } `; +exports[`integration async fs enabled: false Able to provide autocomplete information for each attributes autocomplete key argument 1`] = ` +Array [ + Object { + "documentation": " +The \`key\` option is used to tell Ember how to determine if the items in the +array being iterated over with \`{{#each}}\` has changed between renders. By +default the item's object identity is used. +", + "kind": 10, + "label": "key", + "textEdit": Object { + "newText": "key", + "range": Object { + "end": Object { + "character": 9, + "line": 0, + }, + "start": Object { + "character": 9, + "line": 0, + }, + }, + }, + }, +] +`; + +exports[`integration async fs enabled: false Able to provide autocomplete information for each attributes autocomplete key argument values 1`] = ` +Array [ + Object { + "documentation": "The index of the item in the array.", + "kind": 12, + "label": "@identity", + "textEdit": Object { + "newText": "@identity", + "range": Object { + "end": Object { + "character": 13, + "line": 0, + }, + "start": Object { + "character": 13, + "line": 0, + }, + }, + }, + }, + Object { + "documentation": "The item in the array itself.", + "kind": 12, + "label": "@index", + "textEdit": Object { + "newText": "@index", + "range": Object { + "end": Object { + "character": 13, + "line": 0, + }, + "start": Object { + "character": 13, + "line": 0, + }, + }, + }, + }, +] +`; + exports[`integration async fs enabled: false Able to provide autocomplete information for element attributes support ...attributes autocomplete 1`] = ` Object { "addonsMeta": Array [], @@ -3469,6 +3537,74 @@ Object { } `; +exports[`integration async fs enabled: true Able to provide autocomplete information for each attributes autocomplete key argument 1`] = ` +Array [ + Object { + "documentation": " +The \`key\` option is used to tell Ember how to determine if the items in the +array being iterated over with \`{{#each}}\` has changed between renders. By +default the item's object identity is used. +", + "kind": 10, + "label": "key", + "textEdit": Object { + "newText": "key", + "range": Object { + "end": Object { + "character": 9, + "line": 0, + }, + "start": Object { + "character": 9, + "line": 0, + }, + }, + }, + }, +] +`; + +exports[`integration async fs enabled: true Able to provide autocomplete information for each attributes autocomplete key argument values 1`] = ` +Array [ + Object { + "documentation": "The index of the item in the array.", + "kind": 12, + "label": "@identity", + "textEdit": Object { + "newText": "@identity", + "range": Object { + "end": Object { + "character": 13, + "line": 0, + }, + "start": Object { + "character": 13, + "line": 0, + }, + }, + }, + }, + Object { + "documentation": "The item in the array itself.", + "kind": 12, + "label": "@index", + "textEdit": Object { + "newText": "@index", + "range": Object { + "end": Object { + "character": 13, + "line": 0, + }, + "start": Object { + "character": 13, + "line": 0, + }, + }, + }, + }, +] +`; + exports[`integration async fs enabled: true Able to provide autocomplete information for element attributes support ...attributes autocomplete 1`] = ` Object { "addonsMeta": Array [], diff --git a/test/integration-test.ts b/test/integration-test.ts index a7e35a56..1dd5dcc9 100644 --- a/test/integration-test.ts +++ b/test/integration-test.ts @@ -721,6 +721,44 @@ describe('integration', function () { }); }); + describe('Able to provide autocomplete information for each attributes', () => { + it('autocomplete key argument', async () => { + const result = await getResult( + CompletionRequest.method, + connection, + { + app: { + components: { + 'foo.hbs': ['{{#each k="" }}', '{{/each}}'].join('\n'), + }, + }, + }, + 'app/components/foo.hbs', + { line: 0, character: 9 } + ); + + expect(result.response).toMatchSnapshot(); + }); + + it('autocomplete key argument values', async () => { + const result = await getResult( + CompletionRequest.method, + connection, + { + app: { + components: { + 'foo.hbs': ['{{#each key="" }}', '{{/each}}'].join('\n'), + }, + }, + }, + 'app/components/foo.hbs', + { line: 0, character: 13 } + ); + + expect(result.response).toMatchSnapshot(); + }); + }); + describe('Able to provide autocomplete information for local scoped params', () => { it('support tag blocks', async () => { const result = await getResult(