reovim_plugin_leap/
lib.rs

1//! Leap motion plugin for reovim
2//!
3//! This plugin provides two-character jump navigation (like leap.nvim/hop.nvim).
4//!
5//! # Architecture Note
6//!
7//! Commands emit `EventBus` events that are handled by the runtime.
8//! Uses `PluginStateRegistry` for state management.
9
10mod commands;
11mod events;
12mod handlers;
13mod state;
14
15use std::{any::TypeId, sync::Arc};
16
17use reovim_core::{
18    bind::{CommandRef, KeymapScope},
19    display::{DisplayInfo, SubModeKey},
20    event_bus::{EventBus, EventResult},
21    frame::FrameBuffer,
22    highlight::{Style, Theme},
23    keys,
24    modd::ComponentId,
25    plugin::{
26        EditorContext, Plugin, PluginContext, PluginId, PluginStateRegistry, PluginWindow, Rect,
27        WindowConfig,
28    },
29};
30
31use reovim_sys::style::Color;
32
33pub use {
34    commands::{
35        LEAP_BACKWARD, LEAP_CANCEL, LEAP_FORWARD, LeapBackwardCommand, LeapCancelCommand,
36        LeapForwardCommand,
37    },
38    events::{
39        LeapCancelEvent, LeapFirstCharEvent, LeapJumpEvent, LeapMatchesFoundEvent,
40        LeapSecondCharEvent, LeapSelectLabelEvent, LeapStartEvent,
41    },
42    handlers::{LeapStartHandler, leap_start_result},
43    state::{LeapDirection, LeapMatch, LeapPhase, LeapState, find_matches, generate_labels},
44};
45
46/// Leap styles for rendering
47///
48/// Since leap styles were removed from core theme, this plugin defines its own.
49#[derive(Debug, Clone)]
50pub struct LeapStyles {
51    pub label: Style,
52    pub match_highlight: Style,
53}
54
55impl Default for LeapStyles {
56    fn default() -> Self {
57        Self {
58            label: Style::new().fg(Color::Black).bg(Color::Yellow).bold(),
59            match_highlight: Style::new().fg(Color::Magenta).bold(),
60        }
61    }
62}
63
64/// Plugin window for leap labels
65pub struct LeapPluginWindow;
66
67impl PluginWindow for LeapPluginWindow {
68    fn window_config(
69        &self,
70        state: &Arc<PluginStateRegistry>,
71        ctx: &EditorContext,
72    ) -> Option<WindowConfig> {
73        let is_showing = state
74            .with::<LeapState, _, _>(|leap| leap.is_showing_labels())
75            .unwrap_or(false);
76
77        if !is_showing {
78            return None;
79        }
80
81        // Leap renders labels across the entire screen
82        Some(WindowConfig {
83            bounds: Rect::new(0, 0, ctx.screen_width, ctx.screen_height),
84            z_order: 100, // Same level as editor windows
85            visible: true,
86        })
87    }
88
89    #[allow(clippy::cast_possible_truncation)]
90    fn render(
91        &self,
92        state: &Arc<PluginStateRegistry>,
93        ctx: &EditorContext,
94        buffer: &mut FrameBuffer,
95        _bounds: Rect,
96        _theme: &Theme,
97    ) {
98        // Use plugin's own leap styles
99        let styles = LeapStyles::default();
100        let label_style = &styles.label;
101
102        state.with::<LeapState, _, _>(|leap| {
103            for m in &leap.matches {
104                let screen_x = m.col;
105                let screen_y = m.line;
106
107                if screen_y >= ctx.screen_height.saturating_sub(1) {
108                    continue;
109                }
110
111                for (i, ch) in m.label.chars().enumerate() {
112                    let x = screen_x + i as u16;
113                    if x < buffer.width() {
114                        buffer.put_char(x, screen_y, ch, label_style);
115                    }
116                }
117            }
118        });
119    }
120}
121
122/// Component ID for leap (used in sub-mode)
123pub const COMPONENT_ID: ComponentId = ComponentId("leap");
124
125/// Leap motion plugin
126///
127/// Provides two-character jump navigation:
128/// - s/S for forward/backward leap
129/// - Character-based label selection
130pub struct LeapPlugin;
131
132impl Plugin for LeapPlugin {
133    fn id(&self) -> PluginId {
134        PluginId::new("reovim:leap")
135    }
136
137    fn name(&self) -> &'static str {
138        "Leap"
139    }
140
141    fn description(&self) -> &'static str {
142        "Two-character jump navigation"
143    }
144
145    fn dependencies(&self) -> Vec<TypeId> {
146        vec![]
147    }
148
149    fn build(&self, ctx: &mut PluginContext) {
150        // Register sub-mode display info
151        ctx.register_sub_mode_display(
152            SubModeKey::Interactor(COMPONENT_ID),
153            DisplayInfo::new(" LEAP ", "\u{f0168} "),
154        );
155
156        // Register commands
157        let _ = ctx.register_command(LeapForwardCommand);
158        let _ = ctx.register_command(LeapBackwardCommand);
159        let _ = ctx.register_command(LeapCancelCommand);
160
161        // Register keybindings (previously in core's bind/mod.rs)
162        ctx.bind_key_scoped(
163            KeymapScope::editor_normal(),
164            keys!['s'],
165            CommandRef::Registered(LEAP_FORWARD),
166        );
167        ctx.bind_key_scoped(
168            KeymapScope::editor_normal(),
169            keys!['S'],
170            CommandRef::Registered(LEAP_BACKWARD),
171        );
172    }
173
174    fn init_state(&self, registry: &PluginStateRegistry) {
175        registry.register(LeapState::new());
176        // Register the plugin window
177        registry.register_plugin_window(Arc::new(LeapPluginWindow));
178        tracing::debug!("LeapPlugin: initialized state in registry");
179    }
180
181    fn subscribe(&self, bus: &EventBus, state: Arc<PluginStateRegistry>) {
182        let state_clone = Arc::clone(&state);
183        bus.subscribe::<LeapStartEvent, _>(100, move |event, _ctx| {
184            tracing::trace!(
185                direction = ?event.direction,
186                operator = ?event.operator,
187                "LeapPlugin: leap mode started via event bus"
188            );
189
190            state_clone.with_mut::<LeapState, _, _>(|leap_state| {
191                leap_state.start(event.direction, event.operator, event.count);
192            });
193
194            EventResult::Handled
195        });
196
197        bus.subscribe::<LeapJumpEvent, _>(100, |event, _ctx| {
198            tracing::trace!(
199                from = ?event.from,
200                to = ?event.to,
201                direction = ?event.direction,
202                "LeapPlugin: jump occurred via event bus"
203            );
204            EventResult::Handled
205        });
206
207        bus.subscribe::<LeapMatchesFoundEvent, _>(100, |event, _ctx| {
208            tracing::trace!(
209                match_count = event.match_count,
210                pattern = %event.pattern,
211                "LeapPlugin: matches found via event bus"
212            );
213            EventResult::Handled
214        });
215
216        let state_clone2 = Arc::clone(&state);
217        bus.subscribe::<LeapCancelEvent, _>(100, move |_event, _ctx| {
218            tracing::trace!("LeapPlugin: leap cancelled via event bus");
219
220            state_clone2.with_mut::<LeapState, _, _>(|leap_state| {
221                leap_state.reset();
222            });
223
224            EventResult::Handled
225        });
226
227        tracing::debug!("LeapPlugin: subscribed to leap events via event bus");
228    }
229}