Browse Source
Added a documentation page with a script that upgrades deprecated key codes and behaviors to their replacements. Fixes #299xmkb
Joel Spadin
4 years ago
committed by
innovaker
12 changed files with 562 additions and 4 deletions
@ -0,0 +1,21 @@ |
|||||||
|
--- |
||||||
|
title: Keymap Upgrader |
||||||
|
sidebar_label: Keymap Upgrader |
||||||
|
hide_title: true |
||||||
|
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. |
||||||
|
|
||||||
|
Hover your mouse over the upgraded keymap and click the `Copy` button 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). |
||||||
|
|
||||||
|
import KeymapUpgrader from "@site/src/components/KeymapUpgrader/index"; |
||||||
|
|
||||||
|
<KeymapUpgrader /> |
@ -0,0 +1,47 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2020 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: CC-BY-NC-SA-4.0 |
||||||
|
*/ |
||||||
|
|
||||||
|
import React from "react"; |
||||||
|
import { useAsync } from "react-async"; |
||||||
|
|
||||||
|
import { initParser, upgradeKeymap } from "@site/src/keymap-upgrade"; |
||||||
|
import CodeBlock from "@theme/CodeBlock"; |
||||||
|
|
||||||
|
import styles from "./styles.module.css"; |
||||||
|
|
||||||
|
export default function KeymapUpgrader() { |
||||||
|
const { error, isPending } = useAsync(initParser); |
||||||
|
|
||||||
|
if (isPending) { |
||||||
|
return <p>Loading...</p>; |
||||||
|
} |
||||||
|
|
||||||
|
if (error) { |
||||||
|
return <p className="error">Error: {error.message}</p>; |
||||||
|
} |
||||||
|
|
||||||
|
return <Editor />; |
||||||
|
} |
||||||
|
|
||||||
|
function Editor() { |
||||||
|
const [keymap, setKeymap] = React.useState(""); |
||||||
|
const upgraded = upgradeKeymap(keymap); |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<textarea |
||||||
|
className={styles.editor} |
||||||
|
placeholder="Paste keymap here" |
||||||
|
spellCheck={false} |
||||||
|
value={keymap} |
||||||
|
onChange={(e) => setKeymap(e.target.value)} |
||||||
|
></textarea> |
||||||
|
<div className={styles.result}> |
||||||
|
<CodeBlock metastring={'title="Upgraded Keymap"'}>{upgraded}</CodeBlock> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
} |
@ -0,0 +1,26 @@ |
|||||||
|
/* |
||||||
|
* Copyright (c) 2020 The ZMK Contributors |
||||||
|
* |
||||||
|
* SPDX-License-Identifier: CC-BY-NC-SA-4.0 |
||||||
|
*/ |
||||||
|
|
||||||
|
.editor { |
||||||
|
font-family: var(--ifm-font-family-monospace); |
||||||
|
font-size: var(--ifm-font-size-base); |
||||||
|
line-height: var(--ifm-pre-line-height); |
||||||
|
tab-size: 4; |
||||||
|
|
||||||
|
color: var(--ifm-pre-color); |
||||||
|
background-color: var(--ifm-pre-background); |
||||||
|
|
||||||
|
border: none; |
||||||
|
border-radius: var(--ifm-pre-border-radius); |
||||||
|
|
||||||
|
width: 100%; |
||||||
|
min-height: 10em; |
||||||
|
padding: var(--ifm-pre-padding); |
||||||
|
} |
||||||
|
|
||||||
|
.result { |
||||||
|
tab-size: 4; |
||||||
|
} |
@ -0,0 +1,84 @@ |
|||||||
|
/* |
||||||
|
* 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: "LSHFT", |
||||||
|
RCTL: "RCTRL", |
||||||
|
RSFT: "RSHFT", |
||||||
|
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: "LSHFT", |
||||||
|
MOD_LALT: "LALT", |
||||||
|
MOD_LGUI: "LGUI", |
||||||
|
MOD_RCTL: "RCTRL", |
||||||
|
MOD_RSFT: "RSHFT", |
||||||
|
MOD_RALT: "RALT", |
||||||
|
MOD_RGUI: "RGUI", |
||||||
|
}; |
||||||
|
|
||||||
|
export const Behaviors = { |
||||||
|
cp: "kp", |
||||||
|
inc_dec_cp: "inc_dec_kp", |
||||||
|
}; |
@ -0,0 +1,39 @@ |
|||||||
|
module.exports = function () { |
||||||
|
return { |
||||||
|
configureWebpack(config, isServer) { |
||||||
|
let rules = []; |
||||||
|
|
||||||
|
// Tree-sitter is only used for client-side code.
|
||||||
|
// Don't try to load it on the server.
|
||||||
|
if (isServer) { |
||||||
|
rules.push({ |
||||||
|
test: /web-tree-sitter/, |
||||||
|
loader: "null-loader", |
||||||
|
}); |
||||||
|
} else { |
||||||
|
// web-tree-sitter has a hard-coded path to tree-sitter.wasm,
|
||||||
|
// (see https://github.com/tree-sitter/tree-sitter/issues/559)
|
||||||
|
// which some browsers treat as absolute and others as relative.
|
||||||
|
// This breaks everything. Rewrite it to always use an absolute path.
|
||||||
|
rules.push({ |
||||||
|
test: /tree-sitter\.js$/, |
||||||
|
loader: "string-replace-loader", |
||||||
|
options: { |
||||||
|
search: '"tree-sitter.wasm"', |
||||||
|
replace: '"/tree-sitter.wasm"', |
||||||
|
strict: true, |
||||||
|
}, |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
return { |
||||||
|
// web-tree-sitter tries to import "fs", which can be ignored.
|
||||||
|
// https://github.com/tree-sitter/tree-sitter/issues/466
|
||||||
|
node: { |
||||||
|
fs: "empty", |
||||||
|
}, |
||||||
|
module: { rules }, |
||||||
|
}; |
||||||
|
}, |
||||||
|
}; |
||||||
|
}; |
@ -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(""); |
||||||
|
} |
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue