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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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