1mod 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
28pub use jump::{
30 Direction, JumpCancel, JumpExecute, JumpFindChar, JumpFindCharBack, JumpFindCharStarted,
31 JumpInputChar, JumpMode, JumpSearch, JumpSearchStarted, JumpSelectLabel, JumpTillChar,
32 JumpTillCharBack, OperatorContext, OperatorType, SharedJumpState,
33};
34
35pub use fold::{
37 FoldClose, FoldCloseAll, FoldOpen, FoldOpenAll, FoldRangesUpdated, FoldRenderStage, FoldToggle,
38 SharedFoldManager,
39};
40
41pub mod command_id {
43 use super::CommandId;
44
45 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 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
61pub const JUMP_COMPONENT_ID: ComponentId = ComponentId("range-finder-jump");
63
64pub struct RangeFinderPlugin {
66 jump_state: Arc<SharedJumpState>,
67 fold_manager: Arc<SharedFoldManager>,
68}
69
70impl RangeFinderPlugin {
71 #[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 use reovim_core::{
104 display::DisplayInfo,
105 highlight::{Color, Style},
106 };
107
108 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 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 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 let normal_mode = KeymapScope::editor_normal();
140 let operator_pending = KeymapScope::SubMode(SubModeKind::OperatorPending);
141
142 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 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 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 tracing::info!("RangeFinderPlugin initialized with jump and fold subsystems");
234 }
235
236 fn init_state(&self, registry: &PluginStateRegistry) {
237 registry.register(Arc::clone(&self.jump_state));
239
240 registry.register_plugin_window(Arc::new(jump::render::JumpLabelWindow));
242
243 registry.register(Arc::clone(&self.fold_manager));
245
246 registry.set_visibility_source(Arc::clone(&self.fold_manager)
248 as Arc<dyn reovim_core::visibility::BufferVisibilitySource>);
249
250 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
263impl RangeFinderPlugin {
265 fn subscribe_jump_mode_handlers(&self, bus: &EventBus) {
267 let jump_state = Arc::clone(&self.jump_state);
269 bus.subscribe::<JumpSearchStarted, _>(100, move |event, ctx| {
270 let operator_context = event.operator_context;
272
273 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 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 let jump_state = Arc::clone(&self.jump_state);
298 bus.subscribe::<JumpFindCharStarted, _>(100, move |event, ctx| {
299 let operator_context = event.operator_context;
301
302 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 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 let jump_state = Arc::clone(&self.jump_state);
328 bus.subscribe::<JumpCancel, _>(100, move |_event, ctx| {
329 jump_state.with_mut(jump::state::JumpState::reset);
331
332 ctx.exit_to_normal();
334 ctx.request_render();
335
336 tracing::debug!("Jump explicitly canceled");
337 EventResult::Handled
338 });
339 }
340
341 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 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 let operator_context = jump_state.with_mut(|state| state.operator_context);
367
368 if let Some(op_ctx) = operator_context {
369 tracing::debug!("Jump with operator: {:?}", op_ctx);
371
372 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 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 ctx.exit_to_normal();
396
397 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 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 fn subscribe_fold_handlers(&self, bus: &EventBus) {
419 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 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 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 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}