Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: template suggestion import #356

Merged
merged 19 commits into from
Feb 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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