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"],