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
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}