feat(keymap-upgrader): Upgrade encoder resolution
Added an upgrade function to the keymap upgrader to replace the encoder "resolution" property with "steps" and (if it is not already present) "triggers-per-rotation".
This commit is contained in:
parent
be75da096c
commit
3a4cf185a1
3 changed files with 171 additions and 6 deletions
101
docs/src/keymap-upgrade/encoder.ts
Normal file
101
docs/src/keymap-upgrade/encoder.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import type { SyntaxNode, Tree } from "web-tree-sitter";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getContainingDevicetreeNode,
|
||||||
|
getDevicetreeNodePath,
|
||||||
|
findDevicetreeProperty,
|
||||||
|
} from "./parser";
|
||||||
|
import { TextEdit } from "./textedit";
|
||||||
|
|
||||||
|
const ALPS_EC11_COMPATIBLE = '"alps,ec11"';
|
||||||
|
const DEFAULT_RESOLUTION = 4;
|
||||||
|
const TRIGGERS_PER_ROTATION = 20;
|
||||||
|
const TRIGGERS_PER_ROTATION_DT = `
|
||||||
|
|
||||||
|
&sensors {
|
||||||
|
// Change this to your encoder's number of detents.
|
||||||
|
// If you have multiple encoders with different detents, see
|
||||||
|
// https://zmk.dev/docs/config/encoders#keymap-sensor-config
|
||||||
|
triggers-per-rotation = <${TRIGGERS_PER_ROTATION}>;
|
||||||
|
};`;
|
||||||
|
|
||||||
|
export function upgradeEncoderResolution(tree: Tree) {
|
||||||
|
const edits: TextEdit[] = [];
|
||||||
|
|
||||||
|
const resolutionProps = findEncoderResolution(tree);
|
||||||
|
edits.push(...resolutionProps.flatMap(upgradeResolutionProperty));
|
||||||
|
|
||||||
|
if (resolutionProps.length > 0) {
|
||||||
|
edits.push(...addTriggersPerRotation(tree));
|
||||||
|
}
|
||||||
|
|
||||||
|
return edits;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findEncoderResolution(tree: Tree): SyntaxNode[] {
|
||||||
|
const props = findDevicetreeProperty(tree.rootNode, "resolution", {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return props.filter((prop) => {
|
||||||
|
const node = getContainingDevicetreeNode(prop);
|
||||||
|
return node && isEncoderNode(node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEncoderNode(node: SyntaxNode) {
|
||||||
|
// If a compatible property is set, then we know for sure if this is an encoder.
|
||||||
|
const compatible = findDevicetreeProperty(node, "compatible");
|
||||||
|
if (compatible) {
|
||||||
|
return compatible.childForFieldName("value")?.text === ALPS_EC11_COMPATIBLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compatible properties rarely appear in a keymap though, so just guess based
|
||||||
|
// on the node path/reference otherwise.
|
||||||
|
return getDevicetreeNodePath(node).toLowerCase().includes("encoder");
|
||||||
|
}
|
||||||
|
|
||||||
|
function upgradeResolutionProperty(prop: SyntaxNode): TextEdit[] {
|
||||||
|
const name = prop.childForFieldName("name");
|
||||||
|
const value = prop.childForFieldName("value");
|
||||||
|
|
||||||
|
if (!name || !value) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to set the new steps to be triggers-per-rotation * resolution, but fall
|
||||||
|
// back to a default if the value is something more complex than a single int.
|
||||||
|
const resolution = value.text.trim().replaceAll(/^<|>$/g, "");
|
||||||
|
const steps =
|
||||||
|
(parseInt(resolution) || DEFAULT_RESOLUTION) * TRIGGERS_PER_ROTATION;
|
||||||
|
|
||||||
|
const hint = `/* Change this to your encoder's number of detents times ${resolution} */`;
|
||||||
|
|
||||||
|
return [
|
||||||
|
TextEdit.fromNode(name, "steps"),
|
||||||
|
TextEdit.fromNode(value, `<${steps}> ${hint}`),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
function addTriggersPerRotation(tree: Tree): TextEdit[] {
|
||||||
|
// The keymap might already contain "triggers-per-rotation" for example if the
|
||||||
|
// user already upgraded some but not all "resolution" properties. Don't add
|
||||||
|
// another one if it already exists.
|
||||||
|
if (keymapHasTriggersPerRotation(tree)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inserting a new property into an existing node while keeping the code
|
||||||
|
// readable in all cases is hard, so just append a new &sensors node to the
|
||||||
|
// end of the keymap.
|
||||||
|
const end = tree.rootNode.endIndex;
|
||||||
|
return [new TextEdit(end, end, TRIGGERS_PER_ROTATION_DT)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function keymapHasTriggersPerRotation(tree: Tree) {
|
||||||
|
const props = findDevicetreeProperty(tree.rootNode, "triggers-per-rotation", {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
return props.length > 0;
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ import { createParser } from "./parser";
|
||||||
import { applyEdits, Range } from "./textedit";
|
import { applyEdits, Range } from "./textedit";
|
||||||
|
|
||||||
import { upgradeBehaviors } from "./behaviors";
|
import { upgradeBehaviors } from "./behaviors";
|
||||||
|
import { upgradeEncoderResolution } from "./encoder";
|
||||||
import { upgradeHeaders } from "./headers";
|
import { upgradeHeaders } from "./headers";
|
||||||
import { upgradeKeycodes } from "./keycodes";
|
import { upgradeKeycodes } from "./keycodes";
|
||||||
import { upgradeNodeNames } from "./nodes";
|
import { upgradeNodeNames } from "./nodes";
|
||||||
|
@ -11,6 +12,7 @@ export { initParser } from "./parser";
|
||||||
|
|
||||||
const upgradeFunctions = [
|
const upgradeFunctions = [
|
||||||
upgradeBehaviors,
|
upgradeBehaviors,
|
||||||
|
upgradeEncoderResolution,
|
||||||
upgradeHeaders,
|
upgradeHeaders,
|
||||||
upgradeKeycodes,
|
upgradeKeycodes,
|
||||||
upgradeNodeNames,
|
upgradeNodeNames,
|
||||||
|
|
|
@ -57,9 +57,15 @@ export function captureHasText(
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a list of SyntaxNodes representing a devicetree node with the given path.
|
* Get a list of SyntaxNodes representing a devicetree node with the given path.
|
||||||
* (The same node may be listed multiple times within a file.)
|
* The same node may be listed multiple times within a file.
|
||||||
*
|
*
|
||||||
* @param path Absolute path to the node (must start with "/")
|
* This function does not evaluate which node a reference points to, so given
|
||||||
|
* a file containing "/ { foo: bar {}; }; &foo {};" searching for "&foo" will
|
||||||
|
* return the "&foo {}" node but not "foo: bar {}".
|
||||||
|
*
|
||||||
|
* @param path Path to the node to find. May be an absolute path such as
|
||||||
|
* "/foo/bar", a node reference such as "&foo", or a node reference followed by
|
||||||
|
* a relative path such as "&foo/bar".
|
||||||
*/
|
*/
|
||||||
export function findDevicetreeNode(
|
export function findDevicetreeNode(
|
||||||
tree: Parser.Tree,
|
tree: Parser.Tree,
|
||||||
|
@ -81,6 +87,64 @@ export function findDevicetreeNode(
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface FindPropertyOptions {
|
||||||
|
/** Search in children of the given node as well */
|
||||||
|
recursive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find all instances of a devicetree property with the given name which are
|
||||||
|
* descendants of the given syntax node.
|
||||||
|
*
|
||||||
|
* @param node Any syntax node
|
||||||
|
*/
|
||||||
|
export function findDevicetreeProperty(
|
||||||
|
node: Parser.SyntaxNode,
|
||||||
|
name: string,
|
||||||
|
options: FindPropertyOptions & { recursive: true }
|
||||||
|
): Parser.SyntaxNode[];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find a devicetree node's property with the given name, or null if it doesn't
|
||||||
|
* have one.
|
||||||
|
*
|
||||||
|
* @note If the node contains multiple instances of the same property, this
|
||||||
|
* returns the last once, since that is the one that will actually be applied.
|
||||||
|
*
|
||||||
|
* @param node A syntax node for a devicetree node
|
||||||
|
*/
|
||||||
|
export function findDevicetreeProperty(
|
||||||
|
node: Parser.SyntaxNode,
|
||||||
|
name: string,
|
||||||
|
options?: FindPropertyOptions
|
||||||
|
): Parser.SyntaxNode | null;
|
||||||
|
|
||||||
|
export function findDevicetreeProperty(
|
||||||
|
node: Parser.SyntaxNode,
|
||||||
|
name: string,
|
||||||
|
options?: FindPropertyOptions
|
||||||
|
): Parser.SyntaxNode[] | Parser.SyntaxNode | null {
|
||||||
|
const query = Devicetree.query(
|
||||||
|
`(property name: (identifier) @name (#eq? @name "${name}")) @prop`
|
||||||
|
);
|
||||||
|
const matches = query.matches(node);
|
||||||
|
const props = matches.map(({ captures }) => findCapture("prop", captures)!);
|
||||||
|
|
||||||
|
if (options?.recursive) {
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The query finds all descendants. Filter to just the properties that belong
|
||||||
|
// to the given devicetree node.
|
||||||
|
const childProps = props.filter((prop) =>
|
||||||
|
getContainingDevicetreeNode(prop)?.equals(node)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sort in descending order to select the last instance of the property.
|
||||||
|
childProps.sort((a, b) => b.startIndex - a.startIndex);
|
||||||
|
return childProps[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
export function getDevicetreeNodePath(node: Parser.SyntaxNode | null) {
|
export function getDevicetreeNodePath(node: Parser.SyntaxNode | null) {
|
||||||
const parts = getDevicetreeNodePathParts(node);
|
const parts = getDevicetreeNodePathParts(node);
|
||||||
|
|
||||||
|
@ -99,9 +163,7 @@ export function getDevicetreeNodePath(node: Parser.SyntaxNode | null) {
|
||||||
return parts[0] === "/" ? path.substring(1) : path;
|
return parts[0] === "/" ? path.substring(1) : path;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDevicetreeNodePathParts(
|
function getDevicetreeNodePathParts(node: Parser.SyntaxNode | null): string[] {
|
||||||
node: Parser.SyntaxNode | null
|
|
||||||
): string[] {
|
|
||||||
// There may be intermediate syntax nodes between devicetree nodes, such as
|
// There may be intermediate syntax nodes between devicetree nodes, such as
|
||||||
// #if blocks, so if we aren't currently on a "node" node, traverse up the
|
// #if blocks, so if we aren't currently on a "node" node, traverse up the
|
||||||
// tree until we find one.
|
// tree until we find one.
|
||||||
|
@ -115,7 +177,7 @@ export function getDevicetreeNodePathParts(
|
||||||
return [...getDevicetreeNodePathParts(dtnode.parent), name];
|
return [...getDevicetreeNodePathParts(dtnode.parent), name];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getContainingDevicetreeNode(node: Parser.SyntaxNode | null) {
|
export function getContainingDevicetreeNode(node: Parser.SyntaxNode | null) {
|
||||||
while (node && node.type !== "node") {
|
while (node && node.type !== "node") {
|
||||||
node = node.parent;
|
node = node.parent;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue