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
52impl Motion {
53 #[must_use]
54 pub const fn is_structural(self) -> bool {
55 matches!(
56 self,
57 Self::ForwardSexp
58 | Self::BackwardSexp
59 | Self::UpList
60 | Self::DownList
61 | Self::BeginningOfDefun
62 | Self::EndOfDefun
63 | Self::BeginningOfSexp
64 | Self::EndOfSexp,
65 )
66 }
67}
68
69/// Operators — vim-style verbs. Combined with a motion they produce an edit.
70///
71/// Structural operators (paredit-grade) compose with structural motions:
72/// - `(slurp-forward)` — pull the next sibling into the current list
73/// - `(barf-forward)` — push the last child out of the current list
74/// - `(splice)` — unwrap the current list (remove parens, keep children)
75/// - `(wrap)` — wrap the target in a new list
76/// - `(raise)` — replace the enclosing list with the current sexp
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, JsonSchema)]
78pub enum Operator {
79 Delete,
80 Yank,
81 Change,
82 Indent,
83 Dedent,
84 Filter,
85 Format,
86 // ── Structural (Lisp-aware) operators ──────────────────────────
87 SlurpForward,
88 SlurpBackward,
89 BarfForward,
90 BarfBackward,
91 Splice,
92 Wrap,
93 Raise,
94}
95
96impl Operator {
97 #[must_use]
98 pub const fn leaves_register(self) -> bool {
99 matches!(self, Self::Delete | Self::Yank | Self::Change)
100 }
101
102 #[must_use]
103 pub const fn is_structural(self) -> bool {
104 matches!(
105 self,
106 Self::SlurpForward
107 | Self::SlurpBackward
108 | Self::BarfForward
109 | Self::BarfBackward
110 | Self::Splice
111 | Self::Wrap
112 | Self::Raise,
113 )
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn register_emitting_ops() {
123 assert!(Operator::Delete.leaves_register());
124 assert!(Operator::Yank.leaves_register());
125 assert!(Operator::Change.leaves_register());
126 assert!(!Operator::Format.leaves_register());
127 }
128}