Skip to main content

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}