lattice_grammar/lib.rs
1//! Vim modal editing engine and the unified command/grammar dispatch.
2//!
3//! Per DESIGN.md §5.2:
4//!
5//! - The vim grammar is the public command API.
6//! - There is one `CommandRegistry` and one `execute(...)` dispatcher.
7//! - Operators, motions, text objects, ex-commands, and plugin contributions
8//! are all peers of the dispatcher.
9//! - Built-in motions / operators / text objects live here in native Rust
10//! (off the WASM hot path).
11//!
12//! # What this crate owns
13//!
14//! - **The command registry.** [`CommandRegistry`] holds every motion,
15//! operator, text object, ex-command and action under a namespaced name
16//! (`motion:word-forward`, `operator:delete`, `ex:write`), each with a
17//! typed spec ([`MotionSpec`], [`OperatorSpec`], [`TextObjectSpec`],
18//! [`ExCommandSpec`], [`ActionSpec`]) and introspectable metadata
19//! ([`CommandSpec`]). Plugins register into the same registry through
20//! `register_plugin_*`; [`CommandRegistryHandle`] is how it is shared.
21//! - **The call type and the dispatcher.** [`CommandInvocation`] is the one
22//! shape every front-end produces (chord, `:` line, palette, macro, plugin);
23//! [`execute`] / [`execute_with_env`] resolve it against a
24//! `lattice_core::Document` and return an [`Effect`]. Dispatch is
25//! synchronous and runs on the document's actor.
26//! - **The grammar's typed values.** [`Count`], [`Register`], [`Range`],
27//! [`Target`], [`Args`], [`ModalState`], and the [`Effect`] /
28//! [`AppEffect`] vocabulary commands return to the host.
29//! - **The native vim catalog.** [`builtins::populate`] and
30//! [`ex_commands::populate`] register the built-in motions, operators, text
31//! objects and ex-commands; [`reflow`] is the `gq` engine.
32//! - **Introspection.** [`Introspectable`] and [`render_introspection`] give
33//! every `:describe-*` view one shape.
34//!
35//! # What it must not depend on
36//!
37//! Only `lattice-protocol` and `lattice-core` from the workspace. No
38//! tree-sitter, no `lattice-mode` / `lattice-runtime` / host / UI crate, no
39//! plugin runtime. The grammar runs on every keystroke and is linked by all
40//! of those layers, so it must sit beneath them; anything it needs from
41//! above arrives as data or as a small trait the host implements
42//! ([`ScopeResolver`], [`IndentResolver`], [`FoldResolver`],
43//! [`MarkResolver`], [`ViewportResolver`], [`DisplayResolver`]) bundled in
44//! a per-dispatch [`GrammarEnv`]. That boundary is why this is a crate: it
45//! is what keeps built-in and plugin commands, and every buffer kind, on
46//! one dispatch path without dragging the syntax stack or the host into it.
47//!
48//! # Example
49//!
50//! Register a motion, then dispatch `2` of it through the one dispatcher:
51//!
52//! ```
53//! use std::sync::Arc;
54//! use lattice_core::{BufferId, Document};
55//! use lattice_grammar::registry::MotionResult;
56//! use lattice_grammar::{
57//! CancellationToken, CommandInvocation, CommandRegistry, Count, CurswantEffect, Effect,
58//! MotionSpec, execute,
59//! };
60//! use lattice_protocol::position::Position;
61//!
62//! let mut registry = CommandRegistry::new();
63//! let down = registry.register_motion(
64//! "motion:my-line-down",
65//! "Move `count` lines down, to column 0.",
66//! MotionSpec {
67//! jump: false,
68//! exclusive: false,
69//! curswant: CurswantEffect::default(),
70//! args_schema: vec![],
71//! apply: Arc::new(|ctx| {
72//! let target = Position::new(ctx.from.line + ctx.count.get(), 0);
73//! Ok(MotionResult { target, ..Default::default() })
74//! }),
75//! },
76//! );
77//!
78//! let mut doc = Document::from_text("a\nb\nc\n");
79//! let effect = execute(
80//! ®istry,
81//! &mut doc,
82//! BufferId(0),
83//! Position::ZERO,
84//! CommandInvocation::of(down.0).with_count(Count(2)),
85//! &CancellationToken::never(),
86//! )
87//! .unwrap();
88//! assert!(matches!(effect, Effect::CursorMove(p) if p == Position::new(2, 0)));
89//! ```
90//!
91//! # Design documents
92//!
93//! - `docs/dev/architecture/design.md` §5.2 (modal engine; §5.2.1 unified
94//! dispatch, §5.2.5 latency classes), §5.11 (introspection)
95//! - `docs/dev/architecture/typed-motion-dispatch.md`
96//! - `docs/dev/architecture/treesitter-motions.md`
97//! - `docs/dev/architecture/select-mode.md`
98//! - `docs/dev/architecture/text-reflow.md`
99//! - `docs/dev/architecture/auto-indent.md`
100#![warn(missing_docs)]
101
102pub mod app_effect;
103pub mod args;
104pub mod builtins;
105pub mod cancel;
106pub mod command;
107pub mod dispatcher;
108pub mod effect;
109pub mod error;
110pub mod ex_commands;
111pub mod introspect;
112pub mod modal;
113pub mod range;
114// RF.1: the text-reflow engine. Here rather than in a crate of its own
115// (heuristic #6: it carves out no dependency surface) and here rather
116// than in `lattice-format` (that crate is process spawning and diffing —
117// a different mechanism that shares a word).
118pub mod reflow;
119pub mod register;
120pub mod registry;
121pub mod source;
122pub mod target;
123
124pub use crate::app_effect::{
125 AppEffect, ErrorTarget, HScroll, InsertLineEdit, PaneDirection, ScrollPos, ViewportPos,
126};
127pub use crate::args::{ArgDefault, ArgKind, ArgSpec, ArgValue, Args};
128pub use crate::cancel::{CancellationToken, CheckCancelled};
129pub use crate::command::{
130 CommandInvocation, CommandKind, CommandSpec, Count, LatencyClass, kind_icon,
131};
132pub use crate::dispatcher::{
133 execute, execute_motion_only, execute_motion_only_reporting, execute_with_env, notice_text,
134};
135pub use crate::effect::{
136 EchoLevel, Effect, FileAnchor, LspRequest, QuitScope, SubstituteScope, Utf16Pos, YankKind,
137};
138pub use crate::error::{CommandError, GrammarResult};
139pub use crate::introspect::{
140 HelpSection, Introspectable, RenderedAnchor, RenderedIntrospection, SourceEntry, SourceLabel,
141 render_introspection, render_introspection_lines, render_introspection_with,
142};
143pub use crate::modal::{ModalState, SearchDirection, VisualKind};
144pub use crate::range::{Range, RangeBound};
145pub use crate::register::Register;
146pub use crate::registry::{
147 ActionContext, ActionSpec, CommandRegistration, CommandRegistry, CommentSyntax, Curswant,
148 CurswantEffect, DisplayResolver, ExCommandContext, ExCommandSpec, FindKind, FoldResolver,
149 GrammarEnv, IndentResolver, LastFind, LastSearch, MarkResolver, MotionNotice, MotionSpec,
150 NavBoundary, NavDir, OperatorContext, OperatorSpec, ScopeResolver, ShownLines, SurfaceForm,
151 TextObjectSpec, ViewportResolver,
152};
153pub use crate::registry::{ExCommandId, MotionId, OperatorId, TextObjectId};
154pub use crate::source::{SourceKind, SourceLayer, SourceLocation};
155pub use crate::target::Target;
156
157/// Re-export the protocol's CommandId so callers don't need a second import.
158pub use lattice_protocol::ids::CommandId;
159
160/// The shared, hot-swappable command registry: the typed handle for
161/// `ServiceRegistry` registration + lookup (M.10.3, 2026-06-03). Mode crates pull it via
162/// `ctx.service::<CommandRegistryHandle>()` to look up
163/// CommandIds by action name (`id_by_name("action:...")`) at
164/// `on_activate` time. Same shape as
165/// `lattice_mode::ActionHandlerRegistryHandle` per
166/// `feedback_servicesregistry_arc_typeid`.
167///
168/// PL8.B / B3b (2026-07-15): held behind `ArcSwap` (was a bare
169/// `Arc<CommandRegistry>`) so the plugin loader can RCU-register a
170/// runtime grammar contribution (`register_plugin_motion` /
171/// `_operator` / `_ex_command` / …) into a cloned registry and
172/// `store` it, while the dispatch path — the per-buffer actor and
173/// every host-side ex-command / completion read — snapshots it
174/// wait-free via `.load()` (`.load_full()` where an owned `Arc`
175/// snapshot must outlive a `&mut self` borrow). Mirrors
176/// `lattice_mode::ModeRegistryHandle` / `lattice_picker::PickerRegistryHandle`
177/// (decision B: ArcSwap all plugin-contributed registries).
178pub type CommandRegistryHandle = std::sync::Arc<arc_swap::ArcSwap<CommandRegistry>>;