Skip to content

Commit

Permalink
feat: autocomplete for tag import shorthand
Browse files Browse the repository at this point in the history
  • Loading branch information
DylanPiercey committed Jul 16, 2022
1 parent a5ba1e6 commit 868a0fa
Show file tree
Hide file tree
Showing 5 changed files with 192 additions and 82 deletions.
6 changes: 6 additions & 0 deletions .changeset/khaki-baboons-report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"marko-vscode": patch
"@marko/language-server": patch
---

Add auto completion for tag import shorthand.
129 changes: 47 additions & 82 deletions server/src/service/marko/complete/OpenTagName.ts
Original file line number Diff line number Diff line change
@@ -1,103 +1,68 @@
import path from "path";
import { URI } from "vscode-uri";
import {
CompletionItemKind,
CompletionItem,
InsertTextFormat,
MarkupKind,
TextEdit,
} from "vscode-languageserver";
import type { TagDefinition } from "@marko/babel-utils";
import { type Node, NodeType } from "../../../utils/parser";
import { getDocFile } from "../../../utils/doc-file";
import type { CompletionMeta, CompletionResult } from ".";
import getTagNameCompletion from "../util/get-tag-name-completion";
import type { CompletionItem } from "vscode-languageserver";

export function OpenTagName({
document,
lookup,
parsed,
node,
}: CompletionMeta<Node.OpenTagName>): CompletionResult {
const currentTemplateFilePath = getDocFile(document);
const importer = getDocFile(document);
const tag = node.parent;
const tagNameLocation = parsed.locationAt(node);
let tags: TagDefinition[];
const range = parsed.locationAt(node);
const isAttrTag = tag.type === NodeType.AttrTag;
const result: CompletionItem[] = [];

if (tag.type === NodeType.AttrTag) {
if (isAttrTag) {
let parentTag = tag.owner;
while (parentTag?.type === NodeType.AttrTag) parentTag = parentTag.owner;
const parentTagDef =
parentTag && parentTag.nameText && lookup.getTag(parentTag.nameText);
tags =
(parentTagDef &&
parentTagDef.nestedTags &&
Object.values(parentTagDef.nestedTags)) ||
[];
} else {
tags = lookup.getTagsSorted().filter((it) => !it.isNestedTag);
}

return tags
.filter((it) => !it.deprecated)
.filter((it) => it.name !== "*")
.filter(
(it) => /^[^_]/.test(it.name) || !/\/node_modules\//.test(it.filePath)
)
.map((it) => {
let label = it.isNestedTag ? `@${it.name}` : it.name;
const fileForTag = it.template || it.renderer || it.filePath;
const fileURIForTag = URI.file(fileForTag).toString();
const nodeModuleMatch = /\/node_modules\/((?:@[^/]+\/)?[^/]+)/.exec(
fileForTag
);

const nodeModuleName = nodeModuleMatch && nodeModuleMatch[1];
const isCoreTag = nodeModuleName === "marko";

const documentation = {
kind: MarkupKind.Markdown,
value: it.html
? `Built in [<${it.name}>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/${it.name}) HTML tag.`
: nodeModuleName
? isCoreTag
? `Core Marko [<${it.name}>](${fileURIForTag}) tag.`
: `Custom Marko tag discovered from the ["${nodeModuleName}"](${fileURIForTag}) npm package.`
: `Custom Marko tag discovered from:\n\n[${
currentTemplateFilePath
? path.relative(currentTemplateFilePath, fileForTag)
: currentTemplateFilePath
}](${fileURIForTag})`,
};

if (it.description) {
documentation.value += `\n\n${it.description}`;
}

const autocomplete = it.autocomplete && it.autocomplete[0];

if (autocomplete) {
if (autocomplete.displayText) {
label = autocomplete.displayText;
}

if (autocomplete.description) {
documentation.value += `\n\n${autocomplete.description}`;
}

if (autocomplete.descriptionMoreURL) {
documentation.value += `\n\n[More Info](${autocomplete.descriptionMoreURL})`;
if (parentTagDef) {
const { nestedTags } = parentTagDef;
for (const key in nestedTags) {
if (key !== "*") {
const tag = nestedTags[key];
result.push(
getTagNameCompletion({
tag,
range,
importer,
showAutoComplete: true,
})
);
}
}
}
} else {
const skipStatements = !(
tag.concise && tag.parent.type === NodeType.Program
);
for (const tag of lookup.getTagsSorted()) {
if (
!(
tag.name === "*" ||
tag.isNestedTag ||
(skipStatements && tag.parseOptions?.statement) ||
(tag.name[0] === "_" &&
/^@?marko[/-]|[\\/]node_modules[\\/]/.test(tag.filePath))
)
) {
result.push(
getTagNameCompletion({
tag,
range,
importer,
showAutoComplete: true,
})
);
}
}
}

return {
label,
documentation,
kind: CompletionItemKind.Class,
insertTextFormat: InsertTextFormat.Snippet,
textEdit: TextEdit.replace(
tagNameLocation,
(autocomplete && autocomplete.snippet) || label
),
} as CompletionItem;
});
return result;
}
59 changes: 59 additions & 0 deletions server/src/service/marko/complete/Statement.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import type { Node } from "../../../utils/parser";
import type { CompletionMeta, CompletionResult } from ".";
import { CompletionItem, TextEdit } from "vscode-languageserver";
import getTagNameCompletion from "../util/get-tag-name-completion";
import { getDocFile } from "../../../utils/doc-file";

const importTagReg = /(['"])<((?:[^\1\\>]+|\\.)*)>?\1/g;

export function Statement({
code,
node,
parsed,
lookup,
document,
}: CompletionMeta<Node.Statement>): CompletionResult {
// check for import statement
if (code[node.start] === "i") {
importTagReg.lastIndex = 0;
const value = parsed.read(node);
const match = importTagReg.exec(value);
if (match) {
const importer = getDocFile(document);
const [{ length }] = match;
const range = parsed.locationAt({
start: node.start + match.index + 1,
end: node.start + match.index + length - 1,
});

const result: CompletionItem[] = [];

for (const tag of lookup.getTagsSorted()) {
if (
(tag.template || tag.renderer) &&
!(
tag.html ||
tag.parser ||
tag.translator ||
tag.isNestedTag ||
tag.name === "*" ||
tag.parseOptions?.statement ||
/^@?marko[/-]/.test(tag.taglibId) ||
(tag.name[0] === "_" && /[\\/]node_modules[\\/]/.test(tag.filePath))
)
) {
const completion = getTagNameCompletion({
tag,
importer,
});

completion.label = `<${completion.label}>`;
completion.textEdit = TextEdit.replace(range, completion.label);
result.push(completion);
}
}

return result;
}
}
}
2 changes: 2 additions & 0 deletions server/src/service/marko/complete/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { Tag } from "./Tag";
import { OpenTagName } from "./OpenTagName";
import { AttrName } from "./AttrName";
import { AttrValue } from "./AttrValue";
import { Statement } from "./Statement";
import type { Plugin, Result } from "../../types";
import { NodeType } from "../../../utils/parser";

Expand All @@ -32,6 +33,7 @@ const handlers: Record<
OpenTagName,
AttrName,
AttrValue,
Statement,
};

export const doComplete: Plugin["doComplete"] = async (doc, params) => {
Expand Down
78 changes: 78 additions & 0 deletions server/src/service/marko/util/get-tag-name-completion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import path from "path";
import type { TagDefinition } from "@marko/babel-utils";
import {
type CompletionItem,
type Range,
CompletionItemKind,
CompletionItemTag,
InsertTextFormat,
MarkupKind,
TextEdit,
} from "vscode-languageserver";
import { URI } from "vscode-uri";

const deprecated = [CompletionItemTag.Deprecated] as CompletionItemTag[];

export default function getTagNameCompletion({
tag,
range,
showAutoComplete,
importer,
}: {
tag: TagDefinition;
range?: Range;
importer?: string;
showAutoComplete?: true;
}): CompletionItem {
let label = tag.isNestedTag ? `@${tag.name}` : tag.name;
const fileForTag = tag.template || tag.renderer || tag.filePath;
const fileURIForTag = URI.file(fileForTag).toString();
const nodeModuleMatch = /\/node_modules\/((?:@[^/]+\/)?[^/]+)/.exec(
fileForTag
);

const nodeModuleName = nodeModuleMatch && nodeModuleMatch[1];
const isCoreTag = nodeModuleName === "marko";

const documentation = {
kind: MarkupKind.Markdown,
value: tag.html
? `Built in [<${tag.name}>](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/${tag.name}) HTML tag.`
: nodeModuleName
? isCoreTag
? `Core Marko [<${tag.name}>](${fileURIForTag}) tag.`
: `Custom Marko tag discovered from the ["${nodeModuleName}"](${fileURIForTag}) npm package.`
: `Custom Marko tag discovered from:\n\n[${
importer ? path.relative(importer, fileForTag) : fileForTag
}](${fileURIForTag})`,
};

if (tag.description) {
documentation.value += `\n\n${tag.description}`;
}

const autocomplete = showAutoComplete ? tag.autocomplete?.[0] : undefined;

if (autocomplete) {
if (autocomplete.displayText) {
label = autocomplete.displayText;
}

if (autocomplete.description) {
documentation.value += `\n\n${autocomplete.description}`;
}

if (autocomplete.descriptionMoreURL) {
documentation.value += `\n\n[More Info](${autocomplete.descriptionMoreURL})`;
}
}

return {
label,
documentation,
tags: tag.deprecated ? deprecated : undefined,
insertTextFormat: autocomplete ? InsertTextFormat.Snippet : undefined,
kind: tag.html ? CompletionItemKind.Property : CompletionItemKind.Class,
textEdit: range && TextEdit.replace(range, autocomplete?.snippet || label),
};
}

0 comments on commit 868a0fa

Please sign in to comment.