Generate typed QX bindings and add source editing tools
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
import { lstat, readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { parseQx, validateQxImportPath, walkSyntax } from "./source.js";
|
||||
|
||||
export const readQxSource = async (root: string, name: string) => {
|
||||
validateQxImportPath(name);
|
||||
let current = path.resolve(root);
|
||||
const segments = name.split("/");
|
||||
for (const [index, segment] of segments.entries()) {
|
||||
current = path.join(current, segment);
|
||||
const stat = await lstat(current);
|
||||
if (stat.isSymbolicLink() || (index === segments.length - 1 ? !stat.isFile() : !stat.isDirectory()))
|
||||
throw new Error(`QX imports must be ordinary files beneath ordinary directories: ${name}`);
|
||||
}
|
||||
return readFile(current, "utf8");
|
||||
};
|
||||
|
||||
/** Local imports share one workspace scope; paths are always repository-relative. */
|
||||
export const resolveQxSources = async (
|
||||
read: (name: string) => Promise<string>, entry = "workspace.qx",
|
||||
) => {
|
||||
const files = new Map<string, string>();
|
||||
const active: string[] = [];
|
||||
const visited = new Set<string>();
|
||||
const segments: { start: number; end: number; fileName: string; sourceStart: number }[] = [];
|
||||
let source = "";
|
||||
const append = (fileName: string, text: string, sourceStart: number) => {
|
||||
segments.push({ start: source.length, end: source.length + text.length, fileName, sourceStart });
|
||||
source += text;
|
||||
};
|
||||
const visit = async (name: string, root: boolean) => {
|
||||
validateQxImportPath(name);
|
||||
if (active.includes(name)) throw new Error(`QX import cycle: ${[...active, name].join(" -> ")}`);
|
||||
if (visited.has(name)) return;
|
||||
const text = await read(name);
|
||||
const syntax = parseQx(text, name);
|
||||
if (syntax.diagnostics.length) throw new Error(syntax.diagnostics.map((d) =>
|
||||
`${name}:${d.line}:${d.column + 1}: ${d.message}`).join("\n"));
|
||||
const declaration = syntax.root.children[0]!;
|
||||
if (declaration.kind !== (root ? "workspaceDecl" : "fragmentDecl"))
|
||||
throw new Error(`${name}: expected ${root ? "workspace" : "fragment"} document`);
|
||||
files.set(name, text);
|
||||
active.push(name);
|
||||
visited.add(name);
|
||||
const braces = syntax.tokens.filter((token) => !token.trivia);
|
||||
let cursor = root ? 0 : braces.find((token) => token.kind === "LBRACE")!.end;
|
||||
const end = root ? text.length : braces.filter((token) => token.kind === "RBRACE").at(-1)!.start;
|
||||
for (const node of walkSyntax(declaration)) {
|
||||
if (node.kind !== "sourceImportDecl") continue;
|
||||
append(name, text.slice(cursor, node.start), cursor);
|
||||
const literal = node.children.find((child) => child.kind === "stringLiteral")!;
|
||||
// Separators prevent adjacent tokens/comments joining across files.
|
||||
append(name, "\n", node.start);
|
||||
await visit(JSON.parse(text.slice(literal.start, literal.end)), false);
|
||||
append(name, "\n", node.end);
|
||||
cursor = node.end;
|
||||
}
|
||||
append(name, text.slice(cursor, end), cursor);
|
||||
active.pop();
|
||||
};
|
||||
await visit(entry, true);
|
||||
return {
|
||||
source, sourceFiles: [...files.keys()], files,
|
||||
originalPosition(line: number, column: number) {
|
||||
const lines = source.split("\n");
|
||||
const offset = lines.slice(0, line - 1).reduce((sum, value) => sum + value.length + 1, 0) +
|
||||
[...(lines[line - 1] ?? "")].slice(0, column).join("").length;
|
||||
const segment = segments.find((entry) => entry.start <= offset && entry.end > offset);
|
||||
if (!segment) return { fileName: entry, line, column };
|
||||
const prefix = files.get(segment.fileName)!.slice(0, segment.sourceStart + offset - segment.start);
|
||||
return { fileName: segment.fileName, line: prefix.split("\n").length,
|
||||
column: prefix.length - prefix.lastIndexOf("\n") - 1 };
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const loadQxSources = (root: string) => resolveQxSources((name) => readQxSource(root, name));
|
||||
Reference in New Issue
Block a user