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,
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        use reovim_core::highlight::{Color, Style};
538
539        // Blue background for search/picker
540        let blue = Color::Rgb {
541            r: 97,
542            g: 175,
543            b: 239,
544        };
545        let fg = Color::Rgb {
546            r: 33,
547            g: 37,
548            b: 43,
549        };
550        let style = Style::new().fg(fg).bg(blue).bold();
551
552        ctx.register_display(COMPONENT_ID, DisplayInfo::new(" MICROSCOPE ", "󰍉 ", style));
553
554        // Register picker opening commands
555        let _ = ctx.register_command(MicroscopeFindFiles);
556        let _ = ctx.register_command(MicroscopeFindBuffers);
557        let _ = ctx.register_command(MicroscopeLiveGrep);
558        let _ = ctx.register_command(MicroscopeFindRecent);
559        let _ = ctx.register_command(MicroscopeCommands);
560        let _ = ctx.register_command(MicroscopeHelp);
561        let _ = ctx.register_command(MicroscopeKeymaps);
562        let _ = ctx.register_command(MicroscopeThemes);
563        let _ = ctx.register_command(MicroscopeProfiles);
564
565        // Register navigation commands
566        let _ = ctx.register_command(MicroscopeSelectNext);
567        let _ = ctx.register_command(MicroscopeSelectPrev);
568        let _ = ctx.register_command(MicroscopePageDown);
569        let _ = ctx.register_command(MicroscopePageUp);
570        let _ = ctx.register_command(MicroscopeGotoFirst);
571        let _ = ctx.register_command(MicroscopeGotoLast);
572
573        // Register action commands
574        let _ = ctx.register_command(MicroscopeConfirm);
575        let _ = ctx.register_command(MicroscopeClose);
576        let _ = ctx.register_command(MicroscopeBackspace);
577
578        // Register mode commands
579        let _ = ctx.register_command(MicroscopeEnterInsert);
580        let _ = ctx.register_command(MicroscopeEnterNormal);
581
582        // Register prompt cursor commands
583        let _ = ctx.register_command(MicroscopeCursorLeft);
584        let _ = ctx.register_command(MicroscopeCursorRight);
585        let _ = ctx.register_command(MicroscopeCursorStart);
586        let _ = ctx.register_command(MicroscopeCursorEnd);
587        let _ = ctx.register_command(MicroscopeWordForward);
588        let _ = ctx.register_command(MicroscopeWordBackward);
589        let _ = ctx.register_command(MicroscopeClearQuery);
590        let _ = ctx.register_command(MicroscopeDeleteWord);
591
592        // Register keybindings (editor normal mode)
593        let editor_normal = KeymapScope::editor_normal();
594
595        // Register Space+f prefix for multi-key sequences
596        ctx.keymap_mut()
597            .get_scope_mut(editor_normal.clone())
598            .insert(keys![Space 'f'], KeyMapInner::with_description("+find").with_category("find"));
599
600        ctx.bind_key_scoped(
601            editor_normal.clone(),
602            keys![Space 'f' 'f'],
603            CommandRef::Registered(command_id::MICROSCOPE_FIND_FILES),
604        );
605        ctx.bind_key_scoped(
606            editor_normal.clone(),
607            keys![Space 'f' 'b'],
608            CommandRef::Registered(command_id::MICROSCOPE_FIND_BUFFERS),
609        );
610        ctx.bind_key_scoped(
611            editor_normal.clone(),
612            keys![Space 'f' 'g'],
613            CommandRef::Registered(command_id::MICROSCOPE_LIVE_GREP),
614        );
615        ctx.bind_key_scoped(
616            editor_normal.clone(),
617            keys![Space 'f' 'r'],
618            CommandRef::Registered(command_id::MICROSCOPE_RECENT_FILES),
619        );
620        ctx.bind_key_scoped(
621            editor_normal.clone(),
622            keys![Space 'f' 'c'],
623            CommandRef::Registered(command_id::MICROSCOPE_COMMANDS),
624        );
625        ctx.bind_key_scoped(
626            editor_normal.clone(),
627            keys![Space 'f' 'h'],
628            CommandRef::Registered(command_id::MICROSCOPE_HELP_TAGS),
629        );
630        ctx.bind_key_scoped(
631            editor_normal,
632            keys![Space 'f' 'k'],
633            CommandRef::Registered(command_id::MICROSCOPE_KEYMAPS),
634        );
635
636        // Register keybindings for when microscope is focused (Normal mode)
637        let microscope_normal = KeymapScope::Component {
638            id: COMPONENT_ID,
639            mode: EditModeKind::Normal,
640        };
641
642        // Navigation
643        ctx.bind_key_scoped(
644            microscope_normal.clone(),
645            keys!['j'],
646            CommandRef::Registered(command_id::MICROSCOPE_SELECT_NEXT),
647        );
648        ctx.bind_key_scoped(
649            microscope_normal.clone(),
650            keys!['k'],
651            CommandRef::Registered(command_id::MICROSCOPE_SELECT_PREV),
652        );
653        ctx.bind_key_scoped(
654            microscope_normal.clone(),
655            keys![(Ctrl 'n')],
656            CommandRef::Registered(command_id::MICROSCOPE_SELECT_NEXT),
657        );
658        ctx.bind_key_scoped(
659            microscope_normal.clone(),
660            keys![(Ctrl 'p')],
661            CommandRef::Registered(command_id::MICROSCOPE_SELECT_PREV),
662        );
663        ctx.bind_key_scoped(
664            microscope_normal.clone(),
665            keys![(Ctrl 'd')],
666            CommandRef::Registered(command_id::MICROSCOPE_PAGE_DOWN),
667        );
668        ctx.bind_key_scoped(
669            microscope_normal.clone(),
670            keys![(Ctrl 'u')],
671            CommandRef::Registered(command_id::MICROSCOPE_PAGE_UP),
672        );
673        ctx.bind_key_scoped(
674            microscope_normal.clone(),
675            keys!['g' 'g'],
676            CommandRef::Registered(command_id::MICROSCOPE_GOTO_FIRST),
677        );
678        ctx.bind_key_scoped(
679            microscope_normal.clone(),
680            keys!['G'],
681            CommandRef::Registered(command_id::MICROSCOPE_GOTO_LAST),
682        );
683
684        // Actions
685        ctx.bind_key_scoped(
686            microscope_normal.clone(),
687            keys![Escape],
688            CommandRef::Registered(command_id::MICROSCOPE_CLOSE),
689        );
690        ctx.bind_key_scoped(
691            microscope_normal.clone(),
692            keys!['q'],
693            CommandRef::Registered(command_id::MICROSCOPE_CLOSE),
694        );
695        ctx.bind_key_scoped(
696            microscope_normal.clone(),
697            keys![Enter],
698            CommandRef::Registered(command_id::MICROSCOPE_CONFIRM),
699        );
700
701        // Mode switching
702        ctx.bind_key_scoped(
703            microscope_normal.clone(),
704            keys!['i'],
705            CommandRef::Registered(command_id::MICROSCOPE_ENTER_INSERT),
706        );
707        ctx.bind_key_scoped(
708            microscope_normal.clone(),
709            keys!['a'],
710            CommandRef::Registered(command_id::MICROSCOPE_ENTER_INSERT),
711        );
712
713        // Prompt cursor movement (Normal mode)
714        ctx.bind_key_scoped(
715            microscope_normal.clone(),
716            keys!['h'],
717            CommandRef::Registered(command_id::MICROSCOPE_CURSOR_LEFT),
718        );
719        ctx.bind_key_scoped(
720            microscope_normal.clone(),
721            keys!['l'],
722            CommandRef::Registered(command_id::MICROSCOPE_CURSOR_RIGHT),
723        );
724        ctx.bind_key_scoped(
725            microscope_normal.clone(),
726            keys!['0'],
727            CommandRef::Registered(command_id::MICROSCOPE_CURSOR_START),
728        );
729        ctx.bind_key_scoped(
730            microscope_normal.clone(),
731            keys!['$'],
732            CommandRef::Registered(command_id::MICROSCOPE_CURSOR_END),
733        );
734        ctx.bind_key_scoped(
735            microscope_normal.clone(),
736            keys!['w'],
737            CommandRef::Registered(command_id::MICROSCOPE_WORD_FORWARD),
738        );
739        ctx.bind_key_scoped(
740            microscope_normal.clone(),
741            keys!['b'],
742            CommandRef::Registered(command_id::MICROSCOPE_WORD_BACKWARD),
743        );
744
745        // Register g as prefix for gg
746        ctx.keymap_mut()
747            .get_scope_mut(microscope_normal)
748            .insert(keys!['g'], KeyMapInner::new());
749
750        // Register keybindings for microscope Insert mode
751        let microscope_insert = KeymapScope::Component {
752            id: COMPONENT_ID,
753            mode: EditModeKind::Insert,
754        };
755
756        // Navigation in insert mode
757        ctx.bind_key_scoped(
758            microscope_insert.clone(),
759            keys![(Ctrl 'n')],
760            CommandRef::Registered(command_id::MICROSCOPE_SELECT_NEXT),
761        );
762        ctx.bind_key_scoped(
763            microscope_insert.clone(),
764            keys![(Ctrl 'p')],
765            CommandRef::Registered(command_id::MICROSCOPE_SELECT_PREV),
766        );
767
768        // Editing in insert mode
769        ctx.bind_key_scoped(
770            microscope_insert.clone(),
771            keys![Backspace],
772            CommandRef::Registered(command_id::MICROSCOPE_BACKSPACE),
773        );
774        ctx.bind_key_scoped(
775            microscope_insert.clone(),
776            keys![(Ctrl 'u')],
777            CommandRef::Registered(command_id::MICROSCOPE_CLEAR_QUERY),
778        );
779        ctx.bind_key_scoped(
780            microscope_insert.clone(),
781            keys![(Ctrl 'w')],
782            CommandRef::Registered(command_id::MICROSCOPE_DELETE_WORD),
783        );
784
785        // Actions in insert mode
786        ctx.bind_key_scoped(
787            microscope_insert.clone(),
788            keys![Escape],
789            CommandRef::Registered(command_id::MICROSCOPE_ENTER_NORMAL),
790        );
791        ctx.bind_key_scoped(
792            microscope_insert,
793            keys![Enter],
794            CommandRef::Registered(command_id::MICROSCOPE_CONFIRM),
795        );
796
797        // Register keybindings for Interactor sub-mode (text input mode)
798        // This is used when microscope is in insert mode for capturing text
799        let microscope_interactor = KeymapScope::SubMode(SubModeKind::Interactor(COMPONENT_ID));
800
801        // Navigation in interactor mode
802        ctx.bind_key_scoped(
803            microscope_interactor.clone(),
804            keys![(Ctrl 'n')],
805            CommandRef::Registered(command_id::MICROSCOPE_SELECT_NEXT),
806        );
807        ctx.bind_key_scoped(
808            microscope_interactor.clone(),
809            keys![(Ctrl 'p')],
810            CommandRef::Registered(command_id::MICROSCOPE_SELECT_PREV),
811        );
812
813        // Editing in interactor mode
814        ctx.bind_key_scoped(
815            microscope_interactor.clone(),
816            keys![Backspace],
817            CommandRef::Registered(command_id::MICROSCOPE_BACKSPACE),
818        );
819        ctx.bind_key_scoped(
820            microscope_interactor.clone(),
821            keys![(Ctrl 'u')],
822            CommandRef::Registered(command_id::MICROSCOPE_CLEAR_QUERY),
823        );
824        ctx.bind_key_scoped(
825            microscope_interactor.clone(),
826            keys![(Ctrl 'w')],
827            CommandRef::Registered(command_id::MICROSCOPE_DELETE_WORD),
828        );
829
830        // Actions in interactor mode
831        ctx.bind_key_scoped(
832            microscope_interactor.clone(),
833            keys![Escape],
834            CommandRef::Registered(command_id::MICROSCOPE_ENTER_NORMAL),
835        );
836        ctx.bind_key_scoped(
837            microscope_interactor,
838            keys![Enter],
839            CommandRef::Registered(command_id::MICROSCOPE_CONFIRM),
840        );
841
842        // Register RPC handler for state/microscope queries
843        ctx.register_rpc_handler(Arc::new(MicroscopeStateHandler));
844    }
845
846    fn init_state(&self, registry: &PluginStateRegistry) {
847        // Initialize MicroscopeState
848        registry.register(MicroscopeState::new());
849
850        // Initialize PickerRegistry for dynamic picker registration
851        registry.register(PickerRegistry::new());
852
853        // Register the plugin window
854        registry.register_plugin_window(Arc::new(MicroscopePluginWindow));
855    }
856
857    fn subscribe(&self, bus: &reovim_core::event_bus::EventBus, state: Arc<PluginStateRegistry>) {
858        use reovim_core::{
859            event_bus::{
860                EventResult,
861                core_events::{
862                    PluginBackspace, PluginTextInput, RequestFocusChange, RequestModeChange,
863                },
864            },
865            modd::{EditMode, ModeState},
866        };
867
868        // Helper function to load preview for the currently selected item
869        fn load_preview(state: &Arc<PluginStateRegistry>, picker_name: &str) {
870            let picker = state
871                .with::<PickerRegistry, _, _>(|r| r.get(picker_name))
872                .flatten();
873
874            if let Some(picker) = picker {
875                let selected_item = state
876                    .with::<MicroscopeState, _, _>(|s| s.selected_item().cloned())
877                    .flatten();
878
879                if let Some(item) = selected_item {
880                    // Create picker context with syntax and decoration factories
881                    let picker_ctx = PickerContext {
882                        syntax_factory: state.syntax_factory(),
883                        decoration_factory: state.decoration_factory(),
884                        ..PickerContext::default()
885                    };
886
887                    let preview = tokio::task::block_in_place(|| {
888                        tokio::runtime::Handle::current()
889                            .block_on(picker.preview(&item, &picker_ctx))
890                    });
891                    state.with_mut::<MicroscopeState, _, _>(|s| {
892                        s.set_preview(preview);
893                    });
894                }
895            }
896        }
897
898        // Handle MicroscopeOpen event
899        let state_clone = Arc::clone(&state);
900        bus.subscribe::<commands::MicroscopeOpen, _>(100, move |event, ctx| {
901            let picker_name = event.picker.clone();
902
903            // Get picker from registry
904            let picker = state_clone
905                .with::<PickerRegistry, _, _>(|registry| registry.get(&picker_name))
906                .flatten();
907
908            if let Some(picker) = picker {
909                // Initialize microscope state with the picker
910                state_clone.with_mut::<MicroscopeState, _, _>(|s| {
911                    s.open(&picker_name, picker.title(), picker.prompt());
912                });
913
914                // Fetch items from picker (synchronously for now)
915                // The file walker is synchronous anyway, async is just trait signature
916                let picker_ctx = PickerContext::default();
917                let items = tokio::task::block_in_place(|| {
918                    tokio::runtime::Handle::current().block_on(picker.fetch(&picker_ctx))
919                });
920
921                // Store items in state
922                state_clone.with_mut::<MicroscopeState, _, _>(|s| {
923                    s.update_items(items);
924                    s.set_loading(microscope::LoadingState::Idle);
925                });
926
927                // Load preview for first selected item
928                load_preview(&state_clone, &picker_name);
929
930                // Request focus change to microscope
931                ctx.emit(RequestFocusChange {
932                    target: COMPONENT_ID,
933                });
934
935                // Start in Normal mode (for j/k navigation, press 'i' to enter Insert)
936                let mode = ModeState::with_interactor_id_and_mode(COMPONENT_ID, EditMode::Normal);
937                ctx.emit(RequestModeChange { mode });
938
939                ctx.request_render();
940            } else {
941                // Picker not found - log would require adding tracing dependency
942            }
943
944            EventResult::Handled
945        });
946
947        // Handle text input from runtime (PluginTextInput event)
948        let state_clone = Arc::clone(&state);
949        bus.subscribe_targeted::<PluginTextInput, _>(COMPONENT_ID, 100, move |event, ctx| {
950            let is_active = state_clone
951                .with::<MicroscopeState, _, _>(|s| s.active)
952                .unwrap_or(false);
953
954            if is_active {
955                state_clone.with_mut::<MicroscopeState, _, _>(|s| {
956                    s.insert_char(event.c);
957                });
958                ctx.request_render();
959                EventResult::Handled
960            } else {
961                EventResult::NotHandled
962            }
963        });
964
965        // Handle backspace from runtime (PluginBackspace event)
966        let state_clone = Arc::clone(&state);
967        bus.subscribe_targeted::<PluginBackspace, _>(COMPONENT_ID, 100, move |_event, ctx| {
968            let is_active = state_clone
969                .with::<MicroscopeState, _, _>(|s| s.active)
970                .unwrap_or(false);
971
972            if is_active {
973                state_clone.with_mut::<MicroscopeState, _, _>(|s| {
974                    s.delete_char();
975                });
976                ctx.request_render();
977                EventResult::Handled
978            } else {
979                EventResult::NotHandled
980            }
981        });
982
983        // Handle MicroscopeClose event
984        let state_clone = Arc::clone(&state);
985        bus.subscribe::<commands::MicroscopeClose, _>(100, move |_event, ctx| {
986            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
987                s.close();
988            });
989
990            // Return focus and mode to editor normal
991            ctx.emit(RequestFocusChange {
992                target: reovim_core::modd::ComponentId("editor"),
993            });
994            let mode = ModeState::with_interactor_id_and_mode(
995                reovim_core::modd::ComponentId::EDITOR,
996                EditMode::Normal,
997            );
998            ctx.emit(RequestModeChange { mode });
999
1000            ctx.request_render();
1001            EventResult::Handled
1002        });
1003
1004        // Handle MicroscopeSelectNext event
1005        let state_clone = Arc::clone(&state);
1006        bus.subscribe::<commands::MicroscopeSelectNext, _>(100, move |_event, ctx| {
1007            let picker_name = state_clone
1008                .with::<MicroscopeState, _, _>(|s| s.picker_name.clone())
1009                .unwrap_or_default();
1010
1011            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1012                s.select_next();
1013            });
1014
1015            load_preview(&state_clone, &picker_name);
1016            ctx.request_render();
1017            EventResult::Handled
1018        });
1019
1020        // Handle MicroscopeSelectPrev event
1021        let state_clone = Arc::clone(&state);
1022        bus.subscribe::<commands::MicroscopeSelectPrev, _>(100, move |_event, ctx| {
1023            let picker_name = state_clone
1024                .with::<MicroscopeState, _, _>(|s| s.picker_name.clone())
1025                .unwrap_or_default();
1026
1027            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1028                s.select_prev();
1029            });
1030
1031            load_preview(&state_clone, &picker_name);
1032            ctx.request_render();
1033            EventResult::Handled
1034        });
1035
1036        // Handle MicroscopeBackspace event
1037        let state_clone = Arc::clone(&state);
1038        bus.subscribe::<commands::MicroscopeBackspace, _>(100, move |_event, ctx| {
1039            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1040                s.delete_char();
1041            });
1042            ctx.request_render();
1043            EventResult::Handled
1044        });
1045
1046        // Handle MicroscopeConfirm event - open selected file
1047        let state_clone = Arc::clone(&state);
1048        bus.subscribe::<commands::MicroscopeConfirm, _>(100, move |_event, ctx| {
1049            let selected = state_clone
1050                .with::<MicroscopeState, _, _>(|s| s.selected_item().cloned())
1051                .flatten();
1052
1053            if let Some(item) = selected {
1054                // Close microscope first
1055                state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1056                    s.close();
1057                });
1058
1059                // Return focus and mode to editor
1060                ctx.emit(RequestFocusChange {
1061                    target: reovim_core::modd::ComponentId::EDITOR,
1062                });
1063                let mode = ModeState::with_interactor_id_and_mode(
1064                    reovim_core::modd::ComponentId::EDITOR,
1065                    EditMode::Normal,
1066                );
1067                ctx.emit(RequestModeChange { mode });
1068
1069                // Open the file
1070                ctx.emit(reovim_core::event_bus::core_events::RequestOpenFile {
1071                    path: std::path::PathBuf::from(&item.id),
1072                });
1073
1074                ctx.request_render();
1075            }
1076            EventResult::Handled
1077        });
1078
1079        // Handle MicroscopeClearQuery event
1080        let state_clone = Arc::clone(&state);
1081        bus.subscribe::<commands::MicroscopeClearQuery, _>(100, move |_event, ctx| {
1082            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1083                s.clear_query();
1084            });
1085            ctx.request_render();
1086            EventResult::Handled
1087        });
1088
1089        // Handle MicroscopeDeleteWord event
1090        let state_clone = Arc::clone(&state);
1091        bus.subscribe::<commands::MicroscopeDeleteWord, _>(100, move |_event, ctx| {
1092            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1093                s.delete_word();
1094            });
1095            ctx.request_render();
1096            EventResult::Handled
1097        });
1098
1099        // Handle MicroscopeCursorLeft event
1100        let state_clone = Arc::clone(&state);
1101        bus.subscribe::<commands::MicroscopeCursorLeft, _>(100, move |_event, ctx| {
1102            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1103                s.cursor_left();
1104            });
1105            ctx.request_render();
1106            EventResult::Handled
1107        });
1108
1109        // Handle MicroscopeCursorRight event
1110        let state_clone = Arc::clone(&state);
1111        bus.subscribe::<commands::MicroscopeCursorRight, _>(100, move |_event, ctx| {
1112            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1113                s.cursor_right();
1114            });
1115            ctx.request_render();
1116            EventResult::Handled
1117        });
1118
1119        // Handle MicroscopeCursorStart event
1120        let state_clone = Arc::clone(&state);
1121        bus.subscribe::<commands::MicroscopeCursorStart, _>(100, move |_event, ctx| {
1122            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1123                s.cursor_home();
1124            });
1125            ctx.request_render();
1126            EventResult::Handled
1127        });
1128
1129        // Handle MicroscopeCursorEnd event
1130        let state_clone = Arc::clone(&state);
1131        bus.subscribe::<commands::MicroscopeCursorEnd, _>(100, move |_event, ctx| {
1132            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1133                s.cursor_end();
1134            });
1135            ctx.request_render();
1136            EventResult::Handled
1137        });
1138
1139        // Handle MicroscopeWordForward event
1140        let state_clone = Arc::clone(&state);
1141        bus.subscribe::<commands::MicroscopeWordForward, _>(100, move |_event, ctx| {
1142            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1143                s.word_forward();
1144            });
1145            ctx.request_render();
1146            EventResult::Handled
1147        });
1148
1149        // Handle MicroscopeWordBackward event
1150        let state_clone = Arc::clone(&state);
1151        bus.subscribe::<commands::MicroscopeWordBackward, _>(100, move |_event, ctx| {
1152            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1153                s.word_backward();
1154            });
1155            ctx.request_render();
1156            EventResult::Handled
1157        });
1158
1159        // Handle MicroscopePageDown event
1160        let state_clone = Arc::clone(&state);
1161        bus.subscribe::<commands::MicroscopePageDown, _>(100, move |_event, ctx| {
1162            let picker_name = state_clone
1163                .with::<MicroscopeState, _, _>(|s| s.picker_name.clone())
1164                .unwrap_or_default();
1165
1166            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1167                s.page_down();
1168            });
1169
1170            load_preview(&state_clone, &picker_name);
1171            ctx.request_render();
1172            EventResult::Handled
1173        });
1174
1175        // Handle MicroscopePageUp event
1176        let state_clone = Arc::clone(&state);
1177        bus.subscribe::<commands::MicroscopePageUp, _>(100, move |_event, ctx| {
1178            let picker_name = state_clone
1179                .with::<MicroscopeState, _, _>(|s| s.picker_name.clone())
1180                .unwrap_or_default();
1181
1182            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1183                s.page_up();
1184            });
1185
1186            load_preview(&state_clone, &picker_name);
1187            ctx.request_render();
1188            EventResult::Handled
1189        });
1190
1191        // Handle MicroscopeGotoFirst event
1192        let state_clone = Arc::clone(&state);
1193        bus.subscribe::<commands::MicroscopeGotoFirst, _>(100, move |_event, ctx| {
1194            let picker_name = state_clone
1195                .with::<MicroscopeState, _, _>(|s| s.picker_name.clone())
1196                .unwrap_or_default();
1197
1198            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1199                s.move_to_first();
1200            });
1201
1202            load_preview(&state_clone, &picker_name);
1203            ctx.request_render();
1204            EventResult::Handled
1205        });
1206
1207        // Handle MicroscopeGotoLast event
1208        let state_clone = Arc::clone(&state);
1209        bus.subscribe::<commands::MicroscopeGotoLast, _>(100, move |_event, ctx| {
1210            let picker_name = state_clone
1211                .with::<MicroscopeState, _, _>(|s| s.picker_name.clone())
1212                .unwrap_or_default();
1213
1214            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1215                s.move_to_last();
1216            });
1217
1218            load_preview(&state_clone, &picker_name);
1219            ctx.request_render();
1220            EventResult::Handled
1221        });
1222
1223        // Handle MicroscopeEnterInsert event
1224        let state_clone = Arc::clone(&state);
1225        bus.subscribe::<commands::MicroscopeEnterInsert, _>(100, move |_event, ctx| {
1226            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1227                s.enter_insert();
1228            });
1229
1230            // Enter Interactor sub-mode so characters route to PluginTextInput handler
1231            ctx.enter_interactor_mode(COMPONENT_ID);
1232
1233            ctx.request_render();
1234            EventResult::Handled
1235        });
1236
1237        // Handle MicroscopeEnterNormal event
1238        let state_clone = Arc::clone(&state);
1239        bus.subscribe::<commands::MicroscopeEnterNormal, _>(100, move |_event, ctx| {
1240            state_clone.with_mut::<MicroscopeState, _, _>(|s| {
1241                s.enter_normal();
1242            });
1243
1244            // Exit Interactor sub-mode, back to normal microscope mode
1245            let mode = ModeState::with_interactor_id_and_mode(COMPONENT_ID, EditMode::Normal);
1246            ctx.emit(RequestModeChange { mode });
1247
1248            ctx.request_render();
1249            EventResult::Handled
1250        });
1251    }
1252}