1mod 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#[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
64pub 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 Some(WindowConfig {
83 bounds: Rect::new(0, 0, ctx.screen_width, ctx.screen_height),
84 z_order: 100, 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 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
122pub const COMPONENT_ID: ComponentId = ComponentId("leap");
124
125pub 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 ctx.register_sub_mode_display(
152 SubModeKey::Interactor(COMPONENT_ID),
153 DisplayInfo::new(" LEAP ", "\u{f0168} "),
154 );
155
156 let _ = ctx.register_command(LeapForwardCommand);
158 let _ = ctx.register_command(LeapBackwardCommand);
159 let _ = ctx.register_command(LeapCancelCommand);
160
161 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 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}