Skip to content

Commit

Permalink
feat: template suggestion import (#356)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
lifeart authored Feb 6, 2022
1 parent 5465496 commit 73177fc
Show file tree
Hide file tree
Showing 11 changed files with 436 additions and 32 deletions.
18 changes: 18 additions & 0 deletions src/builtin-addons/core/template-completion-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
};
Expand Down Expand Up @@ -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',
};
Expand All @@ -273,6 +279,9 @@ export default class TemplateCompletionProvider {
return {
label: rawName,
kind: CompletionItemKind.Function,
data: {
files: registry.helper[rawName],
},
detail: 'helper',
};
}),
Expand Down Expand Up @@ -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',
};
Expand All @@ -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',
};
Expand Down Expand Up @@ -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',
};
Expand Down
138 changes: 126 additions & 12 deletions src/completion-provider/glimmer-script-completion-provider.ts
Original file line number Diff line number Diff line change
@@ -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<CompletionItem[]> {
Expand Down Expand Up @@ -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({
Expand All @@ -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;
Expand All @@ -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;
}
}
3 changes: 3 additions & 0 deletions src/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
23 changes: 23 additions & 0 deletions test/__snapshots__/batman-fixture-based-integration-test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -21,6 +27,11 @@ Array [
},
},
Object {
"data": Object {
"files": Array [
"app/components/my-awesome-component.js",
],
},
"detail": "component",
"kind": 7,
"label": "MyAwesomeComponent",
Expand All @@ -39,6 +50,11 @@ Array [
},
},
Object {
"data": Object {
"files": Array [
"app/templates/components/nested/nested-component.hbs",
],
},
"detail": "component",
"kind": 7,
"label": "Nested::NestedComponent",
Expand All @@ -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",
Expand Down
47 changes: 47 additions & 0 deletions test/__snapshots__/fixture-based-integration-test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -21,6 +27,11 @@ Array [
},
},
Object {
"data": Object {
"files": Array [
"app/components/my-awesome-component.js",
],
},
"detail": "component",
"kind": 7,
"label": "MyAwesomeComponent",
Expand All @@ -39,6 +50,11 @@ Array [
},
},
Object {
"data": Object {
"files": Array [
"lib/foo/addon/templates/components/bar.hbs",
],
},
"detail": "component",
"kind": 7,
"label": "Bar",
Expand All @@ -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",
Expand All @@ -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",
Expand All @@ -103,6 +130,11 @@ Array [
},
},
Object {
"data": Object {
"files": Array [
"app/components/my-awesome-component.js",
],
},
"detail": "component",
"kind": 7,
"label": "my-awesome-component",
Expand All @@ -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",
Expand All @@ -139,6 +176,11 @@ Array [
},
},
Object {
"data": Object {
"files": Array [
"lib/foo/addon/templates/components/bar.hbs",
],
},
"detail": "component",
"kind": 7,
"label": "bar",
Expand All @@ -157,6 +199,11 @@ Array [
},
},
Object {
"data": Object {
"files": Array [
"app/helpers/some-helper.js",
],
},
"detail": "helper",
"kind": 3,
"label": "some-helper",
Expand Down
Loading

0 comments on commit 73177fc

Please # to comment.