Files
zmk/docs/src/keymap-upgrade/parser.ts

193 lines
5.2 KiB
TypeScript

import Parser from "web-tree-sitter";
const TREE_SITTER_WASM_URL = new URL(
"/node_modules/web-tree-sitter/tree-sitter.wasm",
import.meta.url
);
const TREE_SITTER_DEVICETREE_WASM_URL = new URL(
"/node_modules/tree-sitter-devicetree/tree-sitter-devicetree.wasm",
import.meta.url
);
export let Devicetree: Parser.Language;
export async function initParser() {
await Parser.init({
locateFile: (path: string, prefix: string) => {
// When locating tree-sitter.wasm, use a path that Webpack can map to the correct URL.
if (path == "tree-sitter.wasm") {
return TREE_SITTER_WASM_URL.href;
}
return prefix + path;
},
});
Devicetree = await Parser.Language.load(TREE_SITTER_DEVICETREE_WASM_URL.href);
}
export function createParser() {
if (!Devicetree) {
throw new Error("Parser not loaded. Call initParser() first.");
}
const parser = new Parser();
parser.setLanguage(Devicetree);
return parser;
}
/**
* Returns the node for the named capture.
*/
export function findCapture(name: string, captures: Parser.QueryCapture[]) {
for (const c of captures) {
if (c.name === name) {
return c.node;
}
}
return null;
}
/**
* Returns whether the node for the named capture exists and has the given text.
*/
export function captureHasText(
name: string,
captures: Parser.QueryCapture[],
text: string
) {
const node = findCapture(name, captures);
return node?.text === text;
}
/**
* Get a list of SyntaxNodes representing a devicetree node with the given path.
* The same node may be listed multiple times within a file.
*
* This function does not evaluate which node a reference points to, so given
* a file containing "/ { foo: bar {}; }; &foo {};" searching for "&foo" will
* return the "&foo {}" node but not "foo: bar {}".
*
* @param path Path to the node to find. May be an absolute path such as
* "/foo/bar", a node reference such as "&foo", or a node reference followed by
* a relative path such as "&foo/bar".
*/
export function findDevicetreeNode(
tree: Parser.Tree,
path: string
): Parser.SyntaxNode[] {
const query = Devicetree.query("(node) @node");
const matches = query.matches(tree.rootNode);
const result: Parser.SyntaxNode[] = [];
for (const { captures } of matches) {
const node = findCapture("node", captures);
if (node && getDevicetreeNodePath(node) === path) {
result.push(node);
}
}
return result;
}
export interface FindPropertyOptions {
/** Search in children of the given node as well */
recursive?: boolean;
}
/**
* Find all instances of a devicetree property with the given name which are
* descendants of the given syntax node.
*
* @param node Any syntax node
*/
export function findDevicetreeProperty(
node: Parser.SyntaxNode,
name: string,
options: FindPropertyOptions & { recursive: true }
): Parser.SyntaxNode[];
/**
* Find a devicetree node's property with the given name, or null if it doesn't
* have one.
*
* @note If the node contains multiple instances of the same property, this
* returns the last once, since that is the one that will actually be applied.
*
* @param node A syntax node for a devicetree node
*/
export function findDevicetreeProperty(
node: Parser.SyntaxNode,
name: string,
options?: FindPropertyOptions
): Parser.SyntaxNode | null;
export function findDevicetreeProperty(
node: Parser.SyntaxNode,
name: string,
options?: FindPropertyOptions
): Parser.SyntaxNode[] | Parser.SyntaxNode | null {
const query = Devicetree.query(
`(property name: (identifier) @name (#eq? @name "${name}")) @prop`
);
const matches = query.matches(node);
const props = matches
.map(({ captures }) => findCapture("prop", captures))
.filter((node) => node !== null);
if (options?.recursive) {
return props;
}
// The query finds all descendants. Filter to just the properties that belong
// to the given devicetree node.
const childProps = props.filter((prop) =>
getContainingDevicetreeNode(prop)?.equals(node)
);
// Sort in descending order to select the last instance of the property.
childProps.sort((a, b) => b.startIndex - a.startIndex);
return childProps[0] ?? null;
}
export function getDevicetreeNodePath(node: Parser.SyntaxNode | null) {
const parts = getDevicetreeNodePathParts(node);
if (parts.length === 0) {
return "";
}
if (parts.length === 1) {
return parts[0];
}
const path = parts.join("/");
// The top-level node should be named "/", which is a special case since the
// path should not start with "//".
return parts[0] === "/" ? path.substring(1) : path;
}
function getDevicetreeNodePathParts(node: Parser.SyntaxNode | null): string[] {
// There may be intermediate syntax nodes between devicetree nodes, such as
// #if blocks, so if we aren't currently on a "node" node, traverse up the
// tree until we find one.
const dtnode = getContainingDevicetreeNode(node);
if (!dtnode) {
return [];
}
const name = dtnode.childForFieldName("name")?.text ?? "";
return [...getDevicetreeNodePathParts(dtnode.parent), name];
}
export function getContainingDevicetreeNode(node: Parser.SyntaxNode | null) {
while (node && node.type !== "node") {
node = node.parent;
}
return node;
}