escriba_core/motion.rs
1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4/// Cursor motions — primitive movements the keymap compiles user keys to.
5///
6/// Two families:
7/// - **Text motions** — vim-ish char/word/line/doc/page motions.
8/// - **Structural motions** — Lisp-aware `(forward-sexp)` / `(backward-sexp)`
9/// / `(up-list)` / `(down-list)` equivalents. Enabled on buffers whose
10/// major mode opts in via `(defmajor-mode … :structural-lisp #t)`.
11/// Matches paredit's model — equal-or-superior to emacs on Lisp UX.
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
13pub enum Motion {
14 // ── Text motions (vim-ish base) ────────────────────────────────
15 Left,
16 Right,
17 Up,
18 Down,
19 WordStartNext,
20 WordEndNext,
21 WordStartPrev,
22 LineStart,
23 LineFirstNonBlank,
24 LineEnd,
25 DocStart,
26 DocEnd,
27 PageUp,
28 PageDown,
29 HalfPageUp,
30 HalfPageDown,
31 GotoLine(u32),
32
33 // ── Structural Lisp motions (paredit-grade) ────────────────────
34 /// Move to the start of the next sibling s-expression.
35 ForwardSexp,
36 /// Move to the start of the previous sibling s-expression.
37 BackwardSexp,
38 /// Move up one parenthesis level — to the opening `(` of the enclosing list.
39 UpList,
40 /// Move down into the current list — past the opening `(`.
41 DownList,
42 /// Move to the start of the enclosing top-level defun / top form.
43 BeginningOfDefun,
44 /// Move to the end of the enclosing top-level defun / top form.
45 EndOfDefun,
46 /// Move to the start of the current s-expression (current atom / list open).
47 BeginningOfSexp,
48 /// Move to the end of the current s-expression (matching close).
49 EndOfSexp,
50
51 // ── search motions ────────────────────────────────────────────────
52 /// To the next search match — vim's `n` used as a MOTION, which is what
53 /// makes `d/foo<CR>`, `dn` and `y*` work. Search being a motion rather
54 /// than a bare cursor jump is the difference between a search box and vim
55 /// search; resolving it needs the committed `SearchState`, so the executor
56 /// supplies it — the enum stays a pure description, like every other arm.
57 SearchNext,
58 /// To the previous search match (vim's `N` as a motion).
59 SearchPrev,
60}
61
62impl Motion {
63 #[must_use]
64 pub const fn is_structural(self) -> bool {
65 matches!(
66 self,
67 Self::ForwardSexp
68 | Self::BackwardSexp
69 | Self::UpList
70 | Self::DownList
71 | Self::BeginningOfDefun
72 | Self::EndOfDefun
73 | Self::BeginningOfSexp
74 | Self::EndOfSexp,
75 )
76 }
77}
78
79/// Operators — vim-style verbs. Combined with a motion they produce an edit.
80///
81/// Structural operators (paredit-grade) compose with structural motions:
82/// - `(slurp-forward)` — pull the next sibling into the current list
83/// - `(barf-forward)` — push the last child out of the current list
84/// - `(splice)` — unwrap the current list (remove parens, keep children)
85/// - `(wrap)` — wrap the target in a new list
86/// - `(raise)` — replace the enclosing list with the current sexp
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
88pub enum Operator {
89 Delete,
90 Yank,
91 Change,
92 Indent,
93 Dedent,
94 Filter,
95 Format,
96 // ── Structural (Lisp-aware) operators ──────────────────────────
97 SlurpForward,
98 SlurpBackward,
99 BarfForward,
100 BarfBackward,
101 Splice,
102 Wrap,
103 Raise,
104}
105
106impl Operator {
107 #[must_use]
108 pub const fn leaves_register(self) -> bool {
109 matches!(self, Self::Delete | Self::Yank | Self::Change)
110 }
111
112 #[must_use]
113 pub const fn is_structural(self) -> bool {
114 matches!(
115 self,
116 Self::SlurpForward
117 | Self::SlurpBackward
118 | Self::BarfForward
119 | Self::BarfBackward
120 | Self::Splice
121 | Self::Wrap
122 | Self::Raise,
123 )
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn register_emitting_ops() {
133 assert!(Operator::Delete.leaves_register());
134 assert!(Operator::Yank.leaves_register());
135 assert!(Operator::Change.leaves_register());
136 assert!(!Operator::Format.leaves_register());
137 }
138}