Skip to main content

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}