lattice_host/operator_chord_wirer.rs
1//! CM.2: the host's answer to a plugin operator that declared a chord.
2//!
3//! `lattice-plugin-loader` learns, at drain time, that a plugin's operator
4//! wants `gc`. It cannot bind it: the operator-pending composition — motion
5//! targets, the doubled linewise form, `i_` / `a_` text-object pendings, the
6//! `f` / `F` / `t` / `T` find-char pendings — is built from host-resolved
7//! builtin ids, and the loader does not (and should not) depend on
8//! `lattice-host`.
9//!
10//! So the host implements [`lattice_mode::OperatorChordWirer`] over
11//! [`crate::keymap_normal::register_operator_bindings_in`] and publishes it as
12//! a service. This is the same split N.1.3 made for narrow's `zn`, with a
13//! process boundary in the middle: the provider owns the spec and `apply`, the
14//! host owns the chord wiring.
15
16use lattice_keymap::{KeymapHandle, KeymapLayer};
17use lattice_protocol::chord::ChordPattern;
18
19// From their origin crates: `keymap_normal` re-exports these privately.
20use lattice_grammar::builtins::Builtins;
21use lattice_syntax::motions::SyntaxMotionIds;
22use lattice_syntax::text_objects::SyntaxTextObjectIds;
23
24/// Holds the boot-resolved pieces the composition needs. Cheap to clone; the
25/// id tables are `Copy`-ish value types captured once at boot.
26pub struct HostOperatorChordWirer {
27 keymap: KeymapHandle,
28 builtins: Builtins,
29 syntax_textobjects: SyntaxTextObjectIds,
30 syntax_motions: SyntaxMotionIds,
31}
32
33impl HostOperatorChordWirer {
34 pub fn new(
35 keymap: KeymapHandle,
36 builtins: Builtins,
37 syntax_textobjects: SyntaxTextObjectIds,
38 syntax_motions: SyntaxMotionIds,
39 ) -> Self {
40 Self {
41 keymap,
42 builtins,
43 syntax_textobjects,
44 syntax_motions,
45 }
46 }
47}
48
49impl lattice_mode::OperatorChordWirer for HostOperatorChordWirer {
50 fn wire(
51 &self,
52 op: lattice_grammar::registry::OperatorId,
53 chord: &str,
54 doubled: Option<char>,
55 mode: lattice_mode::ModeId,
56 plugin_id: u32,
57 plugin_name: &str,
58 post_motion_char: bool,
59 ) -> Result<(), String> {
60 // A chord that does not parse is a manifest error, reported rather than
61 // guessed at. `register-binding` takes the same line: a plugin never
62 // silently mis-binds.
63 let prefix: Vec<ChordPattern> = lattice_protocol::parse_chord_sequence(chord)
64 .map_err(|e| format!("chord {chord:?} does not parse: {e}"))?
65 .into_iter()
66 .map(ChordPattern::Literal)
67 .collect();
68 if prefix.is_empty() {
69 return Err(format!("chord {chord:?} is empty"));
70 }
71
72 crate::keymap_normal::register_operator_bindings_in(
73 // Scoped to the plugin's own mode, never `Builtin`: a chord bound
74 // at `Builtin` would outlive `:set <id>.enabled=false` and point at
75 // a handler that is gone.
76 KeymapLayer::MinorMode(mode),
77 // CM.4: stamped as the PLUGIN's, not this file's. `:describe-key
78 // gc` answers "where did this come from", and the honest answer is
79 // the plugin that declared the chord.
80 lattice_grammar::SourceLocation::plugin_named(plugin_id, plugin_name),
81 &self.keymap,
82 &prefix,
83 op,
84 doubled.map(|c| ChordPattern::Literal(lattice_protocol::chord::KeyChord::char(c))),
85 &self.builtins,
86 &self.syntax_textobjects,
87 &self.syntax_motions,
88 post_motion_char,
89 );
90 Ok(())
91 }
92}