Files
cumulus/src/input/command.rs
T
skkeyeandClaude Opus 5.5 13a603837e Tidy a branch and line nodes up (stage 9a)
The first small part of stage 9 (v1.1). <Space>= then m, c or g (the
user's pick: leader menus), or :tidy map|chart|grid, lays out the
cursor's branch while its top node stays put: a mind map puts each node
in the middle of its children (the user's pick), an org chart puts them
side by side below, and a grid makes a block of cards beside the node
(the user's pick), a card's own branch going along as a mind map. Every
node of the branch gets a place, folded away or not (the user's pick),
as one undo step, and the message says when the branch now covers nodes
outside it.

The lines follow the layouts: a parent whose children are all below it
sends its branches down to a shared bend, and a new child of it goes
beside the last one; a grid card behind another isn't joined by a line,
which would look like it joins the two cards.

In visual mode, <Space>a then h, l, k, j or c lines the selection up on
the box around it, each node bringing its branch along, and : opens the
command line on the selection: :align and :tidy take it, and so do
:color, :border, :style, :tag and :fold, as one undo step. The layouts
are pure functions in view::canvas::arrange, applied as one
EditOp::Reposition. Layout and Side moved from the parser to the model
so keys can name them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 10:42:00 -04:00

601 lines
22 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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.
use crate::model::{Layout, Side};
/// 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,
/// The end of the word (`e`, in text).
WordEnd,
/// The start of the line (`0`, in text).
LineStart,
/// The line's first character that isn't a space (`^`, in text).
FirstNonBlank,
/// The end of the line (`$`, in text).
LineEnd,
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),
/// The last `f`, `F`, `t` or `T` again (`;`).
RepeatFind,
/// The last `f`, `F`, `t` or `T` again, the other way (`,`).
RepeatFindBackward,
}
/// 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),
/// What's inside a piece of text (`iw`, `i"`, `i(`).
Inside(TextObject),
/// A piece of text with its edges (`aw`: the word and a space, `a"`:
/// the quotes too).
Around(TextObject),
}
/// Pieces of text an operator can act on wherever the cursor is in them, as
/// in vim: `ciw` changes the word under the cursor.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum TextObject {
Word,
/// Text in double quotes.
Quotes,
/// Text in round brackets.
Parens,
}
/// 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,
/// `r`, in text: the character to put in place of the cursor's.
Replace,
}
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)),
CharKey::Replace => Command::ReplaceChar(c),
}
}
/// Whether the key moves the cursor (`f`, `t`…), so it can be an
/// operator's target too: `dfx`.
pub fn moves(self) -> bool {
matches!(
self,
CharKey::FindForward | CharKey::FindBackward | CharKey::TillForward | CharKey::TillBackward
)
}
/// 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"), ("'", "back to where you were before the last jump")]
}
CharKey::Replace => &[("any key", "the character to put in its place")],
_ => &[("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,
/// `gu`: up to where the document is reached from (the portals that
/// lead to it).
GoUp,
/// 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,
/// The outline: every body under its title, or none.
ToggleBodies,
// Creating and editing.
AddBelow,
AddAbove,
AddChild,
EditTitle,
EditTitleAtStart,
RetypeTitle,
/// In the K card: type in the body, at its end.
EditBody,
/// In the K card: type in the body, at its start.
EditBodyAtStart,
/// `x`: delete just this node; its children move up.
DeleteNode,
/// Inside text: paste after the text cursor.
PasteAfter,
/// Inside text: paste before the text cursor.
PasteBefore,
/// `p` on nodes: paste as the node's last children.
PasteAsChild,
/// `P` on nodes: paste as the node's next siblings.
PasteAsSibling,
Undo,
Redo,
Repeat,
// Vim keys inside text.
/// `i`: type before the cursor.
InsertBefore,
/// `a`: type after the cursor.
InsertAfter,
/// `I`: type at the start of the line.
InsertAtStart,
/// `A`: type at the end of the line.
InsertAtEnd,
/// `r`: this character in place of the cursor's.
ReplaceChar(char),
/// `~`: upper case to lower case and back.
SwitchCase,
/// `J`: the next line joins this one.
JoinLines,
// Typing.
/// Backspace.
DeleteBefore,
/// Delete.
DeleteAfter,
/// `Ctrl-w`.
DeleteWordBefore,
/// `Ctrl-u`.
DeleteLineBefore,
// Arranging.
/// Lay the cursor's branch out (or each selected one).
Tidy(Layout),
/// Line the selected nodes up.
Align(Side),
// 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("word-end", M::WordEnd, "the end of the word"),
motion("line-start", M::LineStart, "the start of the line"),
motion("first-non-blank", M::FirstNonBlank, "the first character of the line that isn't a space"),
motion("line-end", M::LineEnd, "the end of the line"),
motion("repeat-find", M::RepeatFind, "the last f / t again"),
motion("repeat-find-backward", M::RepeatFindBackward, "the last f / t again, the other way"),
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("go-up", C::GoUp, "up to the portals that lead here"),
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"),
info("toggle-bodies", C::ToggleBodies, "show every body under its title, or none"),
// 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("edit-body", C::EditBody, "edit the body"),
info("edit-body-start", C::EditBodyAtStart, "edit the body from its start"),
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 children"),
info("paste-as-sibling", C::PasteAsSibling, "paste as siblings below"),
info("undo", C::Undo, "undo"),
info("redo", C::Redo, "redo"),
info("repeat", C::Repeat, "repeat the last change"),
info("indent-node", C::Operate(Op::Indent, Target::Current), "under the node above"),
info("outdent-node", C::Operate(Op::Outdent, Target::Current), "out from under its parent"),
// Vim keys inside text.
info("insert", C::InsertBefore, "type before the cursor"),
info("append", C::InsertAfter, "type after the cursor"),
info("insert-at-start", C::InsertAtStart, "type at the start of the line"),
info("append-at-end", C::InsertAtEnd, "type at the end of the line"),
with_char("replace-char", CharKey::Replace, "replace the character"),
info("switch-case", C::SwitchCase, "switch upper and lower case"),
info("join-lines", C::JoinLines, "join the next line to this one"),
// Typing.
info("delete-before", C::DeleteBefore, "delete the character before the cursor"),
info("delete-after", C::DeleteAfter, "delete the character after the cursor"),
info("delete-word-before", C::DeleteWordBefore, "delete the word before the cursor"),
info("delete-line-before", C::DeleteLineBefore, "delete back to the start of the line"),
// Arranging.
info("tidy-map", C::Tidy(Layout::Map), "tidy: a mind map"),
info("tidy-chart", C::Tidy(Layout::Chart), "tidy: an org chart"),
info("tidy-grid", C::Tidy(Layout::Grid), "tidy: a grid beside it"),
info("align-left", C::Align(Side::Left), "left edges in a line"),
info("align-right", C::Align(Side::Right), "right edges in a line"),
info("align-top", C::Align(Side::Top), "top edges in a line"),
info("align-bottom", C::Align(Side::Bottom), "bottom edges in a line"),
info("align-center", C::Align(Side::Center), "middles in a column"),
// 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);
}
}