From 73177fcde8d4a84f711dc3d4f40230a6d68fe62e Mon Sep 17 00:00:00 2001 From: Alex Kanunnikov Date: Mon, 7 Feb 2022 01:38:03 +0300 Subject: [PATCH] feat: template suggestion import (#356) * feat: template imports - auto import suggestions * chore: fix autoimport newline * chore fix edits logic * remove extensions form import * attempt to support addon imports * case transform fixes * keep documentation if exists * add resolved file to metadata * normalize completion request helper * normalize paths in completion requests * fir more tests * fix paths * multiroot normalization * improve roots normalization * sort file results * sort file results * normalize roots * path sep * come on --- .../core/template-completion-provider.ts | 18 +++ .../glimmer-script-completion-provider.ts | 138 ++++++++++++++++-- src/project.ts | 3 + ...man-fixture-based-integration-test.ts.snap | 23 +++ .../fixture-based-integration-test.ts.snap | 47 ++++++ test/__snapshots__/integration-test.ts.snap | 120 +++++++++++++++ test/batman-fixture-based-integration-test.ts | 20 ++- ...glimmer-script-completion-provider-test.ts | 13 ++ test/fixture-based-integration-test.ts | 27 ++-- test/fixtures/batman/project.json | 2 +- test/test_helpers/integration-helpers.ts | 57 +++++++- 11 files changed, 436 insertions(+), 32 deletions(-) diff --git a/src/builtin-addons/core/template-completion-provider.ts b/src/builtin-addons/core/template-completion-provider.ts index 44c03390..0b117ca0 100644 --- a/src/builtin-addons/core/template-completion-provider.ts +++ b/src/builtin-addons/core/template-completion-provider.ts @@ -204,6 +204,9 @@ export default class TemplateCompletionProvider { Object.keys(registry.component).map((rawName) => { return { label: rawName, + data: { + files: registry.component[rawName], + }, kind: CompletionItemKind.Class, detail: 'component', }; @@ -265,6 +268,9 @@ export default class TemplateCompletionProvider { ...Object.keys(registry.component).map((rawName) => { return { label: rawName, + data: { + files: registry.component[rawName], + }, kind: CompletionItemKind.Class, detail: 'component', }; @@ -273,6 +279,9 @@ export default class TemplateCompletionProvider { return { label: rawName, kind: CompletionItemKind.Function, + data: { + files: registry.helper[rawName], + }, detail: 'helper', }; }), @@ -302,6 +311,9 @@ export default class TemplateCompletionProvider { return Object.keys(registry.component).map((rawName) => { return { label: rawName, + data: { + files: registry.component[rawName], + }, kind: CompletionItemKind.Class, detail: 'component', }; @@ -324,6 +336,9 @@ export default class TemplateCompletionProvider { return Object.keys(registry.helper).map((helperName) => { return { label: helperName, + data: { + files: registry.helper[helperName], + }, kind: CompletionItemKind.Function, detail: 'helper', }; @@ -582,6 +597,9 @@ export default class TemplateCompletionProvider { const resolvedModifiers = Object.keys(registry.modifier).map((name) => { return { label: name, + data: { + files: registry.modifier[name], + }, kind: CompletionItemKind.Function, detail: 'modifier', }; diff --git a/src/completion-provider/glimmer-script-completion-provider.ts b/src/completion-provider/glimmer-script-completion-provider.ts index 8eca4873..afc78dc8 100644 --- a/src/completion-provider/glimmer-script-completion-provider.ts +++ b/src/completion-provider/glimmer-script-completion-provider.ts @@ -1,10 +1,15 @@ -import { CompletionItem, TextDocumentPositionParams } from 'vscode-languageserver/node'; +import { CompletionItem, TextDocumentPositionParams, TextEdit, Position, InsertTextFormat } from 'vscode-languageserver/node'; import Server from '../server'; +import ASTPath from '../glimmer-utils'; +import { Project } from '../project'; import { getFileRanges, RangeWalker, getPlaceholderPathFromAst, getScope, documentPartForPosition } from '../utils/glimmer-script'; import { parseScriptFile as parse } from 'ember-meta-explorer'; import { getFocusPath } from '../utils/glimmer-template'; import { TextDocument } from 'vscode-languageserver-textdocument'; - +// @ts-expect-error es module import +import * as camelCase from 'lodash/camelCase'; +import * as path from 'path'; +import { MatchResult } from '../utils/path-matcher'; export default class GlimmerScriptCompletionProvider { constructor(private server: Server) {} async provideCompletions(params: TextDocumentPositionParams): Promise { @@ -32,17 +37,23 @@ export default class GlimmerScriptCompletionProvider { const templateForPosition = documentPartForPosition(templates, params.position); if (templateForPosition) { - const ast = parse(cleanScriptWalker.content, { - sourceType: 'module', - }); - const placeholder = getPlaceholderPathFromAst(ast, templateForPosition.key); + const results: CompletionItem[] = []; + let scopes: string[] = []; - if (!placeholder) { - return []; - } + try { + const ast = parse(cleanScriptWalker.content, { + sourceType: 'module', + }); + const placeholder = getPlaceholderPathFromAst(ast, templateForPosition.key); - const results: CompletionItem[] = []; - const scopes = getScope(placeholder.scope); + if (!placeholder) { + return []; + } + + scopes = getScope(placeholder.scope); + } catch (e) { + // oops + } scopes.forEach((name) => { results.push({ @@ -66,7 +77,7 @@ export default class GlimmerScriptCompletionProvider { const legacyResults = await this.server.templateCompletionProvider.provideCompletionsForFocusPath(info, params.textDocument, params.position, project); legacyResults.forEach((result) => { - results.push(result); + results.push(this.transformLegacyResult(result, scopes, params.position, info.focusPath, project)); }); return results; @@ -76,4 +87,107 @@ export default class GlimmerScriptCompletionProvider { return []; } } + transformLegacyResult(result: CompletionItem, scopes: string[], position: Position, focusPath: ASTPath, project: Project): CompletionItem { + if (!result.data?.files?.length) { + return result; + } + + const files = result.data.files; + const meta: MatchResult[] = files.map((f: string) => { + return project.matchPathToType(f); + }); + + const appScript = meta.find((e) => e.kind === 'script' && e.scope === 'application'); + const appTemplate = meta.find((e) => e.kind === 'template' && e.scope === 'application'); + const addonScript = meta.find((e) => e.kind === 'script' && e.scope === 'addon'); + const addonTemplate = meta.find((e) => e.kind === 'template' && e.scope === 'addon'); + + const fileRef = appScript || appTemplate || addonScript || addonTemplate; + + if (!fileRef) { + return result; + } + + const file = files[meta.indexOf(fileRef)]; + + result.data.resolvedFile = file; + + const fileProject = project.addonForFile(file); + let p = ''; + + if (fileProject) { + p = `${fileProject.name}/${fileRef.type}s/${fileRef.name}`; + } else { + p = path.relative(project.root, file).split('\\').join('/').replace('app', project.name); + p = p.replace('.js', '').replace('.ts', '').replace('.gjs', '').replace('.gts', '').replace('.hbs', ''); + } + + if (p.endsWith('/index')) { + p = p.replace('/index', ''); + } + + let name = result.label; + + if (name.charAt(0).toUpperCase() === name.charAt(0)) { + name = name.includes('::') ? (name.split('::').pop() as string) : name; + + if (name.includes('$')) { + name = name.split('$').pop() as string; + } + // component + } else { + // helper, modifier + name = camelCase(name); + } + + if (!name) { + return result; + } + + if (scopes.includes(name)) { + return result; + } + + const importPath = p; + + result.insertTextFormat = InsertTextFormat.Snippet; + result.detail = `(${result.label}) ${result.detail || ''}`.trim(); + result.documentation = ` + import ${name} from '${importPath}'; + + ${result.documentation || ''} + `.trim(); + result.label = name; + result.additionalTextEdits = [TextEdit.insert(Position.create(0, 0), `import ${name} from '${importPath}';\n`)]; + + const loc = focusPath.node.loc.toJSON(); + + const startPosition = Position.create(position.line, loc.start.column); + let prefix = ``; + + const source = focusPath.sourceForNode(); + + if (source?.startsWith('{{')) { + prefix = '{{'; + } else if (source?.startsWith('(')) { + prefix = '('; + } else if (source?.startsWith('<')) { + prefix = '<'; + } else if (source?.startsWith('@')) { + prefix = '@'; + } + + const txt = `${prefix}${name}`; + const endPosition = Position.create(position.line, loc.start.column + txt.length); + + result.textEdit = TextEdit.replace( + { + start: startPosition, + end: endPosition, + }, + txt + ); + + return result; + } } diff --git a/src/project.ts b/src/project.ts index c7b638a1..04221b71 100644 --- a/src/project.ts +++ b/src/project.ts @@ -156,6 +156,9 @@ export class Project extends BaseProject { this.addonsMeta = this.providers.addonsMeta.filter((el) => el.root !== this.root); this.builtinProviders = initBuiltinProviders(this.addonsMeta); } + addonForFile(filePath: string) { + return this.addonsMeta.find((el) => filePath.startsWith(el.root)); + } constructor(public readonly root: string, addons: string[] = [], pkg: PackageInfo = {}) { super(root); this.addons = addons; diff --git a/test/__snapshots__/batman-fixture-based-integration-test.ts.snap b/test/__snapshots__/batman-fixture-based-integration-test.ts.snap index 94693a19..a5996569 100644 --- a/test/__snapshots__/batman-fixture-based-integration-test.ts.snap +++ b/test/__snapshots__/batman-fixture-based-integration-test.ts.snap @@ -3,6 +3,12 @@ exports[`With \`batman project\` initialized on server Completion request returns all angle-bracket components with same name from different namespaces 1`] = ` Array [ Object { + "data": Object { + "files": Array [ + "app/components/another-awesome-component.js", + "app/templates/components/another-awesome-component.hbs", + ], + }, "detail": "component", "kind": 7, "label": "AnotherAwesomeComponent", @@ -21,6 +27,11 @@ Array [ }, }, Object { + "data": Object { + "files": Array [ + "app/components/my-awesome-component.js", + ], + }, "detail": "component", "kind": 7, "label": "MyAwesomeComponent", @@ -39,6 +50,11 @@ Array [ }, }, Object { + "data": Object { + "files": Array [ + "app/templates/components/nested/nested-component.hbs", + ], + }, "detail": "component", "kind": 7, "label": "Nested::NestedComponent", @@ -57,6 +73,13 @@ Array [ }, }, Object { + "data": Object { + "files": Array [ + "lib/boo/addon/templates/components/bar.hbs", + "lib/foo/addon/components/bar.js", + "lib/foo/addon/templates/components/bar.hbs", + ], + }, "detail": "component", "kind": 7, "label": "Boo$Bar", diff --git a/test/__snapshots__/fixture-based-integration-test.ts.snap b/test/__snapshots__/fixture-based-integration-test.ts.snap index a78b6d52..68085d75 100644 --- a/test/__snapshots__/fixture-based-integration-test.ts.snap +++ b/test/__snapshots__/fixture-based-integration-test.ts.snap @@ -3,6 +3,12 @@ exports[`With \`full-project\` initialized on server Completion request returns all angle-bracket in a element expression 1`] = ` Array [ Object { + "data": Object { + "files": Array [ + "app/components/another-awesome-component.js", + "app/templates/components/another-awesome-component.hbs", + ], + }, "detail": "component", "kind": 7, "label": "AnotherAwesomeComponent", @@ -21,6 +27,11 @@ Array [ }, }, Object { + "data": Object { + "files": Array [ + "app/components/my-awesome-component.js", + ], + }, "detail": "component", "kind": 7, "label": "MyAwesomeComponent", @@ -39,6 +50,11 @@ Array [ }, }, Object { + "data": Object { + "files": Array [ + "lib/foo/addon/templates/components/bar.hbs", + ], + }, "detail": "component", "kind": 7, "label": "Bar", @@ -62,6 +78,11 @@ Array [ exports[`With \`full-project\` initialized on server Completion request returns all angle-bracket in a element expression for in repo addons without batman syntax 1`] = ` Array [ Object { + "data": Object { + "files": Array [ + "lib/foo/addon/templates/components/bar.hbs", + ], + }, "detail": "component", "kind": 7, "label": "Bar", @@ -85,6 +106,12 @@ Array [ exports[`With \`full-project\` initialized on server Completion request returns all components and helpers when requesting completion items in a handlebars expression 1`] = ` Array [ Object { + "data": Object { + "files": Array [ + "app/components/another-awesome-component.js", + "app/templates/components/another-awesome-component.hbs", + ], + }, "detail": "component", "kind": 7, "label": "another-awesome-component", @@ -103,6 +130,11 @@ Array [ }, }, Object { + "data": Object { + "files": Array [ + "app/components/my-awesome-component.js", + ], + }, "detail": "component", "kind": 7, "label": "my-awesome-component", @@ -121,6 +153,11 @@ Array [ }, }, Object { + "data": Object { + "files": Array [ + "app/templates/components/nested/nested-component.hbs", + ], + }, "detail": "component", "kind": 7, "label": "nested/nested-component", @@ -139,6 +176,11 @@ Array [ }, }, Object { + "data": Object { + "files": Array [ + "lib/foo/addon/templates/components/bar.hbs", + ], + }, "detail": "component", "kind": 7, "label": "bar", @@ -157,6 +199,11 @@ Array [ }, }, Object { + "data": Object { + "files": Array [ + "app/helpers/some-helper.js", + ], + }, "detail": "helper", "kind": 3, "label": "some-helper", diff --git a/test/__snapshots__/integration-test.ts.snap b/test/__snapshots__/integration-test.ts.snap index 152d8e63..7ee29425 100644 --- a/test/__snapshots__/integration-test.ts.snap +++ b/test/__snapshots__/integration-test.ts.snap @@ -571,6 +571,11 @@ Object { }, "response": Array [ Object { + "data": Object { + "files": Array [ + "app/components/foo.hbs", + ], + }, "detail": "component", "kind": 7, "label": "Foo", @@ -851,6 +856,11 @@ Object { exports[`integration async fs enabled: false Autocomplete works for broken templates autocomplete information for component #1 {{ 1`] = ` Array [ Object { + "data": Object { + "files": Array [ + "app/components/hello.hbs", + ], + }, "detail": "component", "kind": 7, "label": "hello", @@ -869,6 +879,11 @@ Array [ }, }, Object { + "data": Object { + "files": Array [ + "app/components/darling.hbs", + ], + }, "detail": "component", "kind": 7, "label": "darling", @@ -968,6 +983,11 @@ Object { exports[`integration async fs enabled: false Autocomplete works for broken templates autocomplete information for component #2 < 1`] = ` Array [ Object { + "data": Object { + "files": Array [ + "app/components/hello.hbs", + ], + }, "detail": "component", "kind": 7, "label": "Hello", @@ -986,6 +1006,11 @@ Array [ }, }, Object { + "data": Object { + "files": Array [ + "app/components/darling.hbs", + ], + }, "detail": "component", "kind": 7, "label": "Darling", @@ -1226,6 +1251,11 @@ Object { ], }, Object { + "data": Object { + "files": Array [ + "app/components/hello.hbs", + ], + }, "detail": "component", "kind": 7, "label": "hello", @@ -1246,6 +1276,11 @@ Object { }, }, Object { + "data": Object { + "files": Array [ + "app/components/darling.hbs", + ], + }, "detail": "component", "kind": 7, "label": "darling", @@ -1272,6 +1307,11 @@ Object { exports[`integration async fs enabled: false Autocomplete works for broken templates autocomplete information for helper #5 {{name ( 1`] = ` Array [ Object { + "data": Object { + "files": Array [ + "app/helpers/boo.js", + ], + }, "detail": "helper", "kind": 3, "label": "boo", @@ -1667,6 +1707,11 @@ Object { exports[`integration async fs enabled: false Autocomplete works for broken templates autocomplete information for helper #6 {{name (foo ( 1`] = ` Array [ Object { + "data": Object { + "files": Array [ + "app/helpers/boo.js", + ], + }, "detail": "helper", "kind": 3, "label": "boo", @@ -2121,6 +2166,11 @@ Object { "version": "3.27.0", }, Object { + "data": Object { + "files": Array [ + "app/modifiers/boo.js", + ], + }, "detail": "modifier", "kind": 3, "label": "boo", @@ -2869,6 +2919,11 @@ Array [ }, "response": Array [ Object { + "data": Object { + "files": Array [ + "../addon/components/item.hbs", + ], + }, "detail": "component", "kind": 7, "label": "Item", @@ -2911,6 +2966,11 @@ Array [ }, "response": Array [ Object { + "data": Object { + "files": Array [ + "../addon/components/item.hbs", + ], + }, "detail": "component", "kind": 7, "label": "Item", @@ -3586,6 +3646,11 @@ Object { }, "response": Array [ Object { + "data": Object { + "files": Array [ + "app/components/foo.hbs", + ], + }, "detail": "component", "kind": 7, "label": "Foo", @@ -3866,6 +3931,11 @@ Object { exports[`integration async fs enabled: true Autocomplete works for broken templates autocomplete information for component #1 {{ 1`] = ` Array [ Object { + "data": Object { + "files": Array [ + "app/components/hello.hbs", + ], + }, "detail": "component", "kind": 7, "label": "hello", @@ -3884,6 +3954,11 @@ Array [ }, }, Object { + "data": Object { + "files": Array [ + "app/components/darling.hbs", + ], + }, "detail": "component", "kind": 7, "label": "darling", @@ -3983,6 +4058,11 @@ Object { exports[`integration async fs enabled: true Autocomplete works for broken templates autocomplete information for component #2 < 1`] = ` Array [ Object { + "data": Object { + "files": Array [ + "app/components/hello.hbs", + ], + }, "detail": "component", "kind": 7, "label": "Hello", @@ -4001,6 +4081,11 @@ Array [ }, }, Object { + "data": Object { + "files": Array [ + "app/components/darling.hbs", + ], + }, "detail": "component", "kind": 7, "label": "Darling", @@ -4241,6 +4326,11 @@ Object { ], }, Object { + "data": Object { + "files": Array [ + "app/components/hello.hbs", + ], + }, "detail": "component", "kind": 7, "label": "hello", @@ -4261,6 +4351,11 @@ Object { }, }, Object { + "data": Object { + "files": Array [ + "app/components/darling.hbs", + ], + }, "detail": "component", "kind": 7, "label": "darling", @@ -4287,6 +4382,11 @@ Object { exports[`integration async fs enabled: true Autocomplete works for broken templates autocomplete information for helper #5 {{name ( 1`] = ` Array [ Object { + "data": Object { + "files": Array [ + "app/helpers/boo.js", + ], + }, "detail": "helper", "kind": 3, "label": "boo", @@ -4682,6 +4782,11 @@ Object { exports[`integration async fs enabled: true Autocomplete works for broken templates autocomplete information for helper #6 {{name (foo ( 1`] = ` Array [ Object { + "data": Object { + "files": Array [ + "app/helpers/boo.js", + ], + }, "detail": "helper", "kind": 3, "label": "boo", @@ -5136,6 +5241,11 @@ Object { "version": "3.27.0", }, Object { + "data": Object { + "files": Array [ + "app/modifiers/boo.js", + ], + }, "detail": "modifier", "kind": 3, "label": "boo", @@ -5884,6 +5994,11 @@ Array [ }, "response": Array [ Object { + "data": Object { + "files": Array [ + "../addon/components/item.hbs", + ], + }, "detail": "component", "kind": 7, "label": "Item", @@ -5926,6 +6041,11 @@ Array [ }, "response": Array [ Object { + "data": Object { + "files": Array [ + "../addon/components/item.hbs", + ], + }, "detail": "component", "kind": 7, "label": "Item", diff --git a/test/batman-fixture-based-integration-test.ts b/test/batman-fixture-based-integration-test.ts index ec212757..f7cf01a5 100644 --- a/test/batman-fixture-based-integration-test.ts +++ b/test/batman-fixture-based-integration-test.ts @@ -1,7 +1,15 @@ import * as cp from 'child_process'; import * as path from 'path'; import { URI } from 'vscode-uri'; -import { startServer, initServer, reloadProjects, openFile, normalizeUri, fsProjectToJSON } from './test_helpers/integration-helpers'; +import { + startServer, + initServer, + reloadProjects, + openFile, + normalizeUri, + fsProjectToJSON, + normalizeCompletionRequest, +} from './test_helpers/integration-helpers'; import { createMessageConnection, MessageConnection, Logger, StreamMessageReader, StreamMessageWriter } from 'vscode-jsonrpc/node'; import { CompletionRequest, Definition, DefinitionRequest } from 'vscode-languageserver-protocol/node'; @@ -51,7 +59,8 @@ describe('With `batman project` initialized on server', () => { describe('Completion request', () => { jest.setTimeout(15000); it('returns all angle-bracket in a element expression for in repo addons with batman syntax', async () => { - const applicationTemplatePath = path.join(__dirname, 'fixtures', 'batman', 'app', 'templates', 'batman-completion.hbs'); + const root = path.join(__dirname, 'fixtures', 'batman'); + const applicationTemplatePath = path.join(root, 'app', 'templates', 'batman-completion.hbs'); const params = { textDocument: { uri: URI.file(applicationTemplatePath).toString(), @@ -66,11 +75,12 @@ describe('With `batman project` initialized on server', () => { const response = await connection.sendRequest(CompletionRequest.method, params); - expect(response).toMatchSnapshot(); + expect(normalizeCompletionRequest(response, root)).toMatchSnapshot(); }); it('returns all angle-bracket components with same name from different namespaces', async () => { - const applicationTemplatePath = path.join(__dirname, 'fixtures', 'batman', 'app', 'templates', 'same-component-name.hbs'); + const root = path.join(__dirname, 'fixtures', 'batman'); + const applicationTemplatePath = path.join(root, 'app', 'templates', 'same-component-name.hbs'); const params = { textDocument: { uri: URI.file(applicationTemplatePath).toString(), @@ -85,7 +95,7 @@ describe('With `batman project` initialized on server', () => { const response = await connection.sendRequest(CompletionRequest.method, params); - expect(response).toMatchSnapshot(); + expect(normalizeCompletionRequest(response, root)).toMatchSnapshot(); }); }); diff --git a/test/completion-provider/glimmer-script-completion-provider-test.ts b/test/completion-provider/glimmer-script-completion-provider-test.ts index 3a3595f3..742f6a80 100644 --- a/test/completion-provider/glimmer-script-completion-provider-test.ts +++ b/test/completion-provider/glimmer-script-completion-provider-test.ts @@ -1,6 +1,7 @@ import { Server } from '../../src'; import GlimmerScriptCompletionProvider from '../../src/completion-provider/glimmer-script-completion-provider'; import { Position } from 'vscode-languageserver'; +import { TextDocument } from 'vscode-languageserver-textdocument'; class ProjectMock {} class ServerMock { @@ -54,6 +55,18 @@ describe('GlimmerScriptCompletionProvider', function () { position: Position.create(1, 12), }); + expect(results).toStrictEqual([{ label: 'Component' }, { label: 'n' }]); + }); + it('work with modern logic', async function () { + const tpl = `var n = 42; class Component { \n }`; + const project = new ProjectMock(); + const provider = new GlimmerScriptCompletionProvider(createServer(tpl, project)); + const textDocument = TextDocument.create('/app/foo.gjs', 'javascript', 1, tpl); + const results = await provider.provideCompletions({ + textDocument: textDocument, + position: Position.create(1, 12), + }); + expect(results).toStrictEqual([{ label: 'Component' }, { label: 'n' }]); }); }); diff --git a/test/fixture-based-integration-test.ts b/test/fixture-based-integration-test.ts index 5bf9dac9..2b76d853 100644 --- a/test/fixture-based-integration-test.ts +++ b/test/fixture-based-integration-test.ts @@ -1,7 +1,7 @@ import * as cp from 'child_process'; import * as path from 'path'; import { URI } from 'vscode-uri'; -import { startServer, initServer, reloadProjects, openFile, normalizeUri } from './test_helpers/integration-helpers'; +import { startServer, initServer, reloadProjects, openFile, normalizeUri, normalizeCompletionRequest } from './test_helpers/integration-helpers'; import { createMessageConnection, MessageConnection, Logger, StreamMessageReader, StreamMessageWriter } from 'vscode-jsonrpc/node'; import { CompletionRequest, Definition, DefinitionRequest } from 'vscode-languageserver-protocol/node'; @@ -47,7 +47,8 @@ describe('With `full-project` initialized on server', () => { describe('Completion request', () => { jest.setTimeout(15000); it('returns all components and helpers when requesting completion items in a handlebars expression', async () => { - const applicationTemplatePath = path.join(__dirname, 'fixtures', 'full-project', 'app', 'templates', 'application.hbs'); + const root = path.join(__dirname, 'fixtures', 'full-project'); + const applicationTemplatePath = path.join(root, 'app', 'templates', 'application.hbs'); const params = { textDocument: { uri: URI.file(applicationTemplatePath).toString(), @@ -62,11 +63,12 @@ describe('With `full-project` initialized on server', () => { const response = await connection.sendRequest(CompletionRequest.method, params); - expect(response).toMatchSnapshot(); + expect(normalizeCompletionRequest(response, root)).toMatchSnapshot(); }); it('returns all angle-bracket in a element expression', async () => { - const applicationTemplatePath = path.join(__dirname, 'fixtures', 'full-project', 'app', 'templates', 'angle-completion.hbs'); + const root = path.join(__dirname, 'fixtures', 'full-project'); + const applicationTemplatePath = path.join(root, 'app', 'templates', 'angle-completion.hbs'); const params = { textDocument: { uri: URI.file(applicationTemplatePath).toString(), @@ -81,11 +83,12 @@ describe('With `full-project` initialized on server', () => { const response = await connection.sendRequest(CompletionRequest.method, params); - expect(response).toMatchSnapshot(); + expect(normalizeCompletionRequest(response, root)).toMatchSnapshot(); }); it('returns all routes when requesting completion items in an inline link-to', async () => { - const templatePath = path.join(__dirname, 'fixtures', 'full-project', 'app', 'templates', 'definition.hbs'); + const root = path.join(__dirname, 'fixtures', 'full-project'); + const templatePath = path.join(root, 'app', 'templates', 'definition.hbs'); const params = { textDocument: { uri: URI.file(templatePath).toString(), @@ -100,11 +103,12 @@ describe('With `full-project` initialized on server', () => { const response = await connection.sendRequest(CompletionRequest.method, params); - expect(response).toMatchSnapshot(); + expect(normalizeCompletionRequest(response, root)).toMatchSnapshot(); }); it('returns all routes when requesting completion items in a block link-to', async () => { - const templatePath = path.join(__dirname, 'fixtures', 'full-project', 'app', 'templates', 'definition.hbs'); + const root = path.join(__dirname, 'fixtures', 'full-project'); + const templatePath = path.join(root, 'app', 'templates', 'definition.hbs'); const params = { textDocument: { uri: URI.file(templatePath).toString(), @@ -119,11 +123,12 @@ describe('With `full-project` initialized on server', () => { const response = await connection.sendRequest(CompletionRequest.method, params); - expect(response).toMatchSnapshot(); + expect(normalizeCompletionRequest(response, root)).toMatchSnapshot(); }); it('returns all angle-bracket in a element expression for in repo addons without batman syntax', async () => { - const applicationTemplatePath = path.join(__dirname, 'fixtures', 'full-project', 'app', 'templates', 'inrepo-addon-completion.hbs'); + const root = path.join(__dirname, 'fixtures', 'full-project'); + const applicationTemplatePath = path.join(root, 'app', 'templates', 'inrepo-addon-completion.hbs'); const params = { textDocument: { uri: URI.file(applicationTemplatePath).toString(), @@ -138,7 +143,7 @@ describe('With `full-project` initialized on server', () => { const response = await connection.sendRequest(CompletionRequest.method, params); - expect(response).toMatchSnapshot(); + expect(normalizeCompletionRequest(response, root)).toMatchSnapshot(); }); }); diff --git a/test/fixtures/batman/project.json b/test/fixtures/batman/project.json index 9b3af4c5..6cf1ac9f 100644 --- a/test/fixtures/batman/project.json +++ b/test/fixtures/batman/project.json @@ -1,6 +1,6 @@ { "app/app.js": "import Application from '@ember/application';\r\nimport Resolver from './resolver';\r\nimport loadInitializers from 'ember-load-initializers';\r\nimport config from './config/environment';\r\n\r\nconst App = Application.extend({\r\n modulePrefix: config.modulePrefix,\r\n podModulePrefix: config.podModulePrefix,\r\n Resolver\r\n});\r\n\r\nloadInitializers(App, config.modulePrefix);\r\n\r\nexport default App;\r\n", - "app/components/another-awesome-component.js": "import Bar from 'foo/components/bar';\nexport default class Awesome extends Bar {\n\n}\n", + "app/components/another-awesome-component.js": "import Bar from 'foo/components/bar';\r\nexport default class Awesome extends Bar {\r\n\r\n}\r\n", "app/components/my-awesome-component.js": "", "app/helpers/some-helper.js": "", "app/models/model-a.js": "import DS from 'ember-data';\r\n\r\nexport default DS.Model.extend({\r\n\r\n modelB: DS.hasMany('model-b'),\r\n\r\n someAttr: DS.attr('custom-transform')\r\n\r\n});\r\n", diff --git a/test/test_helpers/integration-helpers.ts b/test/test_helpers/integration-helpers.ts index 22a75bec..6c957eeb 100644 --- a/test/test_helpers/integration-helpers.ts +++ b/test/test_helpers/integration-helpers.ts @@ -62,6 +62,39 @@ export type Registry = { }; }; +export function normalizeCompletionRequest(results: CompletionItem[] | unknown, root: string | string[]) { + const roots_ = Array.isArray(root) ? root : [root]; + const roots = Array.from(new Set(roots_)) + .map((r) => normalizePath(r)) + .sort(); + const bestRootForPath = (fPath) => { + const looksLikeRoots = roots.filter((r) => fPath.startsWith(r + '/')).sort((a, b) => b.length - a.length); + + if (looksLikeRoots.length) { + return looksLikeRoots[0]; + } else { + return roots[0]; + } + }; + + return (results as CompletionItem[]).map((r) => { + if (r.data) { + if (r.data.files) { + r.data.files = r.data.files + .map((f) => normalizePath(f)) + .map((f) => path.relative(bestRootForPath(f), f)) + .sort(); + } + + if (r.data.resolvedFile) { + r.data.resolvedFile = path.relative(bestRootForPath(r.data.resolvedFile), r.data.resolvedFile); + } + } + + return r; + }); +} + export function asyncFSProvider() { // likely we should emit special error objects instead of nulls // if (error !== null && typeof error === 'object' && (error.code === 'ENOENT' || error.code === 'ENOTDIR' || error.code === 'EPERM')) { @@ -308,7 +341,7 @@ export async function createProject( files: unknown, connection: MessageConnection, projectName: string[] -): Promise<{ normalizedPath: string[]; originalPath: string; result: UnknownResult[]; destroy(): Promise }>; +): Promise<{ normalizedPath: string[]; originalPath: string[]; result: UnknownResult[]; destroy(): Promise }>; export async function createProject( files: unknown, connection: MessageConnection, @@ -500,15 +533,33 @@ export async function getResult( const params = textDocument(modelPath, position); openFile(connection, modelPath); - const response = await connection.sendRequest(reqType as never, params); + let response = await connection.sendRequest(reqType as never, params); + + if (reqType === CompletionRequest.method) { + if (!Array.isArray(projectName) && Array.isArray(response)) { + response = normalizeCompletionRequest(response, originalPath); + } + } await destroy(); if (Array.isArray(projectName)) { const resultsArr: IResponse[] = []; + const roots = result.reduce( + (acc, el) => { + acc.push(...el.addonsMeta.map((e) => e.root)); + + return acc; + }, + [...originalPath] + ); for (let i = 0; i < projectName.length; i++) { - resultsArr.push(_buildResponse(response, normalizedPath[i], result[i])); + if (reqType === CompletionRequest.method) { + resultsArr.push(_buildResponse(normalizeCompletionRequest(response, roots), normalizedPath[i], result[i])); + } else { + resultsArr.push(_buildResponse(response, normalizedPath[i], result[i])); + } } return resultsArr;