Skip to main content

reovim_driver_session/
leader_key.rs

1//! Leader key provider for personality-aware binding expansion (#700).
2//!
3//! Personality modules (vim, emacs) register their leader key notation
4//! during `init()`. Bootstrap reads it after all modules have initialized
5//! and expands `<leader>` tokens in keybinding key strings before parsing.
6//!
7//! This follows the established `InitialModeProvider` pattern.
8
9use reovim_kernel::api::v1::Service;
10
11/// Provider for the leader key notation.
12///
13/// Personality modules call [`set`](Self::set) during `init()` to declare
14/// their leader key (e.g., `"<Space>"` for vim). Bootstrap calls [`get`](Self::get)
15/// after all modules have loaded and expands `<leader>` in keybinding strings.
16///
17/// The stored value is `&'static str` because all call sites are personality
18/// module `init()` methods passing compile-time string literals. This matches
19/// the `KeybindingRegistration.keys` lifetime requirement.
20///
21/// If multiple personality modules are loaded, the last writer wins and a
22/// warning should be logged by the caller.
23pub struct LeaderKeyProvider {
24    key: parking_lot::RwLock<Option<&'static str>>,
25}
26
27impl LeaderKeyProvider {
28    /// Create a new empty provider.
29    #[must_use]
30    pub const fn new() -> Self {
31        Self {
32            key: parking_lot::RwLock::new(None),
33        }
34    }
35
36    /// Set the leader key notation. Called by personality modules during `init()`.
37    ///
38    /// The `key` parameter is a key notation string (e.g., `"<Space>"`) that
39    /// will replace `<leader>` tokens in keybinding key strings.
40    ///
41    /// Returns the previous notation if one was already set.
42    pub fn set(&self, key: &'static str) -> Option<&'static str> {
43        self.key.write().replace(key)
44    }
45
46    /// Get the registered leader key notation, if any.
47    #[must_use]
48    pub fn get(&self) -> Option<&'static str> {
49        *self.key.read()
50    }
51
52    /// Expand `<leader>` tokens in a key string using the stored notation.
53    ///
54    /// Returns the original string (owned) if no leader key is registered
55    /// or if the input contains no `<leader>` token.
56    #[must_use]
57    pub fn expand(&self, keys: &str) -> String {
58        self.get()
59            .map_or_else(|| keys.to_owned(), |notation| expand_leader(keys, notation))
60    }
61}
62
63impl Default for LeaderKeyProvider {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69impl Service for LeaderKeyProvider {}
70
71/// Expand all `<leader>` tokens in a key string with the given notation.
72///
73/// Case-insensitive matching: both `<leader>` and `<Leader>` are replaced.
74/// If `notation` is empty, `<leader>` tokens are removed (effectively a
75/// no-op binding prefix).
76///
77/// This is a pure function for testability.
78#[must_use]
79pub fn expand_leader(keys: &str, notation: &str) -> String {
80    // Fast path: no `<` means no special tokens at all.
81    if !keys.contains('<') {
82        return keys.to_owned();
83    }
84
85    let needle = "<leader>";
86    let mut result = String::with_capacity(keys.len());
87    let mut remaining = keys;
88
89    while let Some(pos) = remaining
90        .as_bytes()
91        .windows(needle.len())
92        .position(|w| w.eq_ignore_ascii_case(needle.as_bytes()))
93    {
94        result.push_str(&remaining[..pos]);
95        result.push_str(notation);
96        remaining = &remaining[pos + needle.len()..];
97    }
98    result.push_str(remaining);
99    result
100}
101
102#[cfg(test)]
103#[path = "leader_key_tests.rs"]
104mod tests;