diff options
Diffstat (limited to 'docs/src/keymap-upgrade.js')
-rw-r--r-- | docs/src/keymap-upgrade.js | 232 |
1 files changed, 232 insertions, 0 deletions
diff --git a/docs/src/keymap-upgrade.js b/docs/src/keymap-upgrade.js new file mode 100644 index 0000000..19a5d8e --- /dev/null +++ b/docs/src/keymap-upgrade.js @@ -0,0 +1,232 @@ +import Parser from "web-tree-sitter"; + +import { Codes, Behaviors } from "./data/keymap-upgrade"; + +let Devicetree; + +export async function initParser() { + await Parser.init(); + Devicetree = await Parser.Language.load("/tree-sitter-devicetree.wasm"); +} + +function createParser() { + if (!Devicetree) { + throw new Error("Parser not loaded. Call initParser() first."); + } + + const parser = new Parser(); + parser.setLanguage(Devicetree); + return parser; +} + +export function upgradeKeymap(text) { + const parser = createParser(); + const tree = parser.parse(text); + + const edits = [...upgradeBehaviors(tree), ...upgradeKeycodes(tree)]; + + return applyEdits(text, edits); +} + +class TextEdit { + /** + * Creates a text edit to replace a range or node with new text. + * Construct with one of: + * + * * `Edit(startIndex, endIndex, newText)` + * * `Edit(node, newText)` + */ + constructor(startIndex, endIndex, newText) { + if (typeof startIndex !== "number") { + const node = startIndex; + newText = endIndex; + startIndex = node.startIndex; + endIndex = node.endIndex; + } + + /** @type number */ + this.startIndex = startIndex; + /** @type number */ + this.endIndex = endIndex; + /** @type string */ + this.newText = newText; + } +} + +/** + * Upgrades deprecated behavior references. + * @param {Parser.Tree} tree + */ +function upgradeBehaviors(tree) { + /** @type TextEdit[] */ + let edits = []; + + const query = Devicetree.query("(reference label: (identifier) @ref)"); + const matches = query.matches(tree.rootNode); + + for (const { captures } of matches) { + const node = findCapture("ref", captures); + if (node) { + edits.push(...getUpgradeEdits(node, Behaviors)); + } + } + + return edits; +} + +/** + * Upgrades deprecated key code identifiers. + * @param {Parser.Tree} tree + */ +function upgradeKeycodes(tree) { + /** @type TextEdit[] */ + let edits = []; + + // No need to filter to the bindings array. The C preprocessor would have + // replaced identifiers anywhere, so upgrading all identifiers preserves the + // original behavior of the keymap (even if that behavior wasn't intended). + const query = Devicetree.query("(identifier) @name"); + const matches = query.matches(tree.rootNode); + + for (const { captures } of matches) { + const node = findCapture("name", captures); + if (node) { + edits.push(...getUpgradeEdits(node, Codes, keycodeReplaceHandler)); + } + } + + return edits; +} + +/** + * @param {Parser.SyntaxNode} node + * @param {string | null} replacement + * @returns TextEdit[] + */ +function keycodeReplaceHandler(node, replacement) { + if (replacement) { + return [new TextEdit(node, replacement)]; + } + + const nodes = findBehaviorNodes(node); + + if (nodes.length === 0) { + console.warn( + `Found deprecated code "${node.text}" but it is not a parameter to a behavior` + ); + return [new TextEdit(node, `/* "${node.text}" no longer exists */`)]; + } + + const oldText = nodes.map((n) => n.text).join(" "); + const newText = `&none /* "${oldText}" no longer exists */`; + + const startIndex = nodes[0].startIndex; + const endIndex = nodes[nodes.length - 1].endIndex; + + return [new TextEdit(startIndex, endIndex, newText)]; +} + +/** + * Returns the node for the named capture. + * @param {string} name + * @param {any[]} captures + * @returns {Parser.SyntaxNode | null} + */ +function findCapture(name, captures) { + for (const c of captures) { + if (c.name === name) { + return c.node; + } + } + + return null; +} + +/** + * Given a parameter to a keymap behavior, returns a list of nodes beginning + * with the behavior and including all parameters. + * Returns an empty array if no behavior was found. + * @param {Parser.SyntaxNode} paramNode + */ +function findBehaviorNodes(paramNode) { + // Walk backwards from the given parameter to find the behavior reference. + let behavior = paramNode.previousNamedSibling; + while (behavior && behavior.type !== "reference") { + behavior = behavior.previousNamedSibling; + } + + if (!behavior) { + return []; + } + + // Walk forward from the behavior to collect all its parameters. + + let nodes = [behavior]; + let param = behavior.nextNamedSibling; + while (param && param.type !== "reference") { + nodes.push(param); + param = param.nextNamedSibling; + } + + return nodes; +} + +/** + * Gets a list of text edits to apply based on a node and a map of text + * replacements. + * + * If replaceHandler is given, it will be called if the node matches a + * deprecated value and it should return the text edits to apply. + * + * @param {Parser.SyntaxNode} node + * @param {Map<string, string | null>} replacementMap + * @param {(node: Parser.SyntaxNode, replacement: string | null) => TextEdit[]} replaceHandler + */ +function getUpgradeEdits(node, replacementMap, replaceHandler = undefined) { + for (const [deprecated, replacement] of Object.entries(replacementMap)) { + if (node.text === deprecated) { + if (replaceHandler) { + return replaceHandler(node, replacement); + } else { + return [new TextEdit(node, replacement)]; + } + } + } + return []; +} + +/** + * Sorts a list of text edits in ascending order by position. + * @param {TextEdit[]} edits + */ +function sortEdits(edits) { + return edits.sort((a, b) => a.startIndex - b.startIndex); +} + +/** + * Returns a string with text replacements applied. + * @param {string} text + * @param {TextEdit[]} edits + */ +function applyEdits(text, edits) { + edits = sortEdits(edits); + + /** @type string[] */ + const chunks = []; + let currentIndex = 0; + + for (const edit of edits) { + if (edit.startIndex < currentIndex) { + console.warn("discarding overlapping edit", edit); + continue; + } + + chunks.push(text.substring(currentIndex, edit.startIndex)); + chunks.push(edit.newText); + currentIndex = edit.endIndex; + } + + chunks.push(text.substring(currentIndex)); + + return chunks.join(""); +} |