diff --git a/.changeset/eighty-books-camp.md b/.changeset/eighty-books-camp.md new file mode 100644 index 00000000..ff89200d --- /dev/null +++ b/.changeset/eighty-books-camp.md @@ -0,0 +1,6 @@ +--- +"marko-vscode": patch +"@marko/language-server": patch +--- + +Improve filesystem completions. diff --git a/server/src/service/marko/complete/AttrValue.ts b/server/src/service/marko/complete/AttrValue.ts index 3091f306..ad6f5a4f 100644 --- a/server/src/service/marko/complete/AttrValue.ts +++ b/server/src/service/marko/complete/AttrValue.ts @@ -30,37 +30,39 @@ export async function AttrValue({ end, }); - let segmentStart = rawValue.lastIndexOf("/", relativeOffset); - if (segmentStart === -1) segmentStart = relativeOffset; + const segmentStart = rawValue.lastIndexOf("/", relativeOffset); + if (segmentStart === -1) return; // only resolve after a slash. - const resolveRequest = rawValue.slice(0, segmentStart) || "."; - const dir = resolveUrl(resolveRequest, document.uri); + const req = rawValue.slice(0, segmentStart); + const uri = resolveUrl(req, document.uri); - if (dir?.[0] === "/") { + if (uri) { const result: CompletionItem[] = []; - const curDir = - resolveRequest === "." ? dir : resolveUrl(".", document.uri); - const curFile = curDir === dir ? path.basename(document.uri) : undefined; + const curFile = req === "." ? 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)) { + for (const [entry, type] of await fileSystem.readDirectory(uri)) { 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 + result.push( + type === FileType.Directory ? { - title: "Suggest", - command: "editor.action.triggerSuggest", + label: `${entry}/`, + kind: CompletionItemKind.Folder, + textEdit: TextEdit.replace(replaceRange, `${entry}/`), + command: { + title: "Suggest", + command: "editor.action.triggerSuggest", + }, } - : undefined, - }); + : { + label: entry, + kind: CompletionItemKind.File, + textEdit: TextEdit.replace(replaceRange, entry), + } + ); } } diff --git a/server/src/service/stylesheet/index.ts b/server/src/service/stylesheet/index.ts index d90f42f5..500c49cb 100644 --- a/server/src/service/stylesheet/index.ts +++ b/server/src/service/stylesheet/index.ts @@ -166,7 +166,7 @@ const StyleSheetService: Partial = { return result.length ? result : undefined; } }, - findDocumentLinks(doc) { + async findDocumentLinks(doc) { const infoByExt = getStyleSheetInfo(doc); const result: DocumentLink[] = []; @@ -174,9 +174,11 @@ const StyleSheetService: Partial = { const info = infoByExt[ext]; const { service, virtualDoc } = info; - for (const link of service.findDocumentLinks(virtualDoc, info.parsed, { - resolveReference, - })) { + for (const link of await service.findDocumentLinks2( + virtualDoc, + info.parsed, + { resolveReference } + )) { const range = getSourceRange(doc, info, link.range); if (range) { result.push({ diff --git a/server/src/utils/file-system.ts b/server/src/utils/file-system.ts index df64402a..62a63d11 100644 --- a/server/src/utils/file-system.ts +++ b/server/src/utils/file-system.ts @@ -1,25 +1,27 @@ -import path from "path"; import fs from "fs/promises"; import { type FileStat, FileType } from "vscode-css-languageservice"; +import { fileURLToPath } from "url"; export { FileStat, FileType }; export default { stat, readDirectory, }; -async function stat(fileName: string): Promise { - const stat = await fs.stat(fileName).catch(() => null); +async function stat(uri: string): Promise { let type = FileType.Unknown; - let ctime = 0; - let mtime = 0; - let size = 0; + let ctime = -1; + let mtime = -1; + let size = -1; - if (stat) { + try { + const stat = await fs.stat(fileURLToPath(uri)); if (stat.isDirectory()) type = FileType.Directory; else if (stat.isFile()) type = FileType.File; ctime = stat.ctimeMs; mtime = stat.mtimeMs; size = stat.size; + } catch { + // ignore } return { @@ -30,18 +32,22 @@ async function stat(fileName: string): Promise { }; } -async function readDirectory(dir: string): Promise<[string, FileType][]> { - return ( - await Promise.all( - ( - await fs.readdir(dir).catch(() => []) - ).map( - async (entry) => - [entry, (await stat(path.join(dir, entry))).type] as [ - string, - FileType - ] +async function readDirectory(uri: string): Promise<[string, FileType][]> { + try { + const entries = await fs.readdir(fileURLToPath(uri)); + const base = uri.at(-1) === "/" ? uri : `${uri}/`; + return ( + await Promise.all( + entries.map( + async (entry) => + [entry, (await stat(new URL(entry, base).toString())).type] as [ + string, + FileType + ] + ) ) - ) - ).filter(([, type]) => type !== FileType.Unknown); + ).filter(([, type]) => type !== FileType.Unknown); + } catch { + return []; + } } diff --git a/server/src/utils/resolve-url.ts b/server/src/utils/resolve-url.ts index 6ef64a27..82fd79dd 100644 --- a/server/src/utils/resolve-url.ts +++ b/server/src/utils/resolve-url.ts @@ -1,13 +1,7 @@ export default function resolveUrl(to: string, base: string) { try { - const baseUrl = new URL(base, "file://"); - const resolved = new URL(to, baseUrl); - const { origin, protocol } = baseUrl; - if (resolved.origin === origin && resolved.protocol === protocol) { - // result is relative to the base URL. - return resolved.pathname + resolved.search + resolved.hash; - } - return resolved.toString(); + const url = new URL(to, base); + if (url.protocol === "file:") return url.toString(); } catch { return undefined; }