Skip to main content

reovim_module_range_finder/
lib.rs

1#![cfg_attr(coverage_nightly, allow(unused_features))]
2#![cfg_attr(coverage_nightly, feature(coverage_attribute))]
3//! Range-finder module - POLICY layer.
4//!
5//! This module provides jump navigation and code folding:
6//! - **Jump navigation**: `s{char}{char}` two-char search with label overlay
7//! - **Code folding**: `za`/`zo`/`zc`/`zR`/`zM` fold commands
8//!
9//! # Architecture (#524)
10//!
11//! Jump state (`JumpSessionState`) and fold state (`FoldSessionState`) are
12//! independent `SessionExtension` types. Jump labels are rendered by client
13//! extensions via `ExtensionStateBridge`.
14//!
15//! The module registers its own mode (`range-finder:jump-input`) for label
16//! selection. The parent mode for keybinding inheritance is injected by
17//! the personality manifest (e.g., `vim.toml`) via [`ModeBridgeStore`].
18
19pub mod find_char;
20pub mod fold;
21pub mod jump;
22mod keybinding;
23
24use {
25    reovim_driver_input::{KeybindingStore, ModeInfo, ModeInfoStore, ResolverRegistry},
26    reovim_driver_manifest::ModeBridgeStore,
27    reovim_kernel::api::v1::{
28        CursorStyle, KeybindingRegistration, Module, ModuleContext, ModuleError, ModuleId,
29        ProbeResult, Version,
30    },
31};
32
33pub(crate) const KIND_JUMP: &str = "range-finder-jump";
34pub(crate) const KIND_FOLD: &str = "range-finder-fold";
35const MODULE_ID: ModuleId = ModuleId::new("range-finder");
36
37/// Range-finder module providing jump navigation and code folding.
38pub struct RangeFinderModule;
39
40impl RangeFinderModule {
41    /// Create a new instance.
42    #[must_use]
43    pub const fn new() -> Self {
44        Self
45    }
46}
47
48impl Default for RangeFinderModule {
49    fn default() -> Self {
50        Self::new()
51    }
52}
53
54impl Module for RangeFinderModule {
55    fn id(&self) -> ModuleId {
56        MODULE_ID
57    }
58
59    fn name(&self) -> &'static str {
60        "range-finder"
61    }
62
63    fn version(&self) -> Version {
64        Version::new(0, 1, 0)
65    }
66
67    fn dependencies(&self) -> Vec<ModuleId> {
68        vec![]
69    }
70
71    fn optional_dependencies(&self) -> Vec<ModuleId> {
72        // Personality modules (e.g., vim) populate ModeBridgeStore before us.
73        // Optional: range-finder still works without a personality, just no parent mode bridging.
74        vec![ModuleId::new("vim")]
75    }
76
77    #[cfg_attr(coverage_nightly, coverage(off))]
78    fn init(&mut self, ctx: &ModuleContext) -> ProbeResult {
79        // Register bridges (#524)
80        let provider = ctx
81            .services
82            .get_or_create::<reovim_driver_session::bridges::BridgeProvider>();
83        provider.register(jump::bridge::JumpBridge);
84        provider.register(fold::bridge::FoldBridge);
85
86        // Register jump commands (#524)
87        let command_store = ctx
88            .services
89            .get_or_create::<reovim_driver_command::CommandHandlerStore>();
90        for handler in jump::command::all_commands() {
91            command_store.add(handler);
92        }
93
94        // Register fold commands (#524)
95        for handler in fold::command::all_commands() {
96            command_store.add(handler);
97        }
98
99        // #585: Read parent mode from ModeBridgeStore (populated by personality module)
100        // instead of requiring adapter pre-registration.
101        let modes = ctx.services.get_or_create::<ModeInfoStore>();
102        let parent_mode = resolve_parent_mode(ctx, &modes);
103
104        if parent_mode.is_some() {
105            // Register enhanced find-char command (#535) — overrides vim's basic handler
106            command_store.add(Box::new(find_char::EnhancedFindCharCommand));
107        }
108
109        // Register jump resolver for jump-input mode (#524)
110        let resolvers = ctx.services.get_or_create::<ResolverRegistry>();
111        resolvers.register(jump::resolver::JumpResolver::with_parent(
112            parent_mode.clone().unwrap_or(jump::ids::JUMP_INPUT_MODE),
113        ));
114
115        // Register mode info for jump-input mode (#524)
116        modes.add(ModeInfo {
117            id: jump::ids::JUMP_INPUT_MODE,
118            display_name: "JUMP",
119            cursor_style: CursorStyle::Block,
120            accepts_char_input: true,
121            has_selection: false,
122            inherits_from: parent_mode,
123            is_entry: false,
124        });
125
126        // #700: Register keybindings from personality adapters
127        let keybinding_store = ctx.services.get_or_create::<KeybindingStore>();
128        keybinding_store.add_all(self.keybindings());
129
130        ProbeResult::Success
131    }
132
133    #[cfg_attr(coverage_nightly, coverage(off))]
134    fn exit(&mut self) -> Result<(), ModuleError> {
135        Ok(())
136    }
137
138    fn extension_kinds(&self) -> &[&'static str] {
139        &[KIND_JUMP, KIND_FOLD]
140    }
141
142    #[cfg_attr(coverage_nightly, coverage(off))]
143    fn keybindings(&self) -> Vec<KeybindingRegistration> {
144        keybinding::all()
145    }
146}
147
148/// Resolve the parent mode for jump-input from `ModeBridgeStore`.
149///
150/// Returns `None` if no personality module is loaded (reduced functionality).
151#[cfg_attr(coverage_nightly, coverage(off))]
152fn resolve_parent_mode(
153    ctx: &ModuleContext,
154    modes: &ModeInfoStore,
155) -> Option<reovim_kernel::api::v1::ModeId> {
156    let bridge_store = ctx.services.get::<ModeBridgeStore>()?;
157    let parent_str = bridge_store.find_parent("range-finder:jump-input")?;
158    let (module, name) = parent_str.split_once(':')?;
159
160    if let Some(mode_id) = modes.find_by_name(module, name) {
161        return Some(mode_id);
162    }
163    tracing::warn!("Mode bridge parent '{parent_str}' not found in ModeInfoStore");
164    None
165}
166
167#[cfg(feature = "dynamic")]
168reovim_module_macros::declare_module!(RangeFinderModule);
169
170#[cfg(test)]
171#[path = "lib_tests.rs"]
172mod tests;