From a0465391beb69ff3ccd000a9696e80d73362f5f4 Mon Sep 17 00:00:00 2001 From: Joel Spadin Date: Sun, 21 Jan 2024 17:15:58 -0600 Subject: [PATCH] refactor: Improve keymap upgrader Moved the keymap upgrader to a top-level page like the power profiler to make it more discoverable. It upgrades more things than key codes now, so putting it in the codes category doesn't make much sense. Converted the upgrader code to TypeScript and split it up into smaller files to make it easier to add new upgrade functions. Added upgrade functions to remove/replace "label" properties and rename matrix-transform.h to matrix_transform.h. --- docs/docusaurus.config.js | 5 + docs/sidebars.js | 1 - docs/src/components/KeymapUpgrader/index.jsx | 4 +- docs/src/data/keymap-upgrade.js | 85 ------ docs/src/keymap-upgrade.js | 245 ------------------ docs/src/keymap-upgrade/behaviors.ts | 27 ++ docs/src/keymap-upgrade/headers.ts | 40 +++ docs/src/keymap-upgrade/index.ts | 25 ++ docs/src/keymap-upgrade/keycodes.ts | 150 +++++++++++ docs/src/keymap-upgrade/parser.ts | 56 ++++ docs/src/keymap-upgrade/properties.ts | 65 +++++ docs/src/keymap-upgrade/textedit.ts | 153 +++++++++++ .../codes => src/pages}/keymap-upgrader.mdx | 7 +- docs/tsconfig.json | 2 +- 14 files changed, 528 insertions(+), 337 deletions(-) delete mode 100644 docs/src/data/keymap-upgrade.js delete mode 100644 docs/src/keymap-upgrade.js create mode 100644 docs/src/keymap-upgrade/behaviors.ts create mode 100644 docs/src/keymap-upgrade/headers.ts create mode 100644 docs/src/keymap-upgrade/index.ts create mode 100644 docs/src/keymap-upgrade/keycodes.ts create mode 100644 docs/src/keymap-upgrade/parser.ts create mode 100644 docs/src/keymap-upgrade/properties.ts create mode 100644 docs/src/keymap-upgrade/textedit.ts rename docs/{docs/codes => src/pages}/keymap-upgrader.mdx (52%) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 34e9e92f..c9a07179 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -55,6 +55,11 @@ module.exports = { label: "Power Profiler", position: "left", }, + { + to: "keymap-upgrader", + label: "Keymap Upgrader", + position: "left", + }, { href: "https://github.com/zmkfirmware/zmk", label: "GitHub", diff --git a/docs/sidebars.js b/docs/sidebars.js index 19b0ad7e..284eb09b 100644 --- a/docs/sidebars.js +++ b/docs/sidebars.js @@ -53,7 +53,6 @@ module.exports = { "codes/applications", "codes/input-assist", "codes/power", - "codes/keymap-upgrader", ], Configuration: [ "config/index", diff --git a/docs/src/components/KeymapUpgrader/index.jsx b/docs/src/components/KeymapUpgrader/index.jsx index 8d3a60b2..90429d83 100644 --- a/docs/src/components/KeymapUpgrader/index.jsx +++ b/docs/src/components/KeymapUpgrader/index.jsx @@ -40,7 +40,9 @@ function Editor() { onChange={(e) => setKeymap(e.target.value)} >
- {upgraded} + + {upgraded} +
); diff --git a/docs/src/data/keymap-upgrade.js b/docs/src/data/keymap-upgrade.js deleted file mode 100644 index 8e153828..00000000 --- a/docs/src/data/keymap-upgrade.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2020 The ZMK Contributors - * - * SPDX-License-Identifier: CC-BY-NC-SA-4.0 - */ - -export const Codes = { - NUM_1: "N1", - NUM_2: "N2", - NUM_3: "N3", - NUM_4: "N4", - NUM_5: "N5", - NUM_6: "N6", - NUM_7: "N7", - NUM_8: "N8", - NUM_9: "N9", - NUM_0: "N0", - BKSP: "BSPC", - SPC: "SPACE", - EQL: "EQUAL", - TILD: "TILDE", - SCLN: "SEMI", - QUOT: "SQT", - GRAV: "GRAVE", - CMMA: "COMMA", - PRSC: "PSCRN", - SCLK: "SLCK", - PAUS: "PAUSE_BREAK", - PGUP: "PG_UP", - PGDN: "PG_DN", - RARW: "RIGHT", - LARW: "LEFT", - DARW: "DOWN", - UARW: "UP", - KDIV: "KP_DIVIDE", - KMLT: "KP_MULTIPLY", - KMIN: "KP_MINUS", - KPLS: "KP_PLUS", - UNDO: "K_UNDO", - CUT: "K_CUT", - COPY: "K_COPY", - PSTE: "K_PASTE", - VOLU: "K_VOL_UP", - VOLD: "K_VOL_DN", - CURU: "DLLR", - LPRN: "LPAR", - RPRN: "RPAR", - LCUR: "LBRC", - RCUR: "RBRC", - CRRT: "CARET", - PRCT: "PRCNT", - LABT: "LT", - RABT: "GT", - COLN: "COLON", - KSPC: null, - ATSN: "AT", - BANG: "EXCL", - LCTL: "LCTRL", - LSFT: "LSHIFT", - RCTL: "RCTRL", - RSFT: "RSHIFT", - M_NEXT: "C_NEXT", - M_PREV: "C_PREV", - M_STOP: "C_STOP", - M_EJCT: "C_EJECT", - M_PLAY: "C_PP", - M_MUTE: "C_MUTE", - M_VOLU: "C_VOL_UP", - M_VOLD: "C_VOL_DN", - GUI: "K_CMENU", - MOD_LCTL: "LCTRL", - MOD_LSFT: "LSHIFT", - MOD_LALT: "LALT", - MOD_LGUI: "LGUI", - MOD_RCTL: "RCTRL", - MOD_RSFT: "RSHIFT", - MOD_RALT: "RALT", - MOD_RGUI: "RGUI", -}; - -export const Behaviors = { - cp: "kp", - inc_dec_cp: "inc_dec_kp", - reset: "sys_reset", -}; diff --git a/docs/src/keymap-upgrade.js b/docs/src/keymap-upgrade.js deleted file mode 100644 index 788ab31a..00000000 --- a/docs/src/keymap-upgrade.js +++ /dev/null @@ -1,245 +0,0 @@ -import Parser from "web-tree-sitter"; - -import { Codes, Behaviors } from "./data/keymap-upgrade"; - -const TREE_SITTER_WASM_URL = new URL( - "/node_modules/web-tree-sitter/tree-sitter.wasm", - import.meta.url -); - -let Devicetree; - -export async function initParser() { - await Parser.init({ - locateFile: (path, prefix) => { - // 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"); -} - -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} 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(""); -} diff --git a/docs/src/keymap-upgrade/behaviors.ts b/docs/src/keymap-upgrade/behaviors.ts new file mode 100644 index 00000000..37c865e8 --- /dev/null +++ b/docs/src/keymap-upgrade/behaviors.ts @@ -0,0 +1,27 @@ +import type { Tree } from "web-tree-sitter"; + +import { Devicetree, findCapture } from "./parser"; +import { TextEdit, getUpgradeEdits } from "./textedit"; + +// Map of { "deprecated": "replacement" } behavior names (not including "&" prefixes). +const BEHAVIORS = { + cp: "kp", + inc_dec_cp: "inc_dec_kp", + reset: "sys_reset", +}; + +export function upgradeBehaviors(tree: Tree) { + const edits: TextEdit[] = []; + + 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; +} diff --git a/docs/src/keymap-upgrade/headers.ts b/docs/src/keymap-upgrade/headers.ts new file mode 100644 index 00000000..8aa1928f --- /dev/null +++ b/docs/src/keymap-upgrade/headers.ts @@ -0,0 +1,40 @@ +import type { SyntaxNode, Tree } from "web-tree-sitter"; +import { Devicetree, findCapture } from "./parser"; +import { getUpgradeEdits, MatchFunc, ReplaceFunc, TextEdit } from "./textedit"; + +// Map of { "deprecated": "replacement" } header paths. +const HEADERS = { + "dt-bindings/zmk/matrix-transform.h": "dt-bindings/zmk/matrix_transform.h", +}; + +export function upgradeHeaders(tree: Tree) { + const edits: TextEdit[] = []; + + const query = Devicetree.query( + "(preproc_include path: [(string_literal) (system_lib_string)] @path)" + ); + const matches = query.matches(tree.rootNode); + + for (const { captures } of matches) { + const node = findCapture("path", captures); + if (node) { + edits.push( + ...getUpgradeEdits(node, HEADERS, headerReplaceHandler, isHeaderMatch) + ); + } + } + + return edits; +} + +const isHeaderMatch: MatchFunc = (node, text) => { + return node.text === `"${text}"` || node.text === `<${text}>`; +}; + +const headerReplaceHandler: ReplaceFunc = (node, replacement) => { + if (!replacement) { + throw new Error("Header replacement does not support removing headers"); + } + + return [new TextEdit(node.startIndex + 1, node.endIndex - 1, replacement)]; +}; diff --git a/docs/src/keymap-upgrade/index.ts b/docs/src/keymap-upgrade/index.ts new file mode 100644 index 00000000..4d091e23 --- /dev/null +++ b/docs/src/keymap-upgrade/index.ts @@ -0,0 +1,25 @@ +import { createParser } from "./parser"; +import { applyEdits } from "./textedit"; + +import { upgradeBehaviors } from "./behaviors"; +import { upgradeHeaders } from "./headers"; +import { upgradeKeycodes } from "./keycodes"; +import { upgradeProperties } from "./properties"; + +export { initParser } from "./parser"; + +const upgradeFunctions = [ + upgradeBehaviors, + upgradeHeaders, + upgradeKeycodes, + upgradeProperties, +]; + +export function upgradeKeymap(text: string) { + const parser = createParser(); + const tree = parser.parse(text); + + const edits = upgradeFunctions.map((f) => f(tree)).flat(); + + return applyEdits(text, edits); +} diff --git a/docs/src/keymap-upgrade/keycodes.ts b/docs/src/keymap-upgrade/keycodes.ts new file mode 100644 index 00000000..9a9ede66 --- /dev/null +++ b/docs/src/keymap-upgrade/keycodes.ts @@ -0,0 +1,150 @@ +import type { SyntaxNode, Tree } from "web-tree-sitter"; +import { Devicetree, findCapture } from "./parser"; +import { getUpgradeEdits, TextEdit } from "./textedit"; + +// Map of { "DEPRECATED": "REPLACEMENT" } key codes. +const CODES = { + NUM_1: "N1", + NUM_2: "N2", + NUM_3: "N3", + NUM_4: "N4", + NUM_5: "N5", + NUM_6: "N6", + NUM_7: "N7", + NUM_8: "N8", + NUM_9: "N9", + NUM_0: "N0", + BKSP: "BSPC", + SPC: "SPACE", + EQL: "EQUAL", + TILD: "TILDE", + SCLN: "SEMI", + QUOT: "SQT", + GRAV: "GRAVE", + CMMA: "COMMA", + PRSC: "PSCRN", + SCLK: "SLCK", + PAUS: "PAUSE_BREAK", + PGUP: "PG_UP", + PGDN: "PG_DN", + RARW: "RIGHT", + LARW: "LEFT", + DARW: "DOWN", + UARW: "UP", + KDIV: "KP_DIVIDE", + KMLT: "KP_MULTIPLY", + KMIN: "KP_MINUS", + KPLS: "KP_PLUS", + UNDO: "K_UNDO", + CUT: "K_CUT", + COPY: "K_COPY", + PSTE: "K_PASTE", + VOLU: "K_VOL_UP", + VOLD: "K_VOL_DN", + CURU: "DLLR", + LPRN: "LPAR", + RPRN: "RPAR", + LCUR: "LBRC", + RCUR: "RBRC", + CRRT: "CARET", + PRCT: "PRCNT", + LABT: "LT", + RABT: "GT", + COLN: "COLON", + KSPC: null, + ATSN: "AT", + BANG: "EXCL", + LCTL: "LCTRL", + LSFT: "LSHIFT", + RCTL: "RCTRL", + RSFT: "RSHIFT", + M_NEXT: "C_NEXT", + M_PREV: "C_PREV", + M_STOP: "C_STOP", + M_EJCT: "C_EJECT", + M_PLAY: "C_PP", + M_MUTE: "C_MUTE", + M_VOLU: "C_VOL_UP", + M_VOLD: "C_VOL_DN", + GUI: "K_CMENU", + MOD_LCTL: "LCTRL", + MOD_LSFT: "LSHIFT", + MOD_LALT: "LALT", + MOD_LGUI: "LGUI", + MOD_RCTL: "RCTRL", + MOD_RSFT: "RSHIFT", + MOD_RALT: "RALT", + MOD_RGUI: "RGUI", +}; + +/** + * Upgrades deprecated key code identifiers. + */ +export function upgradeKeycodes(tree: Tree) { + const edits: TextEdit[] = []; + + // 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; +} + +function keycodeReplaceHandler(node: SyntaxNode, replacement: string | null) { + 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)]; +} + +/** + * 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. + */ +function findBehaviorNodes(paramNode: SyntaxNode) { + // 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; +} diff --git a/docs/src/keymap-upgrade/parser.ts b/docs/src/keymap-upgrade/parser.ts new file mode 100644 index 00000000..14ed5f82 --- /dev/null +++ b/docs/src/keymap-upgrade/parser.ts @@ -0,0 +1,56 @@ +import Parser from "web-tree-sitter"; + +const TREE_SITTER_WASM_URL = new URL( + "/node_modules/web-tree-sitter/tree-sitter.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"); +} + +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; +} diff --git a/docs/src/keymap-upgrade/properties.ts b/docs/src/keymap-upgrade/properties.ts new file mode 100644 index 00000000..7edc555a --- /dev/null +++ b/docs/src/keymap-upgrade/properties.ts @@ -0,0 +1,65 @@ +import type { SyntaxNode, Tree } from "web-tree-sitter"; +import { captureHasText, Devicetree, findCapture } from "./parser"; +import { TextEdit } from "./textedit"; + +/** + * Upgrades deprecated properties. + */ +export function upgradeProperties(tree: Tree) { + return removeLabels(tree); +} + +/** + * Renames "label" properties in keymap layers to "display-name". Removes all + * other "label" properties. + */ +function removeLabels(tree: Tree) { + const edits: TextEdit[] = []; + + const query = Devicetree.query("(property name: (identifier) @name) @prop"); + const matches = query.matches(tree.rootNode); + + for (const { captures } of matches) { + const name = findCapture("name", captures); + const node = findCapture("prop", captures); + if (name?.text === "label" && node) { + if (isLayerLabel(node)) { + edits.push(new TextEdit(name, "display-name")); + } else { + edits.push(new TextEdit(node, "")); + } + } + } + + return edits; +} + +/** + * Given a "label" property node, returns whether it is a label for a keymap layer. + */ +function isLayerLabel(node: SyntaxNode) { + const maybeKeymap = node.parent?.parent; + if (!maybeKeymap) { + return false; + } + + const query = Devicetree.query( + `(property + name: (identifier) @name + value: (string_literal) @value + ) @prop` + ); + const matches = query.matches(maybeKeymap); + + for (const { captures } of matches) { + if ( + findCapture("prop", captures)?.parent?.equals(maybeKeymap) && + captureHasText("name", captures, "compatible") && + captureHasText("value", captures, '"zmk,keymap"') + ) { + return true; + } + } + + return false; +} diff --git a/docs/src/keymap-upgrade/textedit.ts b/docs/src/keymap-upgrade/textedit.ts new file mode 100644 index 00000000..a791c1a6 --- /dev/null +++ b/docs/src/keymap-upgrade/textedit.ts @@ -0,0 +1,153 @@ +import type { SyntaxNode } from "web-tree-sitter"; + +export class TextEdit { + startIndex: number; + endIndex: number; + newText: string; + + /** + * Creates a text edit to replace a range with new text. + */ + constructor(startIndex: number, endIndex: number, newText: string); + /** + * Creates a text edit to replace a node with new text. + */ + constructor(node: SyntaxNode, newText: string); + constructor( + startIndex: number | SyntaxNode, + endIndex: number | string, + newText?: string + ) { + if (typeof startIndex !== "number") { + if (typeof endIndex === "string") { + const node = startIndex; + newText = endIndex; + startIndex = node.startIndex; + endIndex = node.endIndex; + } else { + throw new TypeError(); + } + } else if (typeof endIndex !== "number" || typeof newText !== "string") { + throw new TypeError(); + } + + this.startIndex = startIndex; + this.endIndex = endIndex; + this.newText = newText; + } +} + +export type MatchFunc = (node: SyntaxNode, text: string) => boolean; +export type ReplaceFunc = ( + node: SyntaxNode, + replacement: string | null +) => TextEdit[]; + +/** + * 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. + * Otherwise, the full node is replaced by the new text. + * + * If isMatch is given, it will be called to check if a node matches a + * deprecated value. Otherwise, the node's text is matched against the + * deprecated text. + * + * @param {SyntaxNode} node + * @param {Record} replacementMap + * @param {ReplaceFunc} [replaceHandler] + * @param {MatchFunc} [isMatch] + */ +export function getUpgradeEdits( + node: SyntaxNode, + replacementMap: Record, + replaceHandler?: ReplaceFunc, + isMatch?: MatchFunc +) { + const defaultReplace: ReplaceFunc = (node, replacement) => [ + new TextEdit(node, replacement ?? ""), + ]; + const defaultMatch: MatchFunc = (node, text) => node.text === text; + + replaceHandler = replaceHandler ?? defaultReplace; + isMatch = isMatch ?? defaultMatch; + + for (const [deprecated, replacement] of Object.entries(replacementMap)) { + if (isMatch(node, deprecated)) { + return replaceHandler(node, replacement); + } + } + return []; +} + +/** + * Sorts a list of text edits in ascending order by position. + */ +function sortEdits(edits: TextEdit[]) { + return edits.sort((a, b) => a.startIndex - b.startIndex); +} + +/** + * Returns a string with text replacements applied. + */ +export function applyEdits(text: string, edits: TextEdit[]) { + // If we are removing text and it's the only thing on a line, remove the whole line. + edits = edits.map((e) => (e.newText ? e : expandEditToLine(text, e))); + + edits = sortEdits(edits); + + const chunks: string[] = []; + let currentIndex = 0; + + for (let 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(""); +} + +/** + * If the given edit is surrounded by only whitespace on a line, expands it to + * replace the entire line, else returns it unmodified. + */ +function expandEditToLine(text: string, edit: TextEdit) { + // Expand the selection to adjacent whitespace + let newStart = edit.startIndex; + let newEnd = edit.endIndex; + + while (newStart > 0 && text[newStart - 1].match(/[ \t]/)) { + newStart--; + } + + while (newEnd < text.length && text[newEnd].match(/[ \t]/)) { + newEnd++; + } + + // Check that we selected the entire line + if ( + (newEnd !== text.length && text[newEnd] !== "\n") || + (newStart > 0 && text[newStart - 1] !== "\n") + ) { + return edit; + } + + // Select one of the line breaks to remove. + if (newEnd !== text.length) { + newEnd++; + } else if (newStart !== 0) { + newStart--; + } + + return new TextEdit(newStart, newEnd, edit.newText); +} diff --git a/docs/docs/codes/keymap-upgrader.mdx b/docs/src/pages/keymap-upgrader.mdx similarity index 52% rename from docs/docs/codes/keymap-upgrader.mdx rename to docs/src/pages/keymap-upgrader.mdx index bcee82b5..5aafc8be 100644 --- a/docs/docs/codes/keymap-upgrader.mdx +++ b/docs/src/pages/keymap-upgrader.mdx @@ -7,14 +7,13 @@ hide_table_of_contents: true # Keymap Upgrader -Many codes have been renamed to be more consistent with each other. -Paste the contents of a `.keymap` file below to upgrade all deprecated codes to their replacements. +Some behaviors, key codes, and other features have been renamed to be more consistent with each other. This tool will upgrade most deprecated features to their replacements. -Hover your mouse over the upgraded keymap and click the `Copy` button to copy it to your clipboard. +Paste the contents of a `.keymap` file below. Then, hover your mouse over the upgraded keymap and click the `Copy` button in the upper-right corner to copy it to your clipboard. You will likely need to realign columns in the upgraded keymap. The upgrader also does not handle codes inside a `#define`, so you will need to update those manually using -[this list of deprecated codes and replacements](https://github.com/zmkfirmware/zmk/blob/main/docs/src/data/keymap-upgrade.js). +[this list of deprecated codes and replacements](https://github.com/zmkfirmware/zmk/blob/main/docs/src/keymap-upgrade/keycodes.ts). import KeymapUpgrader from "@site/src/components/KeymapUpgrader/index"; diff --git a/docs/tsconfig.json b/docs/tsconfig.json index 86f17b07..f73bc2d9 100644 --- a/docs/tsconfig.json +++ b/docs/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@docusaurus/tsconfig/tsconfig.json", + "extends": "@docusaurus/tsconfig", "include": ["src/"], "compilerOptions": { "types": ["node", "@docusaurus/theme-classic"],