reovim_plugin_range_finder/
lib.rs

1//! Range-Finder Plugin for reovim
2//!
3//! Unified jump navigation and code folding system:
4//! - Jump navigation: Multi-char search (`s`), enhanced f/t motions
5//! - Code folding: Toggle/open/close folds (`za`, `zo`, `zc`, `zR`, `zM`)
6
7mod fold;
8mod jump;
9
10use std::sync::Arc;
11
12use {
13    jump::input::{InputResult, handle_char_input},
14    reovim_core::{
15        bind::{CommandRef, KeymapScope, SubModeKind},
16        command::id::CommandId,
17        event_bus::{
18            EventBus, EventResult,
19            core_events::{BufferClosed, MotionContext, PluginTextInput, RequestCursorMove},
20        },
21        keys,
22        modd::ComponentId,
23        plugin::{Plugin, PluginContext, PluginId, PluginStateRegistry},
24        subscribe_state, subscribe_state_conditional,
25    },
26};
27
28// Re-export jump types
29pub use jump::{
30    Direction, JumpCancel, JumpExecute, JumpFindChar, JumpFindCharBack, JumpFindCharStarted,
31    JumpInputChar, JumpMode, JumpSearch, JumpSearchStarted, JumpSelectLabel, JumpTillChar,
32    JumpTillCharBack, OperatorContext, OperatorType, SharedJumpState,
33};
34
35// Re-export fold types
36pub use fold::{
37    FoldClose, FoldCloseAll, FoldOpen, FoldOpenAll, FoldRangesUpdated, FoldRenderStage, FoldToggle,
38    SharedFoldManager,
39};
40
41/// Plugin-local command IDs
42pub mod command_id {
43    use super::CommandId;
44
45    // Jump commands
46    pub const JUMP_SEARCH: CommandId = CommandId::new("jump_search");
47    pub const JUMP_FIND_CHAR: CommandId = CommandId::new("jump_find_char");
48    pub const JUMP_FIND_CHAR_BACK: CommandId = CommandId::new("jump_find_char_back");
49    pub const JUMP_TILL_CHAR: CommandId = CommandId::new("jump_till_char");
50    pub const JUMP_TILL_CHAR_BACK: CommandId = CommandId::new("jump_till_char_back");
51    pub const JUMP_CANCEL: CommandId = CommandId::new("jump_cancel");
52
53    // Fold commands
54    pub const FOLD_TOGGLE: CommandId = CommandId::new("fold_toggle");
55    pub const FOLD_OPEN: CommandId = CommandId::new("fold_open");
56    pub const FOLD_CLOSE: CommandId = CommandId::new("fold_close");
57    pub const FOLD_OPEN_ALL: CommandId = CommandId::new("fold_open_all");
58    pub const FOLD_CLOSE_ALL: CommandId = CommandId::new("fold_close_all");
59}
60
61/// Component ID for jump navigation interactor
62pub const JUMP_COMPONENT_ID: ComponentId = ComponentId("range-finder-jump");
63
64/// Unified range-finder plugin combining jump navigation and code folding
65pub struct RangeFinderPlugin {
66    jump_state: Arc<SharedJumpState>,
67    fold_manager: Arc<SharedFoldManager>,
68}
69
70impl RangeFinderPlugin {
71    /// Create a new `RangeFinderPlugin` with default configuration
72    #[must_use]
73    pub fn new() -> Self {
74        Self {
75            jump_state: Arc::new(SharedJumpState::new()),
76            fold_manager: Arc::new(SharedFoldManager::new()),
77        }
78    }
79}
80
81impl Default for RangeFinderPlugin {
82    fn default() -> Self {
83        Self::new()
84    }
85}
86
87impl Plugin for RangeFinderPlugin {
88    fn id(&self) -> PluginId {
89        PluginId::new("range-finder")
90    }
91
92    fn name(&self) -> &'static str {
93        "Range-Finder"
94    }
95
96    fn description(&self) -> &'static str {
97        "Unified jump navigation and code folding"
98    }
99
100    #[allow(clippy::too_many_lines)]
101    fn build(&self, ctx: &mut PluginContext) {
102        // === Register Display Info ===
103        use reovim_core::{
104            display::DisplayInfo,
105            highlight::{Color, Style},
106        };
107
108        // Yellow/gold background for jump navigation (targeting/finding)
109        let gold = Color::Rgb {
110            r: 241,
111            g: 196,
112            b: 15,
113        };
114        let fg = Color::Rgb {
115            r: 33,
116            g: 33,
117            b: 33,
118        };
119        let style = Style::new().fg(fg).bg(gold).bold();
120
121        ctx.register_display(JUMP_COMPONENT_ID, DisplayInfo::new(" JUMP ", " ", style));
122
123        // === Register Jump Commands ===
124        let _ = ctx.register_command(JumpSearch::default_instance());
125        let _ = ctx.register_command(JumpFindChar::default_instance());
126        let _ = ctx.register_command(JumpFindCharBack::default_instance());
127        let _ = ctx.register_command(JumpTillChar::default_instance());
128        let _ = ctx.register_command(JumpTillCharBack::default_instance());
129        let _ = ctx.register_command(JumpCancel);
130
131        // === Register Fold Commands ===
132        let _ = ctx.register_command(FoldToggle::default_instance());
133        let _ = ctx.register_command(FoldOpen::default_instance());
134        let _ = ctx.register_command(FoldClose::default_instance());
135        let _ = ctx.register_command(FoldOpenAll::default_instance());
136        let _ = ctx.register_command(FoldCloseAll::default_instance());
137
138        // === Register Jump Keybindings (Normal mode + Operator-Pending) ===
139        let normal_mode = KeymapScope::editor_normal();
140        let operator_pending = KeymapScope::SubMode(SubModeKind::OperatorPending);
141
142        // Multi-char search (works in both normal and operator-pending)
143        ctx.bind_key_scoped(
144            normal_mode.clone(),
145            keys!['s'],
146            CommandRef::Registered(command_id::JUMP_SEARCH),
147        );
148        ctx.bind_key_scoped(
149            operator_pending.clone(),
150            keys!['s'],
151            CommandRef::Registered(command_id::JUMP_SEARCH),
152        );
153
154        // Enhanced f/t motions (works in both normal and operator-pending)
155        ctx.bind_key_scoped(
156            normal_mode.clone(),
157            keys!['f'],
158            CommandRef::Registered(command_id::JUMP_FIND_CHAR),
159        );
160        ctx.bind_key_scoped(
161            operator_pending.clone(),
162            keys!['f'],
163            CommandRef::Registered(command_id::JUMP_FIND_CHAR),
164        );
165
166        ctx.bind_key_scoped(
167            normal_mode.clone(),
168            keys!['F'],
169            CommandRef::Registered(command_id::JUMP_FIND_CHAR_BACK),
170        );
171        ctx.bind_key_scoped(
172            operator_pending.clone(),
173            keys!['F'],
174            CommandRef::Registered(command_id::JUMP_FIND_CHAR_BACK),
175        );
176
177        ctx.bind_key_scoped(
178            normal_mode.clone(),
179            keys!['t'],
180            CommandRef::Registered(command_id::JUMP_TILL_CHAR),
181        );
182        ctx.bind_key_scoped(
183            operator_pending.clone(),
184            keys!['t'],
185            CommandRef::Registered(command_id::JUMP_TILL_CHAR),
186        );
187
188        ctx.bind_key_scoped(
189            normal_mode.clone(),
190            keys!['T'],
191            CommandRef::Registered(command_id::JUMP_TILL_CHAR_BACK),
192        );
193        ctx.bind_key_scoped(
194            operator_pending,
195            keys!['T'],
196            CommandRef::Registered(command_id::JUMP_TILL_CHAR_BACK),
197        );
198
199        // === Register Fold Keybindings (Normal mode) ===
200        ctx.bind_key_scoped(
201            normal_mode.clone(),
202            keys!['z' 'a'],
203            CommandRef::Registered(command_id::FOLD_TOGGLE),
204        );
205
206        ctx.bind_key_scoped(
207            normal_mode.clone(),
208            keys!['z' 'o'],
209            CommandRef::Registered(command_id::FOLD_OPEN),
210        );
211
212        ctx.bind_key_scoped(
213            normal_mode.clone(),
214            keys!['z' 'c'],
215            CommandRef::Registered(command_id::FOLD_CLOSE),
216        );
217
218        ctx.bind_key_scoped(
219            normal_mode.clone(),
220            keys!['z' 'R'],
221            CommandRef::Registered(command_id::FOLD_OPEN_ALL),
222        );
223
224        ctx.bind_key_scoped(
225            normal_mode,
226            keys!['z' 'M'],
227            CommandRef::Registered(command_id::FOLD_CLOSE_ALL),
228        );
229
230        // === Register Jump Cancel (Interactor mode - for when jump is active) ===
231        // Note: Escape in Interactor mode will be handled by PluginTextInput routing
232
233        tracing::info!("RangeFinderPlugin initialized with jump and fold subsystems");
234    }
235
236    fn init_state(&self, registry: &PluginStateRegistry) {
237        // Register jump state
238        registry.register(Arc::clone(&self.jump_state));
239
240        // Register jump label window
241        registry.register_plugin_window(Arc::new(jump::render::JumpLabelWindow));
242
243        // Register fold manager
244        registry.register(Arc::clone(&self.fold_manager));
245
246        // Set fold manager as visibility source (for hiding folded lines)
247        registry.set_visibility_source(Arc::clone(&self.fold_manager)
248            as Arc<dyn reovim_core::visibility::BufferVisibilitySource>);
249
250        // Register fold render stage
251        registry
252            .register_render_stage(Arc::new(FoldRenderStage::new(Arc::clone(&self.fold_manager))));
253    }
254
255    fn subscribe(&self, bus: &EventBus, _state: Arc<PluginStateRegistry>) {
256        self.subscribe_jump_mode_handlers(bus);
257        self.subscribe_jump_input_handler(bus);
258        self.subscribe_fold_handlers(bus);
259        self.subscribe_cleanup(bus);
260    }
261}
262
263// Event subscription sub-methods
264impl RangeFinderPlugin {
265    /// Subscribe to jump mode entry events (search started, find char started)
266    fn subscribe_jump_mode_handlers(&self, bus: &EventBus) {
267        // JumpSearchStarted - enter Interactor mode for multi-char search
268        let jump_state = Arc::clone(&self.jump_state);
269        bus.subscribe::<JumpSearchStarted, _>(100, move |event, ctx| {
270            // Get operator context from event (set by command handler)
271            let operator_context = event.operator_context;
272
273            // Initialize jump state for multi-char search
274            jump_state.with_mut(|state| {
275                state.start_multi_char(
276                    event.buffer_id,
277                    event.cursor_line,
278                    event.cursor_col,
279                    event.direction,
280                    event.lines.clone(),
281                    operator_context,
282                );
283            });
284
285            // Enter Interactor mode to receive character input
286            ctx.enter_interactor_mode(JUMP_COMPONENT_ID);
287            ctx.request_render();
288
289            tracing::debug!(
290                "Jump multi-char search started, operator_context={:?}",
291                operator_context
292            );
293            EventResult::Handled
294        });
295
296        // JumpFindCharStarted - enter Interactor mode for single-char find/till
297        let jump_state = Arc::clone(&self.jump_state);
298        bus.subscribe::<JumpFindCharStarted, _>(100, move |event, ctx| {
299            // Get operator context from event (set by command handler)
300            let operator_context = event.operator_context;
301
302            // Initialize jump state for single-char search
303            jump_state.with_mut(|state| {
304                state.start_find_char(
305                    event.buffer_id,
306                    event.cursor_line,
307                    event.cursor_col,
308                    event.direction,
309                    event.lines.clone(),
310                    operator_context,
311                );
312            });
313
314            // Enter Interactor mode to receive character input
315            ctx.enter_interactor_mode(JUMP_COMPONENT_ID);
316            ctx.request_render();
317
318            tracing::debug!(
319                "Jump single-char search started (mode: {:?}), operator_context={:?}",
320                event.mode,
321                operator_context
322            );
323            EventResult::Handled
324        });
325
326        // JumpCancel - handle explicit cancel (Escape key)
327        let jump_state = Arc::clone(&self.jump_state);
328        bus.subscribe::<JumpCancel, _>(100, move |_event, ctx| {
329            // Reset jump state
330            jump_state.with_mut(jump::state::JumpState::reset);
331
332            // Exit Interactor mode
333            ctx.exit_to_normal();
334            ctx.request_render();
335
336            tracing::debug!("Jump explicitly canceled");
337            EventResult::Handled
338        });
339    }
340
341    /// Subscribe to jump text input handler (character input during jump mode)
342    fn subscribe_jump_input_handler(&self, bus: &EventBus) {
343        let jump_state = Arc::clone(&self.jump_state);
344        bus.subscribe_targeted::<PluginTextInput, _>(JUMP_COMPONENT_ID, 100, move |event, ctx| {
345            // Handle input through the state machine
346            let result = jump_state.with_mut(|state| {
347                let buffer_id = state.buffer_id.unwrap_or(0);
348                let lines = state.lines.clone().unwrap_or_default();
349                let input_result = handle_char_input(state, event.c, &lines);
350                tracing::debug!("Plugin handler: InputResult = {:?}", input_result);
351                (buffer_id, input_result)
352            });
353
354            match result.1 {
355                InputResult::Continue | InputResult::ShowLabels => {
356                    tracing::debug!(
357                        "Plugin handler: Calling request_render for Continue/ShowLabels"
358                    );
359                    ctx.request_render();
360                    EventResult::Handled
361                }
362                InputResult::Jump(jump_execute) => {
363                    tracing::debug!("Jump execute: {:?}", jump_execute);
364
365                    // Get operator context before resetting state
366                    let operator_context = jump_state.with_mut(|state| state.operator_context);
367
368                    if let Some(op_ctx) = operator_context {
369                        // Jump with operator - pass operator via motion_context
370                        tracing::debug!("Jump with operator: {:?}", op_ctx);
371
372                        // Pass operator directly in motion_context to avoid race condition
373                        ctx.emit(RequestCursorMove {
374                            buffer_id: jump_execute.buffer_id,
375                            line: jump_execute.line,
376                            column: jump_execute.col,
377                            motion_context: Some(MotionContext {
378                                linewise: false,
379                                inclusive: true,
380                                operator: Some(op_ctx.operator.into()),
381                                count: op_ctx.count,
382                            }),
383                        });
384                    } else {
385                        // Normal jump without operator
386                        ctx.emit(RequestCursorMove {
387                            buffer_id: jump_execute.buffer_id,
388                            line: jump_execute.line,
389                            column: jump_execute.col,
390                            motion_context: None,
391                        });
392                    }
393
394                    // Return to Normal mode after jump
395                    ctx.exit_to_normal();
396
397                    // Reset jump state
398                    jump_state.with_mut(jump::state::JumpState::reset);
399                    ctx.request_render();
400
401                    EventResult::Handled
402                }
403                InputResult::Cancel(reason) => {
404                    tracing::debug!("Jump canceled: {:?}", reason);
405
406                    // Reset state and exit Interactor mode
407                    jump_state.with_mut(jump::state::JumpState::reset);
408                    ctx.exit_to_normal();
409                    ctx.request_render();
410
411                    EventResult::Handled
412                }
413            }
414        });
415    }
416
417    /// Subscribe to fold operation events (toggle, open, close, open all, close all, ranges updated)
418    fn subscribe_fold_handlers(&self, bus: &EventBus) {
419        // Conditional render subscriptions (render only if state changed)
420        subscribe_state_conditional!(bus, self.fold_manager, FoldToggle, |m, e| {
421            m.toggle(e.buffer_id, e.line)
422        });
423
424        subscribe_state_conditional!(bus, self.fold_manager, FoldOpen, |m, e| {
425            m.open(e.buffer_id, e.line)
426        });
427
428        subscribe_state_conditional!(bus, self.fold_manager, FoldClose, |m, e| {
429            m.close(e.buffer_id, e.line)
430        });
431
432        // Simple mutation + render subscriptions
433        subscribe_state!(bus, self.fold_manager, FoldOpenAll, |m, e| {
434            m.open_all(e.buffer_id);
435        });
436
437        subscribe_state!(bus, self.fold_manager, FoldCloseAll, |m, e| {
438            m.close_all(e.buffer_id);
439        });
440
441        // FoldRangesUpdated (from treesitter) - uses priority 50
442        let fold_manager = Arc::clone(&self.fold_manager);
443        bus.subscribe::<FoldRangesUpdated, _>(50, move |event, ctx| {
444            fold_manager.with_mut(|m| m.set_ranges(event.buffer_id, event.ranges.clone()));
445            ctx.request_render();
446            EventResult::Handled
447        });
448    }
449
450    /// Subscribe to cleanup events (buffer closed)
451    fn subscribe_cleanup(&self, bus: &EventBus) {
452        let fold_manager = Arc::clone(&self.fold_manager);
453        bus.subscribe::<BufferClosed, _>(100, move |event, _ctx| {
454            fold_manager.with_mut(|m| m.remove_buffer(event.buffer_id));
455            EventResult::Handled
456        });
457    }
458}