Keys now work the way vim reads them. A key press becomes a Key (with vim notation for writing keys: `<C-w>v`, `<leader>c`, `<<`), and the engine collects counts (`12j`), registers (`"+p`), sequences (`gg`), operators with their targets (`dd`, `dab`, `2d3d` multiplies to 6) and keys that take a character (`ma`, `'A`), then hands over a typed Command. Every command has a name and a help text in one table, and the default keys live in one table per place they work (motions, normal, canvas, visual, move, connect, lists). keys.toml changes keys per section, with the leader configurable and "nothing" to unbind. Each mistake is skipped and reported in words, with a suggestion for mistyped command names, and a key that would hide a longer one is reported too. The which-key popup reads the same tables: it shows what can follow the keys typed so far, in skyshift's look and place, with the leader menu instant and other prefixes after 300 ms. The mouse reader turns button events into presses, double-clicks, drags and scrolls. `Direction` moved from the glyphs to the model, since input needs it too. The canvas example now runs on all of this, with a two-line keys.toml of its own that makes `q` quit. Co-Authored-By: Claude Opus 5.5 <[email protected]>
475 lines
16 KiB
Rust
475 lines
16 KiB
Rust
//! Everything a key can ask for. Views never see raw key presses: the engine
|
||
//! turns them into [`Command`]s using the tables in
|
||
//! [`bindings`](super::bindings), and each view decides what a command means
|
||
//! there. `Move(Left)` goes to the nearest node on the left on the canvas,
|
||
//! for example, and to the parent in the outline.
|
||
//!
|
||
//! [`COMMANDS`] names every command, for `keys.toml`, and says what it does,
|
||
//! for which-key and the help.
|
||
|
||
/// Ways to move the cursor.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||
pub enum Motion {
|
||
Left,
|
||
Down,
|
||
Up,
|
||
Right,
|
||
Parent,
|
||
FirstChild,
|
||
NextSibling,
|
||
PreviousSibling,
|
||
/// The next node in reading order (in text: the next word).
|
||
Next,
|
||
/// The previous node in reading order (in text: the previous word).
|
||
Previous,
|
||
/// The root (in text: the start).
|
||
First,
|
||
/// The last node (in text: the end).
|
||
Last,
|
||
SearchNext,
|
||
SearchPrevious,
|
||
/// To the next time a character shows up on the line (`f`, in text).
|
||
FindForward(char),
|
||
/// To the previous time a character shows up on the line (`F`).
|
||
FindBackward(char),
|
||
/// To just before the next time a character shows up (`t`).
|
||
TillForward(char),
|
||
/// To just after the previous time a character shows up (`T`).
|
||
TillBackward(char),
|
||
}
|
||
|
||
/// Keys that wait for a target, like vim's `d`: `dd`, `dab`.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||
pub enum Operator {
|
||
Delete,
|
||
Yank,
|
||
Change,
|
||
Indent,
|
||
Outdent,
|
||
}
|
||
|
||
impl Operator {
|
||
/// What the operator does, for which-key's title.
|
||
pub fn name(self) -> &'static str {
|
||
match self {
|
||
Operator::Delete => "delete",
|
||
Operator::Yank => "yank",
|
||
Operator::Change => "change",
|
||
Operator::Indent => "indent",
|
||
Operator::Outdent => "outdent",
|
||
}
|
||
}
|
||
}
|
||
|
||
/// What an operator acts on.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||
pub enum Target {
|
||
/// The operator's own key again (`dd`, `yy`, `>>`): this node, with its
|
||
/// branch (in text: this line).
|
||
Current,
|
||
/// `ab`, around a branch: the node and everything below it.
|
||
Branch,
|
||
/// `ib`, inside a branch: just what's below the node.
|
||
Children,
|
||
/// What's selected (an operator in visual mode).
|
||
Selection,
|
||
/// From the cursor to where a motion goes (`dw`, `dfx`, in text).
|
||
Motion(Motion),
|
||
}
|
||
|
||
/// Keys that need one more character, typed right after them: `m` then a
|
||
/// letter.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||
pub enum CharKey {
|
||
SetMark,
|
||
JumpToMark,
|
||
FindForward,
|
||
FindBackward,
|
||
TillForward,
|
||
TillBackward,
|
||
}
|
||
|
||
impl CharKey {
|
||
/// The command once the character is known.
|
||
pub fn command(self, c: char) -> Command {
|
||
match self {
|
||
CharKey::SetMark => Command::SetMark(c),
|
||
CharKey::JumpToMark => Command::JumpToMark(c),
|
||
CharKey::FindForward => Command::Move(Motion::FindForward(c)),
|
||
CharKey::FindBackward => Command::Move(Motion::FindBackward(c)),
|
||
CharKey::TillForward => Command::Move(Motion::TillForward(c)),
|
||
CharKey::TillBackward => Command::Move(Motion::TillBackward(c)),
|
||
}
|
||
}
|
||
|
||
/// As the target of an operator, once the character is known (`dfx`).
|
||
/// Only motions can be targets.
|
||
pub fn target(self, c: char) -> Option<Target> {
|
||
match self.command(c) {
|
||
Command::Move(motion) => Some(Target::Motion(motion)),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// What the character is for, for which-key: the keys, and what they do.
|
||
pub fn hints(self) -> &'static [(&'static str, &'static str)] {
|
||
match self {
|
||
CharKey::SetMark => &[("a–z", "mark this node"), ("A–Z", "mark it, across documents")],
|
||
CharKey::JumpToMark => &[("a–z A–Z", "go to that mark")],
|
||
_ => &[("any key", "the character to go to")],
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Where text goes and comes from with yank and paste: `"a` before a
|
||
/// command picks register `a`.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||
pub enum Register {
|
||
/// The usual one, when no register is named.
|
||
Unnamed,
|
||
/// `"a` to `"z` (capitals name the same registers).
|
||
Named(char),
|
||
/// `"+` (or `"*`): the system clipboard.
|
||
Clipboard,
|
||
/// `"_`: nowhere, for deleting without keeping a copy.
|
||
BlackHole,
|
||
}
|
||
|
||
impl Register {
|
||
/// The register a character names after `"`, if it names one.
|
||
pub fn named(c: char) -> Option<Register> {
|
||
match c {
|
||
'"' => Some(Register::Unnamed),
|
||
'a'..='z' => Some(Register::Named(c)),
|
||
'A'..='Z' => Some(Register::Named(c.to_ascii_lowercase())),
|
||
'+' | '*' => Some(Register::Clipboard),
|
||
'_' => Some(Register::BlackHole),
|
||
_ => None,
|
||
}
|
||
}
|
||
|
||
/// The registers, for which-key after `"`.
|
||
pub const HINTS: &[(&str, &str)] = &[
|
||
("a–z", "a named register"),
|
||
("+", "the system clipboard"),
|
||
("_", "nowhere (delete without a copy)"),
|
||
];
|
||
}
|
||
|
||
/// Everything a key sequence can ask for.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||
pub enum Command {
|
||
/// Move the cursor.
|
||
Move(Motion),
|
||
/// An operator with its target: `dd`, `yab`.
|
||
Operate(Operator, Target),
|
||
/// Mark the cursor's node with a letter (`ma`).
|
||
SetMark(char),
|
||
/// Go to a marked node (`'a`).
|
||
JumpToMark(char),
|
||
|
||
// Jumping around.
|
||
Search,
|
||
JumpBack,
|
||
JumpForward,
|
||
FollowPortal,
|
||
FollowLink,
|
||
/// Enter: edit the title, or go through a portal.
|
||
Activate,
|
||
|
||
// The view.
|
||
Center,
|
||
ScrollDown,
|
||
ScrollUp,
|
||
ScrollLeft,
|
||
ScrollRight,
|
||
HalfPageDown,
|
||
HalfPageUp,
|
||
ZoomIn,
|
||
ZoomOut,
|
||
ToggleFold,
|
||
OpenFold,
|
||
CloseFold,
|
||
AutoFold,
|
||
OpenAllFolds,
|
||
CloseAllFolds,
|
||
ToggleFocus,
|
||
ShowBody,
|
||
|
||
// Creating and editing.
|
||
AddBelow,
|
||
AddAbove,
|
||
AddChild,
|
||
EditTitle,
|
||
EditTitleAtStart,
|
||
RetypeTitle,
|
||
/// `x`: delete just this node; its children move up.
|
||
DeleteNode,
|
||
PasteAfter,
|
||
PasteBefore,
|
||
PasteAsChild,
|
||
Undo,
|
||
Redo,
|
||
Repeat,
|
||
|
||
// Looks, tags, portals.
|
||
PickColor,
|
||
PickStyle,
|
||
PickBorder,
|
||
EditTags,
|
||
NewPortal,
|
||
Capture,
|
||
|
||
// Modes.
|
||
Visual,
|
||
VisualBranches,
|
||
MoveMode,
|
||
ConnectMode,
|
||
CommandLine,
|
||
Escape,
|
||
|
||
// In visual, move and connect modes.
|
||
SwapEnds,
|
||
FarLeft,
|
||
FarDown,
|
||
FarUp,
|
||
FarRight,
|
||
ToggleWhole,
|
||
Confirm,
|
||
RemoveLink,
|
||
AttachAsChild,
|
||
CycleArrows,
|
||
CycleLine,
|
||
EditLabel,
|
||
|
||
// Panes and documents.
|
||
SplitVertical,
|
||
SplitHorizontal,
|
||
FocusLeft,
|
||
FocusDown,
|
||
FocusUp,
|
||
FocusRight,
|
||
NextPane,
|
||
ClosePane,
|
||
OnlyPane,
|
||
EqualizePanes,
|
||
NextDocument,
|
||
PreviousDocument,
|
||
PickDocument,
|
||
ShowCanvas,
|
||
ShowOutline,
|
||
ShowNote,
|
||
|
||
// Help and the app.
|
||
Help,
|
||
ToggleWhichKey,
|
||
/// `ZZ`: close the pane (everything is saved already).
|
||
Close,
|
||
}
|
||
|
||
/// What a key sequence is bound to.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||
pub enum Bound {
|
||
/// A command, run as soon as its keys are typed.
|
||
Command(Command),
|
||
/// An operator: it waits for a target.
|
||
Operator(Operator),
|
||
/// A key that waits for one more character.
|
||
WithChar(CharKey),
|
||
}
|
||
|
||
/// What a key sequence typed after an operator means.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||
pub enum TargetBound {
|
||
Target(Target),
|
||
WithChar(CharKey),
|
||
}
|
||
|
||
/// A command's name in `keys.toml`, and what it does.
|
||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||
pub struct CommandInfo {
|
||
pub name: &'static str,
|
||
pub bound: Bound,
|
||
pub help: &'static str,
|
||
}
|
||
|
||
const fn info(name: &'static str, command: Command, help: &'static str) -> CommandInfo {
|
||
CommandInfo { name, bound: Bound::Command(command), help }
|
||
}
|
||
|
||
const fn motion(name: &'static str, motion: Motion, help: &'static str) -> CommandInfo {
|
||
info(name, Command::Move(motion), help)
|
||
}
|
||
|
||
const fn operator(name: &'static str, operator: Operator, help: &'static str) -> CommandInfo {
|
||
CommandInfo { name, bound: Bound::Operator(operator), help }
|
||
}
|
||
|
||
const fn with_char(name: &'static str, key: CharKey, help: &'static str) -> CommandInfo {
|
||
CommandInfo { name, bound: Bound::WithChar(key), help }
|
||
}
|
||
|
||
use Command as C;
|
||
use Motion as M;
|
||
use Operator as Op;
|
||
|
||
/// Every command keys can be bound to, by name. A name in `keys.toml` is
|
||
/// one of these.
|
||
pub const COMMANDS: &[CommandInfo] = &[
|
||
// Moving the cursor.
|
||
motion("left", M::Left, "left"),
|
||
motion("down", M::Down, "down"),
|
||
motion("up", M::Up, "up"),
|
||
motion("right", M::Right, "right"),
|
||
motion("parent", M::Parent, "the parent"),
|
||
motion("first-child", M::FirstChild, "the first child"),
|
||
motion("next-sibling", M::NextSibling, "the next sibling"),
|
||
motion("previous-sibling", M::PreviousSibling, "the previous sibling"),
|
||
motion("next", M::Next, "the next node, in reading order"),
|
||
motion("previous", M::Previous, "the previous node, in reading order"),
|
||
motion("first", M::First, "the root"),
|
||
motion("last", M::Last, "the last node"),
|
||
motion("search-next", M::SearchNext, "the next match"),
|
||
motion("search-previous", M::SearchPrevious, "the previous match"),
|
||
with_char("find-forward", CharKey::FindForward, "to a character"),
|
||
with_char("find-backward", CharKey::FindBackward, "back to a character"),
|
||
with_char("till-forward", CharKey::TillForward, "up to a character"),
|
||
with_char("till-backward", CharKey::TillBackward, "back up to a character"),
|
||
with_char("set-mark", CharKey::SetMark, "mark this node"),
|
||
with_char("jump-to-mark", CharKey::JumpToMark, "go to a mark"),
|
||
info("search", C::Search, "search"),
|
||
info("jump-back", C::JumpBack, "jump back"),
|
||
info("jump-forward", C::JumpForward, "jump forward"),
|
||
info("follow-portal", C::FollowPortal, "through the portal"),
|
||
info("follow-link", C::FollowLink, "follow a cross-link"),
|
||
info("activate", C::Activate, "edit the title, or go through a portal"),
|
||
// The view.
|
||
info("center", C::Center, "center the view"),
|
||
info("scroll-down", C::ScrollDown, "scroll down"),
|
||
info("scroll-up", C::ScrollUp, "scroll up"),
|
||
info("scroll-left", C::ScrollLeft, "scroll left"),
|
||
info("scroll-right", C::ScrollRight, "scroll right"),
|
||
info("half-page-down", C::HalfPageDown, "half a screen down"),
|
||
info("half-page-up", C::HalfPageUp, "half a screen up"),
|
||
info("zoom-in", C::ZoomIn, "zoom in"),
|
||
info("zoom-out", C::ZoomOut, "zoom out"),
|
||
info("fold-toggle", C::ToggleFold, "fold or unfold"),
|
||
info("fold-open", C::OpenFold, "unfold"),
|
||
info("fold-close", C::CloseFold, "fold"),
|
||
info("fold-auto", C::AutoFold, "fold automatically (open while inside)"),
|
||
info("fold-open-all", C::OpenAllFolds, "unfold everything"),
|
||
info("fold-close-all", C::CloseAllFolds, "fold everything"),
|
||
info("focus", C::ToggleFocus, "focus: fold the other branches"),
|
||
info("show-body", C::ShowBody, "show the body"),
|
||
// Creating and editing.
|
||
info("add-below", C::AddBelow, "new sibling below"),
|
||
info("add-above", C::AddAbove, "new sibling above"),
|
||
info("add-child", C::AddChild, "new child"),
|
||
info("edit-title", C::EditTitle, "edit the title"),
|
||
info("edit-title-start", C::EditTitleAtStart, "edit the title from its start"),
|
||
info("retype-title", C::RetypeTitle, "retype the title"),
|
||
info("delete-node", C::DeleteNode, "delete just this node (children move up)"),
|
||
operator("delete", Op::Delete, "delete…"),
|
||
operator("yank", Op::Yank, "yank (copy)…"),
|
||
operator("change", Op::Change, "change…"),
|
||
operator("indent", Op::Indent, "indent…"),
|
||
operator("outdent", Op::Outdent, "outdent…"),
|
||
info("delete-branch", C::Operate(Op::Delete, Target::Branch), "delete the branch"),
|
||
info("delete-children", C::Operate(Op::Delete, Target::Children), "delete the children"),
|
||
info("yank-branch", C::Operate(Op::Yank, Target::Branch), "yank the branch"),
|
||
info("yank-children", C::Operate(Op::Yank, Target::Children), "yank the children"),
|
||
info("delete-selection", C::Operate(Op::Delete, Target::Selection), "delete the selection"),
|
||
info("yank-selection", C::Operate(Op::Yank, Target::Selection), "yank the selection"),
|
||
info("indent-selection", C::Operate(Op::Indent, Target::Selection), "indent the selection"),
|
||
info("outdent-selection", C::Operate(Op::Outdent, Target::Selection), "outdent the selection"),
|
||
info("paste-after", C::PasteAfter, "paste after"),
|
||
info("paste-before", C::PasteBefore, "paste before"),
|
||
info("paste-as-child", C::PasteAsChild, "paste as a child"),
|
||
info("undo", C::Undo, "undo"),
|
||
info("redo", C::Redo, "redo"),
|
||
info("repeat", C::Repeat, "repeat the last change"),
|
||
// Looks, tags, portals.
|
||
info("pick-color", C::PickColor, "color…"),
|
||
info("pick-style", C::PickStyle, "style…"),
|
||
info("pick-border", C::PickBorder, "border…"),
|
||
info("edit-tags", C::EditTags, "tags…"),
|
||
info("new-portal", C::NewPortal, "new portal…"),
|
||
info("capture", C::Capture, "capture an idea to the inbox…"),
|
||
// Modes.
|
||
info("visual", C::Visual, "select"),
|
||
info("visual-branches", C::VisualBranches, "select whole branches"),
|
||
info("move-mode", C::MoveMode, "move mode"),
|
||
info("connect-mode", C::ConnectMode, "connect mode"),
|
||
info("command-line", C::CommandLine, "command line"),
|
||
info("escape", C::Escape, "cancel"),
|
||
// In visual, move and connect modes.
|
||
info("swap-ends", C::SwapEnds, "the other end of the selection"),
|
||
info("far-left", C::FarLeft, "left by five"),
|
||
info("far-down", C::FarDown, "down by five"),
|
||
info("far-up", C::FarUp, "up by five"),
|
||
info("far-right", C::FarRight, "right by five"),
|
||
info("toggle-whole", C::ToggleWhole, "the whole branch, or just this node"),
|
||
info("confirm", C::Confirm, "confirm"),
|
||
info("remove-link", C::RemoveLink, "remove the link"),
|
||
info("attach-as-child", C::AttachAsChild, "attach as a child here"),
|
||
info("cycle-arrows", C::CycleArrows, "arrowheads"),
|
||
info("cycle-line", C::CycleLine, "line kind"),
|
||
info("edit-label", C::EditLabel, "label…"),
|
||
// Panes and documents.
|
||
info("split-vertical", C::SplitVertical, "split side by side"),
|
||
info("split-horizontal", C::SplitHorizontal, "split above and below"),
|
||
info("focus-left", C::FocusLeft, "the pane on the left"),
|
||
info("focus-down", C::FocusDown, "the pane below"),
|
||
info("focus-up", C::FocusUp, "the pane above"),
|
||
info("focus-right", C::FocusRight, "the pane on the right"),
|
||
info("next-pane", C::NextPane, "the next pane"),
|
||
info("close-pane", C::ClosePane, "close the pane"),
|
||
info("only-pane", C::OnlyPane, "close the other panes"),
|
||
info("equalize-panes", C::EqualizePanes, "even out the panes"),
|
||
info("next-document", C::NextDocument, "the next document"),
|
||
info("previous-document", C::PreviousDocument, "the previous document"),
|
||
info("pick-document", C::PickDocument, "documents…"),
|
||
info("show-canvas", C::ShowCanvas, "canvas"),
|
||
info("show-outline", C::ShowOutline, "outline"),
|
||
info("show-note", C::ShowNote, "note"),
|
||
// Help and the app.
|
||
info("help", C::Help, "help: every key"),
|
||
info("toggle-which-key", C::ToggleWhichKey, "key hints on or off"),
|
||
info("close", C::Close, "close the pane"),
|
||
];
|
||
|
||
/// The command with this name in `keys.toml`.
|
||
pub fn by_name(name: &str) -> Option<&'static CommandInfo> {
|
||
COMMANDS.iter().find(|info| info.name == name)
|
||
}
|
||
|
||
/// What a binding does, in words (for which-key and the help).
|
||
pub fn help_for(bound: Bound) -> &'static str {
|
||
COMMANDS.iter().find(|info| info.bound == bound).map_or("", |info| info.help)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
#[test]
|
||
fn names_are_unique_kebab_case() {
|
||
for (index, info) in COMMANDS.iter().enumerate() {
|
||
assert!(info.name.chars().all(|c| c.is_ascii_lowercase() || c == '-'), "{}", info.name);
|
||
assert!(COMMANDS[index + 1..].iter().all(|other| other.name != info.name), "{} twice", info.name);
|
||
assert!(
|
||
COMMANDS[index + 1..].iter().all(|other| other.bound != info.bound),
|
||
"{:?} twice",
|
||
info.bound
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn registers_by_name() {
|
||
assert_eq!(Register::named('A'), Some(Register::Named('a')));
|
||
assert_eq!(Register::named('*'), Some(Register::Clipboard));
|
||
assert_eq!(Register::named('1'), None);
|
||
}
|
||
}
|