Skip to main content

lattice_syntax/
motions.rs

1//! Tree-sitter structural motions — `]f`/`[f`/`]F`/`[F` (function),
2//! `]c`/`[c`/`]C`/`[C` (class), `]a`/`[a`/`]A`/`[A` (parameter),
3//! `]l`/`[l`/`]L`/`[L` (loop). The motion counterpart to the structural
4//! text objects (`text_objects.rs`); both read the same textobjects.scm
5//! captures. See docs/dev/architecture/treesitter-motions.md.
6
7use lattice_grammar::registry::{CommandRegistry, MotionResult, MotionSpec};
8use lattice_grammar::{MotionId, NavBoundary, NavDir};
9use std::sync::Arc;
10
11/// The sixteen structural motion [`MotionId`]s, returned by
12/// [`register_syntax_motions`] so the host can bind their chords
13/// (`]f`/`[f`/`]F`/`[F`, `]c`/`[c`/`]C`/`[C`, `]a`/`[a`/`]A`/`[A`,
14/// `]l`/`[l`/`]L`/`[L`) into the Normal/Visual/operator-pending
15/// motion keymap tables.
16#[derive(Debug, Clone, Copy)]
17pub struct SyntaxMotionIds {
18    pub next_function_start: MotionId,
19    pub prev_function_start: MotionId,
20    pub next_function_end: MotionId,
21    pub prev_function_end: MotionId,
22    pub next_class_start: MotionId,
23    pub prev_class_start: MotionId,
24    pub next_class_end: MotionId,
25    pub prev_class_end: MotionId,
26    pub next_parameter_start: MotionId,
27    pub prev_parameter_start: MotionId,
28    pub next_parameter_end: MotionId,
29    pub prev_parameter_end: MotionId,
30    pub next_loop_start: MotionId,
31    pub prev_loop_start: MotionId,
32    pub next_loop_end: MotionId,
33    pub prev_loop_end: MotionId,
34}
35
36/// Register the sixteen structural motions on `registry`. Call once at
37/// boot (after the builtin grammar is registered); thread the returned
38/// ids into the keymap binder. Calling twice registers duplicates, so
39/// call exactly once.
40pub fn register_syntax_motions(registry: &mut CommandRegistry) -> SyntaxMotionIds {
41    SyntaxMotionIds {
42        next_function_start: reg(
43            registry,
44            "motion:next-function-start",
45            "Go to the start of the next function (`]f`). `@function.outer`.",
46            "function.outer",
47            NavDir::Forward,
48            NavBoundary::Start,
49        ),
50        prev_function_start: reg(
51            registry,
52            "motion:prev-function-start",
53            "Go to the start of the previous (or enclosing) function (`[f`).",
54            "function.outer",
55            NavDir::Backward,
56            NavBoundary::Start,
57        ),
58        next_function_end: reg(
59            registry,
60            "motion:next-function-end",
61            "Go to the end of the current/next function (`]F`).",
62            "function.outer",
63            NavDir::Forward,
64            NavBoundary::End,
65        ),
66        prev_function_end: reg(
67            registry,
68            "motion:prev-function-end",
69            "Go to the end of the previous function (`[F`).",
70            "function.outer",
71            NavDir::Backward,
72            NavBoundary::End,
73        ),
74
75        next_class_start: reg(
76            registry,
77            "motion:next-class-start",
78            "Go to the start of the next class/type (`]c`). `@class.outer`.",
79            "class.outer",
80            NavDir::Forward,
81            NavBoundary::Start,
82        ),
83        prev_class_start: reg(
84            registry,
85            "motion:prev-class-start",
86            "Go to the start of the previous (or enclosing) class (`[c`).",
87            "class.outer",
88            NavDir::Backward,
89            NavBoundary::Start,
90        ),
91        next_class_end: reg(
92            registry,
93            "motion:next-class-end",
94            "Go to the end of the current/next class (`]C`).",
95            "class.outer",
96            NavDir::Forward,
97            NavBoundary::End,
98        ),
99        prev_class_end: reg(
100            registry,
101            "motion:prev-class-end",
102            "Go to the end of the previous class (`[C`).",
103            "class.outer",
104            NavDir::Backward,
105            NavBoundary::End,
106        ),
107
108        next_parameter_start: reg(
109            registry,
110            "motion:next-parameter-start",
111            "Go to the start of the next parameter/argument (`]a`). `@parameter.outer`.",
112            "parameter.outer",
113            NavDir::Forward,
114            NavBoundary::Start,
115        ),
116        prev_parameter_start: reg(
117            registry,
118            "motion:prev-parameter-start",
119            "Go to the start of the previous parameter (`[a`).",
120            "parameter.outer",
121            NavDir::Backward,
122            NavBoundary::Start,
123        ),
124        next_parameter_end: reg(
125            registry,
126            "motion:next-parameter-end",
127            "Go to the end of the current/next parameter (`]A`).",
128            "parameter.outer",
129            NavDir::Forward,
130            NavBoundary::End,
131        ),
132        prev_parameter_end: reg(
133            registry,
134            "motion:prev-parameter-end",
135            "Go to the end of the previous parameter (`[A`).",
136            "parameter.outer",
137            NavDir::Backward,
138            NavBoundary::End,
139        ),
140
141        next_loop_start: reg(
142            registry,
143            "motion:next-loop-start",
144            "Go to the start of the next loop (`]l`). `@loop.outer`.",
145            "loop.outer",
146            NavDir::Forward,
147            NavBoundary::Start,
148        ),
149        prev_loop_start: reg(
150            registry,
151            "motion:prev-loop-start",
152            "Go to the start of the previous (or enclosing) loop (`[l`).",
153            "loop.outer",
154            NavDir::Backward,
155            NavBoundary::Start,
156        ),
157        next_loop_end: reg(
158            registry,
159            "motion:next-loop-end",
160            "Go to the end of the current/next loop (`]L`).",
161            "loop.outer",
162            NavDir::Forward,
163            NavBoundary::End,
164        ),
165        prev_loop_end: reg(
166            registry,
167            "motion:prev-loop-end",
168            "Go to the end of the previous loop (`[L`).",
169            "loop.outer",
170            NavDir::Backward,
171            NavBoundary::End,
172        ),
173    }
174}
175
176/// Register one structural motion. `boundary == Start` ⇒ exclusive; `End` ⇒
177/// inclusive (so `d]F` eats the closing brace). All are jumps (`<C-o>` returns);
178/// jump recording is gated to standalone Normal use by the dispatcher.
179fn reg(
180    registry: &mut CommandRegistry,
181    name: &str,
182    doc: &str,
183    suffix: &'static str,
184    dir: NavDir,
185    boundary: NavBoundary,
186) -> MotionId {
187    let exclusive = matches!(boundary, NavBoundary::Start);
188    registry.register_motion(
189        name,
190        doc,
191        MotionSpec {
192            curswant: lattice_grammar::CurswantEffect::default(),
193            jump: true,
194            exclusive,
195            args_schema: Vec::new(),
196            apply: Arc::new(move |ctx| {
197                let count = ctx.count.get().max(1);
198                let target = ctx
199                    .scope_resolver
200                    .and_then(|r| {
201                        r.scope_toward(ctx.from.line, ctx.from.byte, suffix, dir, boundary, count)
202                    })
203                    .unwrap_or(ctx.from); // graceful no-op: no tree / no match / boundary
204                Ok(MotionResult {
205                    target,
206                    linewise: false,
207                    exclusive: None,
208                    notice: None,
209                    curswant: None,
210                })
211            }),
212        },
213    )
214}
215
216#[cfg(test)]
217mod tests {
218    use super::*;
219
220    #[test]
221    fn registers_sixteen_distinct_motions() {
222        let mut registry = CommandRegistry::new();
223        let ids = register_syntax_motions(&mut registry);
224        let all = [
225            ids.next_function_start,
226            ids.prev_function_start,
227            ids.next_function_end,
228            ids.prev_function_end,
229            ids.next_class_start,
230            ids.prev_class_start,
231            ids.next_class_end,
232            ids.prev_class_end,
233            ids.next_parameter_start,
234            ids.prev_parameter_start,
235            ids.next_parameter_end,
236            ids.prev_parameter_end,
237            ids.next_loop_start,
238            ids.prev_loop_start,
239            ids.next_loop_end,
240            ids.prev_loop_end,
241        ];
242        let mut seen = std::collections::HashSet::new();
243        for id in all {
244            assert!(seen.insert(id.0), "duplicate motion id");
245            let spec = registry.lookup(id.0).expect("registered");
246            assert_eq!(spec.kind, lattice_grammar::CommandKind::Motion);
247        }
248        assert_eq!(seen.len(), 16);
249        // Named + jump=true; start-targets exclusive, end-targets inclusive.
250        assert!(registry.id_by_name("motion:next-function-start").is_some());
251        assert!(registry.id_by_name("motion:prev-loop-end").is_some());
252    }
253}