Skip to main content

lattice_syntax/
text_objects.rs

1//! N.1.4c: structural (tree-sitter) text objects -- the universal
2//! `af`/`if` (function), `ac`/`ic` (class), `aa`/`ia` (parameter),
3//! `al`/`il` (loop) grammar objects. They are first-class citizens of
4//! Lattice's vim grammar (compose with every operator: `daf`, `vic`,
5//! `yaa`, and the `zn` narrow operator's `znaf`), owned here in
6//! `lattice-syntax` per the locked N.1 design.
7//!
8//! Each object's `apply` reads the cursor's enclosing scope from
9//! `ctx.scope_resolver` -- the host threads the active buffer's
10//! `SyntaxSnapshot` (which impls [`lattice_grammar::ScopeResolver`],
11//! N.1.4b) into the [`lattice_grammar::registry::TextObjectContext`] via
12//! `Document::dispatch_with_scope_resolver`. The resolver returns the
13//! byte-precise `[start, end)` span of the smallest matching
14//! `textobjects.scm` capture (`@function.outer`, `@loop.inner`, ...).
15//!
16//! Graceful failure: with no resolver (a plain-language buffer with no
17//! syntax tree) or no enclosing match (cursor outside any function /
18//! class / ...), `apply` returns an *empty* range at the cursor, so the
19//! paired operator no-ops -- matching vim, where `daf` with no enclosing
20//! function does nothing.
21
22use lattice_grammar::TextObjectId;
23use lattice_grammar::registry::{CommandRegistry, TextObjectSpec};
24use std::sync::Arc;
25
26/// The eight structural text-object [`TextObjectId`]s, returned by
27/// [`register_syntax_text_objects`] so the host can bind their chords
28/// (`af`/`if`/`ac`/`ic`/`aa`/`ia`/`al`/`il`) into the operator-pending
29/// and Visual text-object keymap tables (mirrors how the builtin
30/// objects' ids drive `register_text_object_resolutions`).
31#[derive(Debug, Clone, Copy)]
32pub struct SyntaxTextObjectIds {
33    pub around_function: TextObjectId,
34    pub inner_function: TextObjectId,
35    pub around_class: TextObjectId,
36    pub inner_class: TextObjectId,
37    pub around_parameter: TextObjectId,
38    pub inner_parameter: TextObjectId,
39    pub around_loop: TextObjectId,
40    pub inner_loop: TextObjectId,
41}
42
43/// Register the eight structural text objects on `registry`. Call once
44/// at boot (after the builtin grammar is registered); thread the
45/// returned ids into the keymap binder. Calling twice registers
46/// duplicates, so call exactly once.
47pub fn register_syntax_text_objects(registry: &mut CommandRegistry) -> SyntaxTextObjectIds {
48    SyntaxTextObjectIds {
49        around_function: reg(
50            registry,
51            "text-object:around-function",
52            "A function -- the whole `fn` / `def` / declaration including its \
53             body (vim-grammar `af`). Resolved from the tree-sitter \
54             `@function.outer` capture.",
55            "function.outer",
56        ),
57        inner_function: reg(
58            registry,
59            "text-object:inner-function",
60            "Inner function -- the body block / suite (`if`). \
61             `@function.inner`.",
62            "function.inner",
63        ),
64        around_class: reg(
65            registry,
66            "text-object:around-class",
67            "A class / type -- struct / enum / union / trait / impl / class \
68             including its body (`ac`). `@class.outer`.",
69            "class.outer",
70        ),
71        inner_class: reg(
72            registry,
73            "text-object:inner-class",
74            "Inner class -- the body (field / declaration list) (`ic`). \
75             `@class.inner`.",
76            "class.inner",
77        ),
78        around_parameter: reg(
79            registry,
80            "text-object:around-parameter",
81            "A parameter / argument (`aa`). `@parameter.outer`.",
82            "parameter.outer",
83        ),
84        inner_parameter: reg(
85            registry,
86            "text-object:inner-parameter",
87            "Inner parameter / argument (`ia`). `@parameter.inner`.",
88            "parameter.inner",
89        ),
90        around_loop: reg(
91            registry,
92            "text-object:around-loop",
93            "A loop -- for / while / loop including its body (`al`). \
94             `@loop.outer`.",
95            "loop.outer",
96        ),
97        inner_loop: reg(
98            registry,
99            "text-object:inner-loop",
100            "Inner loop -- the body block / suite (`il`). `@loop.inner`.",
101            "loop.inner",
102        ),
103    }
104}
105
106/// Register one structural object whose `apply` forwards the cursor to
107/// `ctx.scope_resolver.scope_at(line, byte, capture_suffix)`. The
108/// `'static` suffix is moved into the boxed closure.
109fn reg(
110    registry: &mut CommandRegistry,
111    name: &str,
112    doc: &str,
113    capture_suffix: &'static str,
114) -> TextObjectId {
115    registry.register_text_object(
116        name,
117        doc,
118        TextObjectSpec {
119            apply: Arc::new(move |ctx| {
120                Ok(ctx
121                    .scope_resolver
122                    .and_then(|r| r.scope_at(ctx.at.line, ctx.at.byte, capture_suffix))
123                    .unwrap_or_else(|| lattice_protocol::position::Range::empty(ctx.at)))
124            }),
125            args_schema: Vec::new(),
126        },
127    )
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn registers_eight_distinct_text_objects() {
136        let mut registry = CommandRegistry::new();
137        let ids = register_syntax_text_objects(&mut registry);
138        let all = [
139            ids.around_function,
140            ids.inner_function,
141            ids.around_class,
142            ids.inner_class,
143            ids.around_parameter,
144            ids.inner_parameter,
145            ids.around_loop,
146            ids.inner_loop,
147        ];
148        let mut seen = std::collections::HashSet::new();
149        for id in all {
150            assert!(seen.insert(id.0), "duplicate text-object id");
151            let spec = registry.lookup(id.0).expect("registered");
152            assert_eq!(spec.kind, lattice_grammar::CommandKind::TextObject);
153        }
154        assert_eq!(seen.len(), 8, "expected 8 distinct structural objects");
155        // Resolvable by their dashed namespaced names.
156        assert!(registry.id_by_name("text-object:around-function").is_some());
157        assert!(registry.id_by_name("text-object:inner-loop").is_some());
158    }
159}