-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: autocomplete for tag import shorthand
- Loading branch information
1 parent
a5ba1e6
commit 868a0fa
Showing
5 changed files
with
192 additions
and
82 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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), | ||
}; | ||
} |