reovim_plugin_microscope/
lib.rs

1//! Microscope fuzzy finder plugin for reovim
2//!
3//! This plugin provides fuzzy finding capabilities:
4//! - File picker (Space ff)
5//! - Buffer picker (Space fb)
6//! - Grep picker (Space fg)
7//! - Command palette
8//! - Themes picker
9//! - Keymaps viewer
10//! - And more...
11//!
12//! # Architecture
13//!
14//! Commands emit `EventBus` events that are handled by the runtime.
15//! State is managed via `PluginStateRegistry`.
16//! Rendering is done via the `PluginWindow` trait.
17
18pub mod commands;
19pub mod microscope;
20
21use std::{any::TypeId, sync::Arc};
22
23use reovim_core::{
24    bind::{CommandRef, EditModeKind, KeyMapInner, KeymapScope, SubModeKind},
25    display::{DisplayInfo, EditModeKey},
26    frame::FrameBuffer,
27    highlight::Theme,
28    keys,
29    modd::ComponentId,
30    plugin::{
31        EditorContext, Plugin, PluginContext, PluginId, PluginStateRegistry, PluginWindow, Rect,
32        WindowConfig,
33    },
34    rpc::{RpcHandler, RpcHandlerContext, RpcResult},
35};
36
37// Re-export unified command-event types
38pub use commands::{
39    MicroscopeBackspace, MicroscopeClearQuery, MicroscopeClose, MicroscopeCommands,
40    MicroscopeConfirm, MicroscopeCursorEnd, MicroscopeCursorLeft, MicroscopeCursorRight,
41    MicroscopeCursorStart, MicroscopeDeleteWord, MicroscopeEnterInsert, MicroscopeEnterNormal,
42    MicroscopeFindBuffers, MicroscopeFindFiles, MicroscopeFindRecent, MicroscopeGotoFirst,
43    MicroscopeGotoLast, MicroscopeHelp, MicroscopeInsertChar, MicroscopeKeymaps,
44    MicroscopeLiveGrep, MicroscopeOpen, MicroscopePageDown, MicroscopePageUp, MicroscopeProfiles,
45    MicroscopeSelectNext, MicroscopeSelectPrev, MicroscopeThemes, MicroscopeWordBackward,
46    MicroscopeWordForward,
47};
48
49// Re-export microscope types (non-command/event)
50pub use microscope::{
51    BufferInfo, LayoutBounds, LayoutConfig, LoadingState, MatcherItem, MatcherStatus,
52    MicroscopeAction, MicroscopeData, MicroscopeItem, MicroscopeMatcher, MicroscopeState,
53    PanelBounds, Picker, PickerContext, PickerRegistry, PreviewContent, PromptMode, push_item,
54    push_items,
55};
56
57/// Plugin window for microscope (Helix-style bottom-anchored layout)
58pub struct MicroscopePluginWindow;
59
60impl PluginWindow for MicroscopePluginWindow {
61    fn window_config(
62        &self,
63        state: &Arc<PluginStateRegistry>,
64        ctx: &EditorContext,
65    ) -> Option<WindowConfig> {
66        // First check if active
67        let is_active = state
68            .with::<MicroscopeState, _, _>(|s| s.active)
69            .unwrap_or(false);
70
71        if !is_active {
72            return None;
73        }
74
75        // Calculate and update layout bounds based on current screen dimensions
76        state.with_mut::<MicroscopeState, _, _>(|s| {
77            s.update_bounds(ctx.screen_width, ctx.screen_height);
78        });
79
80        // Now get the updated bounds
81        state.with::<MicroscopeState, _, _>(|microscope| {
82            let total = &microscope.bounds.total;
83            Some(WindowConfig {
84                bounds: Rect::new(total.x, total.y, total.width, total.height),
85                z_order: 300, // Floating picker
86                visible: true,
87            })
88        })?
89    }
90
91    #[allow(clippy::cast_possible_truncation, clippy::too_many_lines)]
92    fn render(
93        &self,
94        state: &Arc<PluginStateRegistry>,
95        _ctx: &EditorContext,
96        buffer: &mut FrameBuffer,
97        _bounds: Rect,
98        theme: &Theme,
99    ) {
100        let Some(microscope) = state.with::<MicroscopeState, _, _>(Clone::clone) else {
101            return;
102        };
103
104        let border_style = &theme.popup.border;
105        let normal_style = &theme.popup.normal;
106        let selected_style = &theme.popup.selected;
107
108        let results = microscope.bounds.results;
109        let status = microscope.bounds.status;
110
111        // === Render Results Panel ===
112        Self::render_results_panel(
113            buffer,
114            &microscope,
115            &results,
116            border_style,
117            normal_style,
118            selected_style,
119        );
120
121        // === Render Preview Panel (if enabled) ===
122        if let Some(preview_bounds) = microscope.bounds.preview {
123            Self::render_preview_panel(
124                buffer,
125                &microscope,
126                &preview_bounds,
127                border_style,
128                normal_style,
129                selected_style, // Use selected style for highlight_line
130            );
131        }
132
133        // === Render Status Line ===
134        Self::render_status_line(buffer, &microscope, &status, border_style, normal_style);
135    }
136}
137
138impl MicroscopePluginWindow {
139    /// Render the results panel (left side)
140    #[allow(clippy::cast_possible_truncation)]
141    fn render_results_panel(
142        buffer: &mut FrameBuffer,
143        microscope: &MicroscopeState,
144        bounds: &PanelBounds,
145        border_style: &reovim_core::highlight::Style,
146        normal_style: &reovim_core::highlight::Style,
147        selected_style: &reovim_core::highlight::Style,
148    ) {
149        let x = bounds.x;
150        let y = bounds.y;
151        let width = bounds.width;
152        let height = bounds.height;
153
154        // Top border with title
155        buffer.put_char(x, y, '╭', border_style);
156        let title = format!(" {} ", microscope.title);
157        for (i, ch) in title.chars().enumerate() {
158            let cx = x + 1 + i as u16;
159            if cx < x + width - 1 {
160                buffer.put_char(cx, y, ch, border_style);
161            }
162        }
163        for cx in (x + 1 + title.len() as u16)..(x + width - 1) {
164            buffer.put_char(cx, y, '─', border_style);
165        }
166        buffer.put_char(x + width - 1, y, '╮', border_style);
167
168        // Input line with prompt
169        let input_y = y + 1;
170        buffer.put_char(x, input_y, '│', border_style);
171        buffer.put_char(x + 1, input_y, '>', normal_style);
172        buffer.put_char(x + 2, input_y, ' ', normal_style);
173
174        // Query text with cursor position indicator
175        for (i, ch) in microscope.query.chars().enumerate() {
176            let cx = x + 3 + i as u16;
177            if cx < x + width - 1 {
178                buffer.put_char(cx, input_y, ch, normal_style);
179            }
180        }
181        // Fill remaining space
182        for cx in (x + 3 + microscope.query.len() as u16)..(x + width - 1) {
183            buffer.put_char(cx, input_y, ' ', normal_style);
184        }
185        buffer.put_char(x + width - 1, input_y, '│', border_style);
186
187        // Separator line
188        let sep_y = y + 2;
189        buffer.put_char(x, sep_y, '├', border_style);
190        for cx in (x + 1)..(x + width - 1) {
191            buffer.put_char(cx, sep_y, '─', border_style);
192        }
193        buffer.put_char(x + width - 1, sep_y, '┤', border_style);
194
195        // Results list
196        let results_start = y + 3;
197        let visible_items = microscope.visible_items();
198        let max_results = height.saturating_sub(4) as usize;
199        let selected_in_view = microscope
200            .selected_index
201            .saturating_sub(microscope.scroll_offset);
202
203        for (idx, item) in visible_items.iter().take(max_results).enumerate() {
204            let ry = results_start + idx as u16;
205            let is_selected = idx == selected_in_view;
206            let style = if is_selected {
207                selected_style
208            } else {
209                normal_style
210            };
211
212            buffer.put_char(x, ry, '│', border_style);
213
214            // Selection indicator
215            let indicator = if is_selected { '>' } else { ' ' };
216            buffer.put_char(x + 1, ry, indicator, style);
217            buffer.put_char(x + 2, ry, ' ', style);
218
219            // Item icon if present
220            let mut text_start = x + 3;
221            if let Some(icon) = item.icon {
222                buffer.put_char(text_start, ry, icon, style);
223                buffer.put_char(text_start + 1, ry, ' ', style);
224                text_start += 2;
225            }
226
227            // Item display text
228            for (i, ch) in item.display.chars().enumerate() {
229                let cx = text_start + i as u16;
230                if cx < x + width - 1 {
231                    buffer.put_char(cx, ry, ch, style);
232                }
233            }
234
235            // Fill remaining space
236            let text_end = text_start + item.display.len() as u16;
237            for cx in text_end..(x + width - 1) {
238                buffer.put_char(cx, ry, ' ', style);
239            }
240
241            buffer.put_char(x + width - 1, ry, '│', border_style);
242        }
243
244        // Empty rows
245        let items_shown = visible_items.len().min(max_results) as u16;
246        for ry in (results_start + items_shown)..(y + height - 1) {
247            buffer.put_char(x, ry, '│', border_style);
248            for cx in (x + 1)..(x + width - 1) {
249                buffer.put_char(cx, ry, ' ', normal_style);
250            }
251            buffer.put_char(x + width - 1, ry, '│', border_style);
252        }
253
254        // Bottom border
255        let bottom_y = y + height - 1;
256        buffer.put_char(x, bottom_y, '╰', border_style);
257        for cx in (x + 1)..(x + width - 1) {
258            buffer.put_char(cx, bottom_y, '─', border_style);
259        }
260        buffer.put_char(x + width - 1, bottom_y, '┴', border_style);
261    }
262
263    /// Render the preview panel (right side)
264    #[allow(clippy::cast_possible_truncation)]
265    fn render_preview_panel(
266        buffer: &mut FrameBuffer,
267        microscope: &MicroscopeState,
268        bounds: &PanelBounds,
269        border_style: &reovim_core::highlight::Style,
270        normal_style: &reovim_core::highlight::Style,
271        highlight_style: &reovim_core::highlight::Style,
272    ) {
273        let x = bounds.x;
274        let y = bounds.y;
275        let width = bounds.width;
276        let height = bounds.height;
277
278        // Top border with preview title
279        buffer.put_char(x, y, '┬', border_style);
280        let title = microscope
281            .preview
282            .as_ref()
283            .and_then(|p| p.title.as_deref())
284            .unwrap_or(" Preview ");
285        let title = format!(" {title} ");
286        for (i, ch) in title.chars().enumerate() {
287            let cx = x + 1 + i as u16;
288            if cx < x + width - 1 {
289                buffer.put_char(cx, y, ch, border_style);
290            }
291        }
292        for cx in (x + 1 + title.len() as u16)..(x + width - 1) {
293            buffer.put_char(cx, y, '─', border_style);
294        }
295        buffer.put_char(x + width - 1, y, '╮', border_style);
296
297        // Preview content
298        let content_start = y + 1;
299        let max_lines = height.saturating_sub(2) as usize;
300
301        if let Some(preview) = &microscope.preview {
302            // Check if we have styled_lines for syntax highlighting
303            let styled_lines = preview.styled_lines.as_ref();
304
305            for (idx, line) in preview.lines.iter().take(max_lines).enumerate() {
306                let ry = content_start + idx as u16;
307                buffer.put_char(x, ry, '│', border_style);
308
309                // Check if this line should be highlighted
310                let is_highlight_line = preview.highlight_line == Some(idx);
311                let base_style = if is_highlight_line {
312                    highlight_style
313                } else {
314                    normal_style
315                };
316
317                // Get styled spans for this line if available
318                let line_spans = styled_lines.and_then(|lines| lines.get(idx));
319
320                // Render each character with appropriate style
321                let mut byte_offset = 0;
322                for (i, ch) in line.chars().enumerate() {
323                    let cx = x + 1 + i as u16;
324                    if cx < x + width - 1 {
325                        // Find style for this character position
326                        let char_style = if let Some(spans) = line_spans {
327                            // Look for a span that covers this byte offset
328                            spans
329                                .iter()
330                                .find(|span| byte_offset >= span.start && byte_offset < span.end)
331                                .map(|span| &span.style)
332                                .unwrap_or(base_style)
333                        } else {
334                            base_style
335                        };
336                        buffer.put_char(cx, ry, ch, char_style);
337                    }
338                    byte_offset += ch.len_utf8();
339                }
340
341                // Fill remaining space
342                let line_end = x + 1 + line.chars().count().min((width - 2) as usize) as u16;
343                for cx in line_end..(x + width - 1) {
344                    buffer.put_char(cx, ry, ' ', base_style);
345                }
346
347                buffer.put_char(x + width - 1, ry, '│', border_style);
348            }
349
350            // Empty rows after content
351            let lines_shown = preview.lines.len().min(max_lines) as u16;
352            for ry in (content_start + lines_shown)..(y + height - 1) {
353                buffer.put_char(x, ry, '│', border_style);
354                for cx in (x + 1)..(x + width - 1) {
355                    buffer.put_char(cx, ry, ' ', normal_style);
356                }
357                buffer.put_char(x + width - 1, ry, '│', border_style);
358            }
359        } else {
360            // No preview content
361            for ry in content_start..(y + height - 1) {
362                buffer.put_char(x, ry, '│', border_style);
363                for cx in (x + 1)..(x + width - 1) {
364                    buffer.put_char(cx, ry, ' ', normal_style);
365                }
366                buffer.put_char(x + width - 1, ry, '│', border_style);
367            }
368        }
369
370        // Bottom border
371        let bottom_y = y + height - 1;
372        buffer.put_char(x, bottom_y, '┴', border_style);
373        for cx in (x + 1)..(x + width - 1) {
374            buffer.put_char(cx, bottom_y, '─', border_style);
375        }
376        buffer.put_char(x + width - 1, bottom_y, '╯', border_style);
377    }
378
379    /// Render the status line (bottom)
380    #[allow(clippy::cast_possible_truncation)]
381    fn render_status_line(
382        buffer: &mut FrameBuffer,
383        microscope: &MicroscopeState,
384        bounds: &PanelBounds,
385        _border_style: &reovim_core::highlight::Style,
386        normal_style: &reovim_core::highlight::Style,
387    ) {
388        let x = bounds.x;
389        let y = bounds.y;
390        let width = bounds.width;
391
392        // Status text on left
393        let status = microscope.status_text();
394        for (i, ch) in status.chars().enumerate() {
395            let cx = x + i as u16;
396            if cx < x + width {
397                buffer.put_char(cx, y, ch, normal_style);
398            }
399        }
400
401        // Help text on right
402        let help = "<CR> Confirm | <Esc> Close";
403        let help_start = (x + width).saturating_sub(help.len() as u16);
404        for (i, ch) in help.chars().enumerate() {
405            let cx = help_start + i as u16;
406            if cx >= x + status.len() as u16 && cx < x + width {
407                buffer.put_char(cx, y, ch, normal_style);
408            }
409        }
410
411        // Fill gap
412        for cx in (x + status.len() as u16)..help_start {
413            buffer.put_char(cx, y, ' ', normal_style);
414        }
415    }
416}
417
418/// Component ID for microscope
419pub const COMPONENT_ID: ComponentId = ComponentId("microscope");
420
421/// Command IDs for microscope
422pub mod command_id {
423    use reovim_core::command::CommandId;
424
425    // Picker opening commands
426    pub const MICROSCOPE_FIND_FILES: CommandId = CommandId::new("microscope_find_files");
427    pub const MICROSCOPE_FIND_BUFFERS: CommandId = CommandId::new("microscope_find_buffers");
428    pub const MICROSCOPE_LIVE_GREP: CommandId = CommandId::new("microscope_live_grep");
429    pub const MICROSCOPE_RECENT_FILES: CommandId = CommandId::new("microscope_recent_files");
430    pub const MICROSCOPE_COMMANDS: CommandId = CommandId::new("microscope_commands");
431    pub const MICROSCOPE_HELP_TAGS: CommandId = CommandId::new("microscope_help_tags");
432    pub const MICROSCOPE_KEYMAPS: CommandId = CommandId::new("microscope_keymaps");
433    pub const MICROSCOPE_THEMES: CommandId = CommandId::new("microscope_themes");
434
435    // Navigation commands
436    pub const MICROSCOPE_SELECT_NEXT: CommandId = CommandId::new("microscope_select_next");
437    pub const MICROSCOPE_SELECT_PREV: CommandId = CommandId::new("microscope_select_prev");
438    pub const MICROSCOPE_PAGE_DOWN: CommandId = CommandId::new("microscope_page_down");
439    pub const MICROSCOPE_PAGE_UP: CommandId = CommandId::new("microscope_page_up");
440    pub const MICROSCOPE_GOTO_FIRST: CommandId = CommandId::new("microscope_goto_first");
441    pub const MICROSCOPE_GOTO_LAST: CommandId = CommandId::new("microscope_goto_last");
442
443    // Action commands
444    pub const MICROSCOPE_CONFIRM: CommandId = CommandId::new("microscope_confirm");
445    pub const MICROSCOPE_CLOSE: CommandId = CommandId::new("microscope_close");
446    pub const MICROSCOPE_BACKSPACE: CommandId = CommandId::new("microscope_backspace");
447
448    // Mode commands
449    pub const MICROSCOPE_ENTER_INSERT: CommandId = CommandId::new("microscope_enter_insert");
450    pub const MICROSCOPE_ENTER_NORMAL: CommandId = CommandId::new("microscope_enter_normal");
451
452    // Prompt cursor commands
453    pub const MICROSCOPE_CURSOR_LEFT: CommandId = CommandId::new("microscope_cursor_left");
454    pub const MICROSCOPE_CURSOR_RIGHT: CommandId = CommandId::new("microscope_cursor_right");
455    pub const MICROSCOPE_CURSOR_START: CommandId = CommandId::new("microscope_cursor_start");
456    pub const MICROSCOPE_CURSOR_END: CommandId = CommandId::new("microscope_cursor_end");
457    pub const MICROSCOPE_WORD_FORWARD: CommandId = CommandId::new("microscope_word_forward");
458    pub const MICROSCOPE_WORD_BACKWARD: CommandId = CommandId::new("microscope_word_backward");
459    pub const MICROSCOPE_CLEAR_QUERY: CommandId = CommandId::new("microscope_clear_query");
460    pub const MICROSCOPE_DELETE_WORD: CommandId = CommandId::new("microscope_delete_word");
461}
462
463/// Microscope fuzzy finder plugin
464///
465/// Provides fuzzy finding capabilities:
466/// - File picker (Space ff)
467/// - Buffer picker (Space fb)
468/// - Grep picker (Space fg)
469/// - Command palette
470/// - And more...
471pub struct MicroscopePlugin;
472
473/// RPC handler for state/microscope queries
474struct MicroscopeStateHandler;
475
476impl RpcHandler for MicroscopeStateHandler {
477    fn method(&self) -> &'static str {
478        "state/microscope"
479    }
480
481    fn handle(&self, _params: &serde_json::Value, ctx: &RpcHandlerContext) -> RpcResult {
482        let state = ctx
483            .with_state::<MicroscopeState, _, _>(|s| {
484                serde_json::json!({
485                    "active": s.active,
486                    "query": s.query,
487                    "selected_index": s.selected_index,
488                    "item_count": s.items.len(),
489                    "picker_name": s.picker_name,
490                    "title": s.title,
491                    "selected_item": s.selected_item().map(|i| i.display.clone()),
492                    "prompt_mode": match s.prompt_mode {
493                        PromptMode::Insert => "Insert",
494                        PromptMode::Normal => "Normal",
495                    },
496                })
497            })
498            .unwrap_or_else(|| {
499                serde_json::json!({
500                    "active": false,
501                    "query": "",
502                    "selected_index": 0,
503                    "item_count": 0,
504                    "picker_name": "",
505                    "title": "",
506                    "selected_item": null,
507                    "prompt_mode": "Insert",
508                })
509            });
510        RpcResult::Success(state)
511    }
512
513    fn description(&self) -> &'static str {
514        "Get microscope fuzzy finder state"
515    }
516}
517
518impl Plugin for MicroscopePlugin {
519    fn id(&self) -> PluginId {
520        PluginId::new("reovim:microscope")
521    }
522
523    fn name(&self) -> &'static str {
524        "Microscope"
525    }
526
527    fn description(&self) -> &'static str {
528        "Fuzzy finder: files, buffers, grep, commands"
529    }
530
531    fn dependencies(&self) -> Vec<TypeId> {
532        vec![]
533    }
534
535    fn build(&self, ctx: &mut PluginContext) {
536        // Register display info for status line
537        ctx.register_display(COMPONENT_ID, DisplayInfo::new(" MICROSCOPE ", "󰍉 "));
538        ctx.register_component_mode_display(
539            COMPONENT_ID,
540            EditModeKey::Normal,
541            DisplayInfo::new(" MICROSCOPE ", "󰍉 "),
542        );
543        ctx.register_component_mode_display(
544            COMPONENT_ID,
545            EditModeKey::Insert,
546            DisplayInfo::new(" MICROSCOPE | INSERT ", "󰍉 "),
547        );
548
549        // Register picker opening commands
550        let _ = ctx.register_command(MicroscopeFindFiles);
551        let _ = ctx.register_command(MicroscopeFindBuffers);
552        let _ = ctx.register_command(MicroscopeLiveGrep);
553        let _ = ctx.register_command(MicroscopeFindRecent);
554        let _ = ctx.register_command(MicroscopeCommands);
555        let _ = ctx.register_command(MicroscopeHelp);
556        let _ = ctx.register_command(MicroscopeKeymaps);
557        let _ = ctx.register_command(MicroscopeThemes);
558        let _ = ctx.register_command(MicroscopeProfiles);
559
560        // Register navigation commands
561        let _ = ctx.register_command(MicroscopeSelectNext);
562        let _ = ctx.register_command(MicroscopeSelectPrev);
563        let _ = ctx.register_command(MicroscopePageDown);
564        let _ = ctx.register_command(MicroscopePageUp);
565        let _ = ctx.register_command(MicroscopeGotoFirst);
566        let _ = ctx.register_command(MicroscopeGotoLast);
567
568        // Register action commands
569        let _ = ctx.register_command(MicroscopeConfirm);
570        let _ = ctx.register_command(MicroscopeClose);
571        let _ = ctx.register_command(MicroscopeBackspace);
572
573        // Register mode commands
574        let _ = ctx.register_command(MicroscopeEnterInsert);
575        let _ = ctx.register_command(MicroscopeEnterNormal);
576
577        // Register prompt cursor commands
578        let _ = ctx.register_command(MicroscopeCursorLeft);
579        let _ = ctx.register_command(MicroscopeCursorRight);
580        let _ = ctx.register_command(MicroscopeCursorStart);
581        let _ = ctx.register_command(MicroscopeCursorEnd);
582        let _ = ctx.register_command(MicroscopeWordForward);
583        let _ = ctx.register_command(MicroscopeWordBackward);
584        let _ = ctx.register_command(MicroscopeClearQuery);
585        let _ = ctx.register_command(MicroscopeDeleteWord);
586
587        // Register keybindings (editor normal mode)
588        let editor_normal = KeymapScope::editor_normal();
589
590        // Register Space+f prefix for multi-key sequences
591        ctx.keymap_mut()
592            .get_scope_mut(editor_normal.clone())
593            .insert(keys![Space 'f'], KeyMapInner::with_description("+find").with_category("find"));
594
595        ctx.bind_key_scoped(
596            editor_normal.clone(),
597            keys![Space 'f' 'f'],
598            CommandRef::Registered(command_id::MICROSCOPE_FIND_FILES),
599        );
600        ctx.bind_key_scoped(
601            editor_normal.clone(),
602            keys![Space 'f' 'b'],
603            CommandRef::Registered(command_id::MICROSCOPE_FIND_BUFFERS),
604        );
605        ctx.bind_key_scoped(
606            editor_normal.clone(),
607            keys![Space 'f' 'g'],
608            CommandRef::Registered(command_id::MICROSCOPE_LIVE_GREP),
609        );
610        ctx.bind_key_scoped(
611            editor_normal.clone(),
612            keys![Space 'f' 'r'],
613            CommandRef::Registered(command_id::MICROSCOPE_RECENT_FILES),
614        );
615        ctx.bind_key_scoped(
616            editor_normal.clone(),
617            keys![Space 'f' 'c'],
618            CommandRef::Registered(command_id::MICROSCOPE_COMMANDS),
619        );
620        ctx.bind_key_scoped(
621            editor_normal.clone(),
622            keys![Space 'f' 'h'],
623            CommandRef::Registered(command_id::MICROSCOPE_HELP_TAGS),
624        );
625        ctx.bind_key_scoped(
626            editor_normal,
627            keys![Space 'f' 'k'],
628            CommandRef::Registered(command_id::MICROSCOPE_KEYMAPS),
629        );
630
631        // Register keybindings for when microscope is focused (Normal mode)
632        let microscope_normal = KeymapScope::Component {
633            id: COMPONENT_ID,
634            mode: EditModeKind::Normal,
635        };
636
637        // Navigation
638        ctx.bind_key_scoped(
639            microscope_normal.clone(),
640            keys!['j'],
641            CommandRef::Registered(command_id::MICROSCOPE_SELECT_NEXT),
642        );
643        ctx.bind_key_scoped(
644            microscope_normal.clone(),
645            keys!['k'],
646            CommandRef::Registered(command_id::MICROSCOPE_SELECT_PREV),
647        );
648        ctx.bind_key_scoped(
649            microscope_normal.clone(),
650            keys![(Ctrl 'n')],
651            CommandRef::Registered(command_id::MICROSCOPE_SELECT_NEXT),
652        );
653        ctx.bind_key_scoped(
654            microscope_normal.clone(),
655            keys![(Ctrl 'p')],
656            CommandRef::Registered(command_id::MICROSCOPE_SELECT_PREV),
657        );
658        ctx.bind_key_scoped(
659            microscope_normal.clone(),
660            keys![(Ctrl 'd')],
661            CommandRef::Registered(command_id::MICROSCOPE_PAGE_DOWN),
662        );
663        ctx.bind_key_scoped(
664            microscope_normal.clone(),
665            keys![(Ctrl 'u')],
666            CommandRef::Registered(command_id::MICROSCOPE_PAGE_UP),
667        );
668        ctx.bind_key_scoped(
669            microscope_normal.clone(),
670            keys!['g' 'g'],
671            CommandRef::Registered(command_id::MICROSCOPE_GOTO_FIRST),
672        );
673        ctx.bind_key_scoped(
674            microscope_normal.clone(),
675            keys!['G'],
676            CommandRef::Registered(command_id::MICROSCOPE_GOTO_LAST),
677        );
678
679        // Actions
680        ctx.bind_key_scoped(
681            microscope_normal.clone(),
682            keys![Escape],
683            CommandRef::Registered(command_id::MICROSCOPE_CLOSE),
684        );
685        ctx.bind_key_scoped(
686            microscope_normal.clone(),
687            keys!['q'],
688            CommandRef::Registered(command_id::MICROSCOPE_CLOSE),
689        );
690        ctx.bind_key_scoped(
691            microscope_normal.clone(),
692            keys![Enter],
693            CommandRef::Registered(command_id::MICROSCOPE_CONFIRM),
694        );
695
696        // Mode switching
697        ctx.bind_key_scoped(
698            microscope_normal.clone(),
699            keys!['i'],
700            CommandRef::Registered(command_id::MICROSCOPE_ENTER_INSERT),
701        );
702        ctx.bind_key_scoped(
703            microscope_normal.clone(),
704            keys!['a'],
705            CommandRef::Registered(command_id::MICROSCOPE_ENTER_INSERT),
706        );
707
708        // Prompt cursor movement (Normal mode)
709        ctx.bind_key_scoped(
710            microscope_normal.clone(),
711            keys!['h'],
712            CommandRef::Registered(command_id::MICROSCOPE_CURSOR_LEFT),
713        );
714        ctx.bind_key_scoped(
715            microscope_normal.clone(),
716            keys!['l'],
717            CommandRef::Registered(command_id::MICROSCOPE_CURSOR_RIGHT),
718        );
719        ctx.bind_key_scoped(
720            microscope_normal.clone(),
721            keys!['0'],
722            CommandRef::Registered(command_id::MICROSCOPE_CURSOR_START),
723        );
724        ctx.bind_key_scoped(
725            microscope_normal.clone(),
726            keys!['$'],
727            CommandRef::Registered(command_id::MICROSCOPE_CURSOR_END),
728        );
729        ctx.bind_key_scoped(
730            microscope_normal.clone(),
731            keys!['w'],
732            CommandRef::Registered(command_id::MICROSCOPE_WORD_FORWARD),
733        );
734        ctx.bind_key_scoped(
735            microscope_normal.clone(),
736            keys!['b'],
737            CommandRef::Registered(command_id::MICROSCOPE_WORD_BACKWARD),
738        );
739
740        // Register g as prefix for gg
741        ctx.keymap_mut()
742            .get_scope_mut(microscope_normal)
743            .insert(keys!['g'], KeyMapInner::new());
744
745        // Register keybindings for microscope Insert mode
746        let microscope_insert = KeymapScope::Component {
747            id: COMPONENT_ID,
748            mode: EditModeKind::Insert,
749        };
750
751        // Navigation in insert mode
752        ctx.bind_key_scoped(
753            microscope_insert.clone(),
754            keys![(Ctrl 'n')],
755            CommandRef::Registered(command_id::MICROSCOPE_SELECT_NEXT),
756        );
757        ctx.bind_key_scoped(
758            microscope_insert.clone(),
759            keys![(Ctrl 'p')],
760            CommandRef::Registered(command_id::MICROSCOPE_SELECT_PREV),
761        );
762
763        // Editing in insert mode
764        ctx.bind_key_scoped(
765            microscope_insert.clone(),
766            keys![Backspace],
767            CommandRef::Registered(command_id::MICROSCOPE_BACKSPACE),
768        );
769        ctx.bind_key_scoped(
770            microscope_insert.clone(),
771            keys![(Ctrl 'u')],
772            CommandRef::Registered(command_id::MICROSCOPE_CLEAR_QUERY),
773        );
774        ctx.bind_key_scoped(
775            microscope_insert.clone(),
776            keys![(Ctrl 'w')],
777            CommandRef::Registered(command_id::MICROSCOPE_DELETE_WORD),
778        );
779
780        // Actions in insert mode
781        ctx.bind_key_scoped(
782            microscope_insert.clone(),
783            keys![Escape],
784            CommandRef::Registered(command_id::MICROSCOPE_ENTER_NORMAL),
785        );
786        ctx.bind_key_scoped(
787            microscope_insert,
788            keys![Enter],
789            CommandRef::Registered(command_id::MICROSCOPE_CONFIRM),
790        );
791
792        // Register keybindings for Interactor sub-mode (text input mode)
793        // This is used when microscope is in insert mode for capturing text
794        let microscope_interactor = KeymapScope::SubMode(SubModeKind::Interactor(COMPONENT_ID));
795
796        // Navigation in interactor mode
797        ctx.bind_key_scoped(
798            microscope_interactor.clone(),
799            keys![(Ctrl 'n')],
800            CommandRef::Registered(command_id::MICROSCOPE_SELECT_NEXT),
801        );
802        ctx.bind_key_scoped(
803            microscope_interactor.clone(),
804            keys![(Ctrl 'p')],
805            CommandRef::Registered(command_id::MICROSCOPE_SELECT_PREV),
806        );
807
808        // Editing in interactor mode
809        ctx.bind_key_scoped(
810            microscope_interactor.clone(),
811            keys![Backspace],
812            CommandRef::Registered(command_id::MICROSCOPE_BACKSPACE),
813        );
814        ctx.bind_key_scoped(
815            microscope_interactor.clone(),
816            keys![(Ctrl 'u')],
817            CommandRef::Registered(command_id::MICROSCOPE_CLEAR_QUERY),
818        );
819        ctx.bind_key_scoped(
820            microscope_interactor.clone(),
821            keys![(Ctrl 'w')],
822            CommandRef::Registered(command_id::MICROSCOPE_DELETE_WORD),
823        );
824
825        // Actions in interactor mode
826        ctx.bind_key_scoped(
827            microscope_interactor.clone(),
828            keys![Escape],
829            CommandRef::Registered(command_id::MICROSCOPE_ENTER_NORMAL),
830        );
831        ctx.bind_key_scoped(
832            microscope_interactor,
833            keys![Enter],
834            CommandRef::Registered(command_id::MICROSCOPE_CONFIRM),
835        );
836
837        // Register RPC handler for state/microscope queries
838        ctx.register_rpc_handler(Arc::new(MicroscopeStateHandler));
839    }
840
841    fn init_state(&self, registry: &PluginStateRegistry) {
842        // Initialize MicroscopeState
843        registry.register(MicroscopeState::new());
844
845        // Initialize PickerRegistry for dynamic picker registration
846        registry.register(PickerRegistry::new());
847
848        // Register the plugin window
849        registry.register_plugin_window(Arc::new(MicroscopePluginWindow));
850    }
851
852    fn subscribe(&self, bus: &reovim_core::event_bus::EventBus, state: Arc<PluginStateRegistry>) {
853        use reovim_core::{
854            event_bus::{
855                EventResult,
856                core_events::{
857                    PluginBackspace, PluginTextInput, RequestFocusChange, RequestModeChange,
858                },
859            },
860            modd::{EditMode, ModeState, SubMode},
861        };
862
863        // Helper function to load preview for the currently selected item
864        fn load_preview(state: &Arc<PluginStateRegistry>, picker_name: &str) {
865            let picker = state
866                .with::<PickerRegistry, _, _>(|r| r.get(picker_name))
867                .flatten();
868
869            if let Some(picker) = picker {
870                let selected_item = state
871                    .with::<MicroscopeState, _, _>(|s| s.selected_item().cloned())
872                    .flatten();
873
874                if let Some(item) = selected_item {
875                    let preview = tokio::task::block_in_place(|| {
876                        tokio::runtime::Handle::current().block_on(picker.preview(&item))
877                    });
878                    state.with_mut::<MicroscopeState, _, _>(|s| {
879                        s.set_preview(preview);
880                    });
881                }
882            }
883        }
884
885        // Handle MicroscopeOpen event
886        let state_clone = Arc::clone(&state);
887        bus.subscribe::<commands::MicroscopeOpen, _>(100, move |event, ctx| {
888            let picker_name = event.picker.clone();
889
890            // Get picker from registry
891            let picker = state_clone
892                .with::<PickerRegistry, _, _>(|registry| registry.get(&picker_name))
893                .flatten();
894
895            if let Some(picker) = picker {
896                // Initialize microscope state with the picker
897                state_clone.with_mut::<MicroscopeState, _, _>(|s| {
898                    s.open(&picker_name, picker.title(), picker.prompt());
899                });
900
901                // Fetch items from picker (synchronously for now)
902                // The file walker is synchronous anyway, async is just trait signature
903                let picker_ctx = PickerContext::default();
904                let items = tokio::task::block_in_place(|| {
905                    tokio::runtime::Handle::current().block_on(picker.fetch(&picker_ctx))
906                });
907
908                // Store items in state
909                state_clone.with_mut::<MicroscopeState, _, _>(|s| {
910                    s.update_items(items);
911                    s.set_loading(microscope::LoadingState::Idle);
912                });
913
914                // Load preview for first selected item
915                load_preview(&state_clone, &picker_name);
916
917                // Request focus change to microscope
918                ctx.emit(RequestFocusChange {
919                    target: COMPONENT_ID,
920                });
921
922                // Start in Normal mode (for j/k navigation, press 'i' to enter Insert)
923                let mode = ModeState::with_interactor_id_and_mode(COMPONENT_ID, EditMode::Normal);
924                ctx.emit(RequestModeChange { mode });
925
926                ctx.request_render();
927            } else {
928                // Picker not found - log would require adding tracing dependency
929            }
930
931            EventResult::Handled
932        });
933
934        // Handle text input from runtime (PluginTextInput event)
935        let state_clone = Arc::clone(&state);
936        bus.subscribe::<PluginTextInput, _>(100, move |event, ctx| {
937            // Only handle if target is microscope and microscope is active
938            if event.target != COMPONENT_ID {
939                return EventResult::NotHandled;
940            }
941
942            let is_active = state_clone
943                .with::<MicroscopeState, _, _>(|s| s.active)
944                .unwrap_or(false);
945
946            if is_active {
947                state_clone.with_mut::<MicroscopeState, _, _>(|s| {
948                    s.insert_char(event.c);
949                });
950                ctx.request_render();
951                EventResult::Handled
952            } else {
953                EventResult::NotHandled
954            }
955        });
956
957        // Handle backspace from runtime (PluginBackspace event)
958        let state_clone = Arc::clone(&state);
959        bus.subscribe::<PluginBackspace, _>(100, move |event, ctx| {
960            // Only handle if we're the target
961            if event.target != COMPONENT_ID {
962                return EventResult::NotHandled;
963            }
964
965            let is_active = state_clone
966                .with::<MicroscopeState, _, _>(|s| s.active)
967                .unwrap_or(false);
968
969            if is_active {
970                state_clone.with_mut::<MicroscopeState, _, _>(|s| {
971                    s.delete_char();
972                });
973                ctx.request_render();
974                EventResult::Handled
975            } else {
976                EventResult::NotHandled
977            }
978        });
979
980        // Handle MicroscopeClose event
981        let state_clone = Arc::clone(&state);
982        bus.subscribe::<commands::MicroscopeClose, _>(100, move |_event, ctx| {
983            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
984                s.close();
985            });
986
987            // Return focus and mode to editor normal
988            ctx.emit(RequestFocusChange {
989                target: reovim_core::modd::ComponentId("editor"),
990            });
991            let mode = ModeState::with_interactor_id_and_mode(
992                reovim_core::modd::ComponentId::EDITOR,
993                EditMode::Normal,
994            );
995            ctx.emit(RequestModeChange { mode });
996
997            ctx.request_render();
998            EventResult::Handled
999        });
1000
1001        // Handle MicroscopeSelectNext event
1002        let state_clone = Arc::clone(&state);
1003        bus.subscribe::<commands::MicroscopeSelectNext, _>(100, move |_event, ctx| {
1004            let picker_name = state_clone
1005                .with::<MicroscopeState, _, _>(|s| s.picker_name.clone())
1006                .unwrap_or_default();
1007
1008            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1009                s.select_next();
1010            });
1011
1012            load_preview(&state_clone, &picker_name);
1013            ctx.request_render();
1014            EventResult::Handled
1015        });
1016
1017        // Handle MicroscopeSelectPrev event
1018        let state_clone = Arc::clone(&state);
1019        bus.subscribe::<commands::MicroscopeSelectPrev, _>(100, move |_event, ctx| {
1020            let picker_name = state_clone
1021                .with::<MicroscopeState, _, _>(|s| s.picker_name.clone())
1022                .unwrap_or_default();
1023
1024            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1025                s.select_prev();
1026            });
1027
1028            load_preview(&state_clone, &picker_name);
1029            ctx.request_render();
1030            EventResult::Handled
1031        });
1032
1033        // Handle MicroscopeBackspace event
1034        let state_clone = Arc::clone(&state);
1035        bus.subscribe::<commands::MicroscopeBackspace, _>(100, move |_event, ctx| {
1036            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1037                s.delete_char();
1038            });
1039            ctx.request_render();
1040            EventResult::Handled
1041        });
1042
1043        // Handle MicroscopeConfirm event - open selected file
1044        let state_clone = Arc::clone(&state);
1045        bus.subscribe::<commands::MicroscopeConfirm, _>(100, move |_event, ctx| {
1046            let selected = state_clone
1047                .with::<MicroscopeState, _, _>(|s| s.selected_item().cloned())
1048                .flatten();
1049
1050            if let Some(item) = selected {
1051                // Close microscope first
1052                state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1053                    s.close();
1054                });
1055
1056                // Return focus and mode to editor
1057                ctx.emit(RequestFocusChange {
1058                    target: reovim_core::modd::ComponentId::EDITOR,
1059                });
1060                let mode = ModeState::with_interactor_id_and_mode(
1061                    reovim_core::modd::ComponentId::EDITOR,
1062                    EditMode::Normal,
1063                );
1064                ctx.emit(RequestModeChange { mode });
1065
1066                // Open the file
1067                ctx.emit(reovim_core::event_bus::core_events::RequestOpenFile {
1068                    path: std::path::PathBuf::from(&item.id),
1069                });
1070
1071                ctx.request_render();
1072            }
1073            EventResult::Handled
1074        });
1075
1076        // Handle MicroscopeClearQuery event
1077        let state_clone = Arc::clone(&state);
1078        bus.subscribe::<commands::MicroscopeClearQuery, _>(100, move |_event, ctx| {
1079            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1080                s.clear_query();
1081            });
1082            ctx.request_render();
1083            EventResult::Handled
1084        });
1085
1086        // Handle MicroscopeDeleteWord event
1087        let state_clone = Arc::clone(&state);
1088        bus.subscribe::<commands::MicroscopeDeleteWord, _>(100, move |_event, ctx| {
1089            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1090                s.delete_word();
1091            });
1092            ctx.request_render();
1093            EventResult::Handled
1094        });
1095
1096        // Handle MicroscopeCursorLeft event
1097        let state_clone = Arc::clone(&state);
1098        bus.subscribe::<commands::MicroscopeCursorLeft, _>(100, move |_event, ctx| {
1099            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1100                s.cursor_left();
1101            });
1102            ctx.request_render();
1103            EventResult::Handled
1104        });
1105
1106        // Handle MicroscopeCursorRight event
1107        let state_clone = Arc::clone(&state);
1108        bus.subscribe::<commands::MicroscopeCursorRight, _>(100, move |_event, ctx| {
1109            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1110                s.cursor_right();
1111            });
1112            ctx.request_render();
1113            EventResult::Handled
1114        });
1115
1116        // Handle MicroscopeCursorStart event
1117        let state_clone = Arc::clone(&state);
1118        bus.subscribe::<commands::MicroscopeCursorStart, _>(100, move |_event, ctx| {
1119            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1120                s.cursor_home();
1121            });
1122            ctx.request_render();
1123            EventResult::Handled
1124        });
1125
1126        // Handle MicroscopeCursorEnd event
1127        let state_clone = Arc::clone(&state);
1128        bus.subscribe::<commands::MicroscopeCursorEnd, _>(100, move |_event, ctx| {
1129            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1130                s.cursor_end();
1131            });
1132            ctx.request_render();
1133            EventResult::Handled
1134        });
1135
1136        // Handle MicroscopeWordForward event
1137        let state_clone = Arc::clone(&state);
1138        bus.subscribe::<commands::MicroscopeWordForward, _>(100, move |_event, ctx| {
1139            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1140                s.word_forward();
1141            });
1142            ctx.request_render();
1143            EventResult::Handled
1144        });
1145
1146        // Handle MicroscopeWordBackward event
1147        let state_clone = Arc::clone(&state);
1148        bus.subscribe::<commands::MicroscopeWordBackward, _>(100, move |_event, ctx| {
1149            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1150                s.word_backward();
1151            });
1152            ctx.request_render();
1153            EventResult::Handled
1154        });
1155
1156        // Handle MicroscopePageDown event
1157        let state_clone = Arc::clone(&state);
1158        bus.subscribe::<commands::MicroscopePageDown, _>(100, move |_event, ctx| {
1159            let picker_name = state_clone
1160                .with::<MicroscopeState, _, _>(|s| s.picker_name.clone())
1161                .unwrap_or_default();
1162
1163            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1164                s.page_down();
1165            });
1166
1167            load_preview(&state_clone, &picker_name);
1168            ctx.request_render();
1169            EventResult::Handled
1170        });
1171
1172        // Handle MicroscopePageUp event
1173        let state_clone = Arc::clone(&state);
1174        bus.subscribe::<commands::MicroscopePageUp, _>(100, move |_event, ctx| {
1175            let picker_name = state_clone
1176                .with::<MicroscopeState, _, _>(|s| s.picker_name.clone())
1177                .unwrap_or_default();
1178
1179            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1180                s.page_up();
1181            });
1182
1183            load_preview(&state_clone, &picker_name);
1184            ctx.request_render();
1185            EventResult::Handled
1186        });
1187
1188        // Handle MicroscopeGotoFirst event
1189        let state_clone = Arc::clone(&state);
1190        bus.subscribe::<commands::MicroscopeGotoFirst, _>(100, move |_event, ctx| {
1191            let picker_name = state_clone
1192                .with::<MicroscopeState, _, _>(|s| s.picker_name.clone())
1193                .unwrap_or_default();
1194
1195            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1196                s.move_to_first();
1197            });
1198
1199            load_preview(&state_clone, &picker_name);
1200            ctx.request_render();
1201            EventResult::Handled
1202        });
1203
1204        // Handle MicroscopeGotoLast event
1205        let state_clone = Arc::clone(&state);
1206        bus.subscribe::<commands::MicroscopeGotoLast, _>(100, move |_event, ctx| {
1207            let picker_name = state_clone
1208                .with::<MicroscopeState, _, _>(|s| s.picker_name.clone())
1209                .unwrap_or_default();
1210
1211            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1212                s.move_to_last();
1213            });
1214
1215            load_preview(&state_clone, &picker_name);
1216            ctx.request_render();
1217            EventResult::Handled
1218        });
1219
1220        // Handle MicroscopeEnterInsert event
1221        let state_clone = Arc::clone(&state);
1222        bus.subscribe::<commands::MicroscopeEnterInsert, _>(100, move |_event, ctx| {
1223            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1224                s.enter_insert();
1225            });
1226
1227            // Enter Interactor sub-mode so characters route to PluginTextInput handler
1228            let mode = ModeState::with_interactor_id_sub_mode(
1229                COMPONENT_ID,
1230                EditMode::Normal,
1231                SubMode::Interactor(COMPONENT_ID),
1232            );
1233            ctx.emit(RequestModeChange { mode });
1234
1235            ctx.request_render();
1236            EventResult::Handled
1237        });
1238
1239        // Handle MicroscopeEnterNormal event
1240        let state_clone = Arc::clone(&state);
1241        bus.subscribe::<commands::MicroscopeEnterNormal, _>(100, move |_event, ctx| {
1242            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1243                s.enter_normal();
1244            });
1245
1246            // Exit Interactor sub-mode, back to normal microscope mode
1247            let mode = ModeState::with_interactor_id_and_mode(COMPONENT_ID, EditMode::Normal);
1248            ctx.emit(RequestModeChange { mode });
1249
1250            ctx.request_render();
1251            EventResult::Handled
1252        });
1253    }
1254}