You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
232 lines
5.7 KiB
232 lines
5.7 KiB
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(""); |
|
}
|
|
|