Skip to content

Commit

Permalink
feat: completions for filesystem paths
Browse files Browse the repository at this point in the history
chore: refactor stylesheet plugin to avoid mutations in most cases
  • Loading branch information
DylanPiercey committed Jul 15, 2022
1 parent c1f0d97 commit f88ca6d
Show file tree
Hide file tree
Showing 11 changed files with 393 additions and 139 deletions.
6 changes: 6 additions & 0 deletions .changeset/nasty-spies-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"marko-vscode": patch
"@marko/language-server": patch
---

Improve completions for file system paths.
3 changes: 2 additions & 1 deletion server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ console.error = (...args: unknown[]) => {
process.on("uncaughtException", console.error);
process.on("unhandledRejection", console.error);

connection.onInitialize(() => {
connection.onInitialize(async (params) => {
setupMessages(connection);
await service.initialize(params);

return {
capabilities: {
Expand Down
3 changes: 3 additions & 0 deletions server/src/service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ const plugins = [MarkoPlugin, StyleSheetPlugin];
* Facade to all embedded plugins, eg css, typescript and our own.
*/
const service: Plugin = {
async initialize(params) {
await Promise.all(plugins.map((plugin) => plugin.initialize?.(params)));
},
async doComplete(doc, params, cancel) {
const result = CompletionList.create([], false);

Expand Down
70 changes: 70 additions & 0 deletions server/src/service/marko/complete/AttrValue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import path from "path";
import {
CompletionItem,
CompletionItemKind,
Range,
TextEdit,
} from "vscode-languageserver";
import type { Node } from "../../../utils/parser";
import isDocumentLinkAttr from "../util/is-document-link-attr";
import fileSystem, { FileType } from "../../../utils/file-system";
import resolveUrl from "../../../utils/resolve-url";
import type { CompletionMeta } from ".";

export async function AttrValue({
document,
offset,
node,
parsed,
code,
}: CompletionMeta<Node.AttrValue>): Promise<void | CompletionItem[]> {
const attr = node.parent;
if (isDocumentLinkAttr(document, attr.parent, attr)) {
const start = node.value.start + 1;
if (code[start] !== ".") return; // only resolve relative paths

const end = node.value.end - 1;
const relativeOffset = offset - start;
const rawValue = parsed.read({
start,
end,
});

let segmentStart = rawValue.lastIndexOf("/", relativeOffset);
if (segmentStart === -1) segmentStart = relativeOffset;

const resolveRequest = rawValue.slice(0, segmentStart) || ".";
const dir = resolveUrl(resolveRequest, document.uri);

if (dir?.[0] === "/") {
const result: CompletionItem[] = [];
const curDir =
resolveRequest === "." ? dir : resolveUrl(".", document.uri);
const curFile = curDir === dir ? path.basename(document.uri) : undefined;
const replaceRange = Range.create(
document.positionAt(start + segmentStart + 1),
document.positionAt(start + rawValue.length)
);

for (const [entry, type] of await fileSystem.readDirectory(dir)) {
if (entry[0] !== "." && entry !== curFile) {
const isDir = type === FileType.Directory;
const label = isDir ? `${entry}/` : entry;
result.push({
label,
kind: isDir ? CompletionItemKind.Folder : CompletionItemKind.File,
textEdit: TextEdit.replace(replaceRange, label),
command: isDir
? {
title: "Suggest",
command: "editor.action.triggerSuggest",
}
: undefined,
});
}
}

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 @@ -9,6 +9,7 @@ import { getCompilerInfo, parse } from "../../../utils/compiler";
import { Tag } from "./Tag";
import { OpenTagName } from "./OpenTagName";
import { AttrName } from "./AttrName";
import { AttrValue } from "./AttrValue";
import type { Plugin, Result } from "../../types";
import { NodeType } from "../../../utils/parser";

Expand All @@ -30,6 +31,7 @@ const handlers: Record<
Tag,
OpenTagName,
AttrName,
AttrValue,
};

export const doComplete: Plugin["doComplete"] = async (doc, params) => {
Expand Down
44 changes: 11 additions & 33 deletions server/src/service/marko/document-links/extract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,9 @@ import {
NodeType,
} from "../../../utils/parser";
import resolveUrl from "../../../utils/resolve-url";
import isDocumentLinkAttr from "../util/is-document-link-attr";

const importTagReg = /(['"])<((?:[^\1\\>]+|\\.)*)>?\1/g;
const linkedAttrs: Record<string, Set<string>> = {
src: new Set([
"audio",
"embed",
"iframe",
"img",
"input",
"script",
"source",
"track",
"video",
]),
href: new Set(["a", "area", "link"]),
data: new Set(["object"]),
poster: new Set(["video"]),
};

/**
* Iterate over the Marko CST and extract all the file links in the document.
Expand All @@ -49,23 +34,16 @@ export function extractDocumentLinks(
case NodeType.Tag:
if (node.attrs && node.nameText) {
for (const attr of node.attrs) {
if (
attr.type === NodeType.AttrNamed &&
attr.value?.type === NodeType.AttrValue &&
/^['"]$/.test(code[attr.value.value.start])
) {
const attrName = read(attr.name);
if (linkedAttrs[attrName]?.has(node.nameText)) {
links.push(
DocumentLink.create(
{
start: parsed.positionAt(attr.value.value.start),
end: parsed.positionAt(attr.value.value.end),
},
resolveUrl(read(attr.value.value).slice(1, -1), doc.uri)
)
);
}
if (isDocumentLinkAttr(doc, node, attr)) {
links.push(
DocumentLink.create(
{
start: parsed.positionAt(attr.value.value.start),
end: parsed.positionAt(attr.value.value.end),
},
resolveUrl(read(attr.value.value).slice(1, -1), doc.uri)
)
);
}
}
}
Expand Down
39 changes: 39 additions & 0 deletions server/src/service/marko/util/is-document-link-attr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import type { TextDocument } from "vscode-languageserver-textdocument";
import { type Node, NodeType } from "../../../utils/parser";

const linkedAttrs: Map<string, Set<string>> = new Map([
[
"src",
new Set([
"audio",
"embed",
"iframe",
"img",
"input",
"script",
"source",
"track",
"video",
]),
],
["href", new Set(["a", "area", "link"])],
["data", new Set(["object"])],
["poster", new Set(["video"])],
]);

export default function isDocumentLinkAttr(
doc: TextDocument,
tag: Node.ParentTag,
attr: Node.AttrNode
): attr is Node.AttrNamed & { value: Node.AttrValue } {
return (
(tag.nameText &&
attr.type === NodeType.AttrNamed &&
attr.value?.type === NodeType.AttrValue &&
/^['"]$/.test(doc.getText()[attr.value.value.start]) &&
linkedAttrs
.get(doc.getText().slice(attr.name.start, attr.name.end))
?.has(tag.nameText)) ||
false
);
}
Loading

0 comments on commit f88ca6d

Please sign in to comment.