reovim_plugin_explorer/
lib.rs

1//! File explorer plugin for reovim
2//!
3//! This plugin provides file browser functionality:
4//! - Tree navigation with expand/collapse
5//! - File/directory operations (create, rename, delete)
6//! - Copy/cut/paste operations
7//! - Visual selection mode
8//! - Filter and search
9//!
10//! # Architecture
11//!
12//! State (`ExplorerState`) is registered in `PluginStateRegistry` and accessed
13//! via `RuntimeContext::with_state_mut::<ExplorerState>()`.
14//!
15//! Commands emit `EventBus` events that are handled by event subscriptions.
16
17use std::{any::TypeId, sync::Arc};
18
19use reovim_core::{
20    bind::{CommandRef, EditModeKind, KeymapScope},
21    event_bus::{EventBus, EventResult},
22    keys,
23    modd::ComponentId,
24    plugin::{Plugin, PluginContext, PluginId, PluginStateRegistry},
25    subscribe_state, subscribe_state_mode,
26};
27
28mod command;
29mod file_colors;
30mod node;
31mod provider;
32mod render;
33mod state;
34mod tree;
35mod tree_render;
36mod window;
37
38#[cfg(test)]
39mod tests;
40
41// Re-export state, events, and provider
42pub use {provider::ExplorerBufferProvider, state::ExplorerState};
43
44/// Command IDs for explorer commands
45pub mod command_id {
46    use reovim_core::command::id::CommandId;
47
48    pub const TOGGLE_EXPLORER: CommandId = CommandId::new("explorer_toggle");
49
50    // Navigation
51    pub const CURSOR_UP: CommandId = CommandId::new("explorer_cursor_up");
52    pub const CURSOR_DOWN: CommandId = CommandId::new("explorer_cursor_down");
53    pub const PAGE_UP: CommandId = CommandId::new("explorer_page_up");
54    pub const PAGE_DOWN: CommandId = CommandId::new("explorer_page_down");
55    pub const GOTO_FIRST: CommandId = CommandId::new("explorer_goto_first");
56    pub const GOTO_LAST: CommandId = CommandId::new("explorer_goto_last");
57    pub const GO_TO_PARENT: CommandId = CommandId::new("explorer_goto_parent");
58    pub const CHANGE_ROOT: CommandId = CommandId::new("explorer_change_root");
59
60    // Tree operations
61    pub const TOGGLE_NODE: CommandId = CommandId::new("explorer_toggle_node");
62    pub const OPEN_NODE: CommandId = CommandId::new("explorer_open_node");
63    pub const CLOSE_PARENT: CommandId = CommandId::new("explorer_close_parent");
64    pub const REFRESH: CommandId = CommandId::new("explorer_refresh");
65    pub const TOGGLE_HIDDEN: CommandId = CommandId::new("explorer_toggle_hidden");
66    pub const TOGGLE_SIZES: CommandId = CommandId::new("explorer_toggle_sizes");
67    pub const CLOSE: CommandId = CommandId::new("explorer_close");
68    pub const FOCUS_EDITOR: CommandId = CommandId::new("explorer_focus_editor");
69
70    // File operations
71    pub const CREATE_FILE: CommandId = CommandId::new("explorer_create_file");
72    pub const CREATE_DIR: CommandId = CommandId::new("explorer_create_dir");
73    pub const RENAME: CommandId = CommandId::new("explorer_rename");
74    pub const DELETE: CommandId = CommandId::new("explorer_delete");
75    pub const FILTER: CommandId = CommandId::new("explorer_filter");
76    pub const CLEAR_FILTER: CommandId = CommandId::new("explorer_clear_filter");
77
78    // Clipboard
79    pub const YANK: CommandId = CommandId::new("explorer_yank");
80    pub const CUT: CommandId = CommandId::new("explorer_cut");
81    pub const PASTE: CommandId = CommandId::new("explorer_paste");
82
83    // Visual mode
84    pub const VISUAL_MODE: CommandId = CommandId::new("explorer_visual_mode");
85    pub const TOGGLE_SELECT: CommandId = CommandId::new("explorer_toggle_select");
86    pub const SELECT_ALL: CommandId = CommandId::new("explorer_select_all");
87    pub const EXIT_VISUAL: CommandId = CommandId::new("explorer_exit_visual");
88
89    // Input mode
90    pub const CONFIRM_INPUT: CommandId = CommandId::new("explorer_confirm_input");
91    pub const CANCEL_INPUT: CommandId = CommandId::new("explorer_cancel_input");
92    pub const INPUT_BACKSPACE: CommandId = CommandId::new("explorer_input_backspace");
93
94    // File info popup
95    pub const SHOW_INFO: CommandId = CommandId::new("explorer_show_info");
96    pub const CLOSE_POPUP: CommandId = CommandId::new("explorer_close_popup");
97    pub const COPY_PATH: CommandId = CommandId::new("explorer_copy_path");
98}
99
100// Plugin unified command-event types
101pub use command::{
102    ExplorerCancelInput, ExplorerChangeRoot, ExplorerClearFilter, ExplorerClose,
103    ExplorerCloseParent, ExplorerClosePopup, ExplorerConfirmInput, ExplorerCopyPath,
104    ExplorerCreateDir, ExplorerCreateFile, ExplorerCursorDown, ExplorerCursorUp, ExplorerCut,
105    ExplorerDelete, ExplorerExitVisual, ExplorerFocusEditor, ExplorerGoToParent, ExplorerGotoFirst,
106    ExplorerGotoLast, ExplorerInputBackspace, ExplorerInputChar, ExplorerOpenNode,
107    ExplorerPageDown, ExplorerPageUp, ExplorerPaste, ExplorerRefresh, ExplorerRename,
108    ExplorerSelectAll, ExplorerShowInfo, ExplorerStartFilter, ExplorerToggle, ExplorerToggleHidden,
109    ExplorerToggleNode, ExplorerToggleSelect, ExplorerToggleSizes, ExplorerVisualMode,
110    ExplorerYank,
111};
112
113/// File explorer plugin
114///
115/// Provides file browser sidebar:
116/// - Tree navigation
117/// - File/directory operations
118/// - Copy/cut/paste
119/// - Visual selection
120pub struct ExplorerPlugin;
121
122impl Plugin for ExplorerPlugin {
123    fn id(&self) -> PluginId {
124        PluginId::new("reovim:explorer")
125    }
126
127    fn name(&self) -> &'static str {
128        "Explorer"
129    }
130
131    fn description(&self) -> &'static str {
132        "File browser sidebar with tree navigation"
133    }
134
135    fn dependencies(&self) -> Vec<TypeId> {
136        // CorePlugin dependency
137        vec![]
138    }
139
140    fn build(&self, ctx: &mut PluginContext) {
141        self.register_display_info(ctx);
142        self.register_navigation_commands(ctx);
143        self.register_tree_commands(ctx);
144        self.register_file_commands(ctx);
145        self.register_clipboard_commands(ctx);
146        self.register_visual_commands(ctx);
147        self.register_input_commands(ctx);
148        self.register_keybindings(ctx);
149        // Input handling is done via PluginTextInput/PluginBackspace event subscriptions
150    }
151
152    fn init_state(&self, registry: &PluginStateRegistry) {
153        // Initialize ExplorerState with current working directory
154        // If cwd fails, explorer will show error on first use
155        if let Ok(cwd) = std::env::current_dir()
156            && let Ok(state) = ExplorerState::new(cwd)
157        {
158            registry.register(state);
159            tracing::info!("ExplorerPlugin: registered state");
160        } else {
161            tracing::error!("ExplorerPlugin: failed to create ExplorerState");
162        }
163
164        // Register plugin windows
165        registry.register_plugin_window(Arc::new(window::ExplorerPluginWindow));
166        registry.register_plugin_window(Arc::new(window::FileDetailsPluginWindow));
167        tracing::info!("ExplorerPlugin: registered plugin windows");
168    }
169
170    fn subscribe(&self, bus: &EventBus, state: Arc<PluginStateRegistry>) {
171        self.subscribe_raw_input(bus, &state);
172        self.subscribe_navigation(bus, &state);
173        self.subscribe_tree_operations(bus, &state);
174        self.subscribe_clipboard(bus, &state);
175        self.subscribe_file_operations(bus, &state);
176        self.subscribe_input_handling(bus, &state);
177        self.subscribe_visual_mode(bus, &state);
178        self.subscribe_focus_visibility(bus, &state);
179        self.subscribe_popup(bus, &state);
180        self.subscribe_settings(bus, &state);
181    }
182}
183
184impl ExplorerPlugin {
185    /// Register and subscribe to explorer settings
186    fn subscribe_settings(&self, bus: &EventBus, state: &Arc<PluginStateRegistry>) {
187        use reovim_core::option::{
188            OptionChanged, OptionSpec, OptionValue, RegisterOption, RegisterSettingSection,
189        };
190
191        // Register settings section
192        bus.emit(
193            RegisterSettingSection::new("explorer", "File Explorer")
194                .with_description("File tree browser settings")
195                .with_order(110),
196        ); // After core (0-50), before custom (200+)
197
198        // Register options
199        bus.emit(RegisterOption::new(
200            OptionSpec::new(
201                "explorer.enable_colors",
202                "Enable file type coloring",
203                OptionValue::Bool(true),
204            )
205            .with_section("File Explorer")
206            .with_display_order(10),
207        ));
208
209        bus.emit(RegisterOption::new(
210            OptionSpec::new(
211                "explorer.tree_style",
212                "Tree drawing style",
213                OptionValue::String("box_drawing".to_string()),
214            )
215            .with_section("File Explorer")
216            .with_display_order(20),
217        ));
218
219        bus.emit(RegisterOption::new(
220            OptionSpec::new("explorer.show_hidden", "Show hidden files", OptionValue::Bool(false))
221                .with_section("File Explorer")
222                .with_display_order(30),
223        ));
224
225        bus.emit(RegisterOption::new(
226            OptionSpec::new("explorer.show_sizes", "Show file sizes", OptionValue::Bool(false))
227                .with_section("File Explorer")
228                .with_display_order(40),
229        ));
230
231        // Subscribe to option changes
232        let state_clone = Arc::clone(state);
233        bus.subscribe::<OptionChanged, _>(100, move |event, ctx| {
234            match event.name.as_str() {
235                "explorer.enable_colors" => {
236                    if let Some(enabled) = event.new_value.as_bool() {
237                        state_clone.with_mut::<ExplorerState, _, _>(|explorer| {
238                            explorer.enable_colors = enabled;
239                        });
240                        ctx.request_render();
241                    }
242                }
243                "explorer.tree_style" => {
244                    if let Some(style_str) = event.new_value.as_str() {
245                        let tree_style = match style_str {
246                            "none" => crate::state::TreeStyle::None,
247                            "simple" => crate::state::TreeStyle::Simple,
248                            "box_drawing" => crate::state::TreeStyle::BoxDrawing,
249                            _ => crate::state::TreeStyle::BoxDrawing,
250                        };
251                        state_clone.with_mut::<ExplorerState, _, _>(|explorer| {
252                            explorer.tree_style = tree_style;
253                        });
254                        ctx.request_render();
255                    }
256                }
257                "explorer.show_hidden" => {
258                    if let Some(show) = event.new_value.as_bool() {
259                        state_clone.with_mut::<ExplorerState, _, _>(|explorer| {
260                            explorer.show_hidden = show;
261                        });
262                        ctx.request_render();
263                    }
264                }
265                "explorer.show_sizes" => {
266                    if let Some(show) = event.new_value.as_bool() {
267                        state_clone.with_mut::<ExplorerState, _, _>(|explorer| {
268                            explorer.show_sizes = show;
269                        });
270                        ctx.request_render();
271                    }
272                }
273                _ => {}
274            }
275            EventResult::Handled
276        });
277    }
278}
279
280// Event subscription sub-methods
281impl ExplorerPlugin {
282    /// Subscribe to raw text input events (PluginTextInput, PluginBackspace)
283    fn subscribe_raw_input(&self, bus: &EventBus, state: &Arc<PluginStateRegistry>) {
284        use reovim_core::event_bus::core_events::{PluginBackspace, PluginTextInput};
285
286        // Handle text input from runtime (PluginTextInput event)
287        let state_clone = Arc::clone(state);
288        bus.subscribe_targeted::<PluginTextInput, _>(COMPONENT_ID, 100, move |event, ctx| {
289            state_clone.with_mut::<ExplorerState, _, _>(|s| {
290                s.input_char(event.c);
291            });
292            ctx.request_render();
293            EventResult::Handled
294        });
295
296        // Handle backspace from runtime (PluginBackspace event)
297        // In input mode: delete character from input buffer
298        // In normal mode: navigate to parent directory
299        let state_clone = Arc::clone(state);
300        bus.subscribe_targeted::<PluginBackspace, _>(COMPONENT_ID, 100, move |_event, ctx| {
301            let is_input_mode = state_clone
302                .with::<ExplorerState, _, _>(|s| s.is_input_mode())
303                .unwrap_or(false);
304
305            if is_input_mode {
306                // In input mode, handle as text backspace
307                state_clone.with_mut::<ExplorerState, _, _>(|s| {
308                    s.input_backspace();
309                });
310            } else {
311                // In normal mode, navigate to parent directory
312                state_clone.with_mut::<ExplorerState, _, _>(|s| {
313                    s.go_to_parent();
314                    s.update_scroll();
315                    s.sync_popup();
316                });
317            }
318            ctx.request_render();
319            EventResult::Handled
320        });
321    }
322
323    /// Subscribe to navigation events (cursor movement, page up/down, goto)
324    fn subscribe_navigation(&self, bus: &EventBus, state: &Arc<PluginStateRegistry>) {
325        subscribe_state!(bus, state, ExplorerCursorUp, ExplorerState, |s, e| {
326            s.move_cursor(-(e.count as isize));
327            s.update_scroll();
328            s.sync_popup();
329        });
330
331        subscribe_state!(bus, state, ExplorerCursorDown, ExplorerState, |s, e| {
332            s.move_cursor(e.count as isize);
333            s.update_scroll();
334            s.sync_popup();
335        });
336
337        subscribe_state!(bus, state, ExplorerPageUp, ExplorerState, |s| {
338            s.move_page(s.visible_height, false);
339            s.update_scroll();
340            s.sync_popup();
341        });
342
343        subscribe_state!(bus, state, ExplorerPageDown, ExplorerState, |s| {
344            s.move_page(s.visible_height, true);
345            s.update_scroll();
346            s.sync_popup();
347        });
348
349        subscribe_state!(bus, state, ExplorerGotoFirst, ExplorerState, |s| {
350            s.move_to_first();
351            s.update_scroll();
352            s.sync_popup();
353        });
354
355        subscribe_state!(bus, state, ExplorerGotoLast, ExplorerState, |s| {
356            s.move_to_last();
357            s.update_scroll();
358            s.sync_popup();
359        });
360
361        subscribe_state!(bus, state, ExplorerGoToParent, ExplorerState, |s| {
362            s.go_to_parent();
363            s.update_scroll();
364            s.sync_popup();
365        });
366
367        subscribe_state!(bus, state, ExplorerChangeRoot, ExplorerState, |s| {
368            s.change_root_to_current();
369            s.update_scroll();
370            s.sync_popup();
371        });
372    }
373
374    /// Subscribe to tree operation events (open, toggle, refresh, etc.)
375    fn subscribe_tree_operations(&self, bus: &EventBus, state: &Arc<PluginStateRegistry>) {
376        use reovim_core::event_bus::core_events::{RequestFocusChange, RequestOpenFile};
377
378        // Open file or toggle directory
379        let state_clone = Arc::clone(state);
380        bus.subscribe::<ExplorerOpenNode, _>(100, move |_event, ctx| {
381            tracing::info!("ExplorerPlugin: ExplorerOpenNode received");
382
383            let result = state_clone.with::<ExplorerState, _, _>(|explorer| {
384                if let Some(node) = explorer.current_node() {
385                    if node.is_file() {
386                        // Open the file
387                        Some((node.path.clone(), true))
388                    } else if node.is_dir() {
389                        // Toggle directory
390                        Some((node.path.clone(), false))
391                    } else {
392                        None
393                    }
394                } else {
395                    None
396                }
397            });
398
399            if let Some(Some((path, is_file))) = result {
400                if is_file {
401                    tracing::info!("ExplorerPlugin: Requesting to open file: {:?}", path);
402                    ctx.emit(RequestOpenFile { path });
403                    // Return focus to editor after opening file
404                    ctx.emit(RequestFocusChange {
405                        target: ComponentId::EDITOR,
406                    });
407                } else {
408                    tracing::info!("ExplorerPlugin: Toggling directory: {:?}", path);
409                    state_clone.with_mut::<ExplorerState, _, _>(|explorer| {
410                        let _ = explorer.toggle_current();
411                    });
412                }
413                ctx.request_render();
414            }
415
416            EventResult::Handled
417        });
418
419        // Tree manipulation events
420        subscribe_state!(bus, state, ExplorerToggleNode, ExplorerState, |s| {
421            let _ = s.toggle_current();
422        });
423
424        subscribe_state!(bus, state, ExplorerCloseParent, ExplorerState, |s| {
425            s.collapse_current();
426        });
427
428        subscribe_state!(bus, state, ExplorerRefresh, ExplorerState, |s| {
429            let _ = s.refresh();
430        });
431
432        subscribe_state!(bus, state, ExplorerToggleHidden, ExplorerState, |s| {
433            s.toggle_hidden();
434        });
435
436        subscribe_state!(bus, state, ExplorerToggleSizes, ExplorerState, |s| {
437            s.toggle_sizes();
438        });
439    }
440
441    /// Subscribe to clipboard events (yank, cut, paste, copy path)
442    fn subscribe_clipboard(&self, bus: &EventBus, state: &Arc<PluginStateRegistry>) {
443        subscribe_state!(bus, state, ExplorerYank, ExplorerState, |s| {
444            s.yank_current();
445        });
446
447        subscribe_state!(bus, state, ExplorerCut, ExplorerState, |s| {
448            s.cut_current();
449        });
450
451        subscribe_state!(bus, state, ExplorerPaste, ExplorerState, |s| {
452            let _ = s.paste();
453        });
454
455        // Copy path to clipboard
456        let state_clone = Arc::clone(state);
457        bus.subscribe::<ExplorerCopyPath, _>(100, move |_event, ctx| {
458            use reovim_core::event_bus::core_events::RequestSetRegister;
459
460            let path = state_clone
461                .with::<ExplorerState, _, _>(|s| {
462                    s.current_node()
463                        .map(|n| n.path.to_string_lossy().to_string())
464                })
465                .flatten();
466
467            if let Some(path) = path {
468                // Set both unnamed register and system clipboard
469                ctx.emit(RequestSetRegister {
470                    register: None, // Unnamed register for 'p' paste
471                    text: path.clone(),
472                });
473                ctx.emit(RequestSetRegister {
474                    register: Some('+'), // System clipboard
475                    text: path,
476                });
477
478                state_clone.with_mut::<ExplorerState, _, _>(|s| {
479                    s.close_popup();
480                    s.message = Some("Path copied to clipboard".to_string());
481                });
482            }
483
484            ctx.request_render();
485            EventResult::Handled
486        });
487    }
488
489    /// Subscribe to file operation events (create, rename, delete, filter)
490    fn subscribe_file_operations(&self, bus: &EventBus, state: &Arc<PluginStateRegistry>) {
491        use reovim_core::modd::{EditMode, ModeState, SubMode};
492
493        subscribe_state_mode!(
494            bus,
495            state,
496            ExplorerCreateFile,
497            ExplorerState,
498            |s| {
499                s.start_create_file();
500            },
501            ModeState::with_interactor_id_sub_mode(
502                COMPONENT_ID,
503                EditMode::Normal,
504                SubMode::Interactor(COMPONENT_ID)
505            )
506        );
507
508        subscribe_state_mode!(
509            bus,
510            state,
511            ExplorerCreateDir,
512            ExplorerState,
513            |s| {
514                s.start_create_dir();
515            },
516            ModeState::with_interactor_id_sub_mode(
517                COMPONENT_ID,
518                EditMode::Normal,
519                SubMode::Interactor(COMPONENT_ID)
520            )
521        );
522
523        subscribe_state_mode!(
524            bus,
525            state,
526            ExplorerRename,
527            ExplorerState,
528            |s| {
529                s.start_rename();
530            },
531            ModeState::with_interactor_id_sub_mode(
532                COMPONENT_ID,
533                EditMode::Normal,
534                SubMode::Interactor(COMPONENT_ID)
535            )
536        );
537
538        subscribe_state_mode!(
539            bus,
540            state,
541            ExplorerDelete,
542            ExplorerState,
543            |s| {
544                s.start_delete();
545            },
546            ModeState::with_interactor_id_sub_mode(
547                COMPONENT_ID,
548                EditMode::Normal,
549                SubMode::Interactor(COMPONENT_ID)
550            )
551        );
552
553        subscribe_state_mode!(
554            bus,
555            state,
556            ExplorerStartFilter,
557            ExplorerState,
558            |s| {
559                s.start_filter();
560            },
561            ModeState::with_interactor_id_sub_mode(
562                COMPONENT_ID,
563                EditMode::Normal,
564                SubMode::Interactor(COMPONENT_ID)
565            )
566        );
567
568        // Exit Interactor sub-mode back to normal explorer mode
569        subscribe_state_mode!(
570            bus,
571            state,
572            ExplorerClearFilter,
573            ExplorerState,
574            |s| {
575                s.clear_filter();
576            },
577            ModeState::with_interactor_id_and_mode(COMPONENT_ID, EditMode::Normal)
578        );
579    }
580
581    /// Subscribe to input handling events (confirm, cancel, char input)
582    fn subscribe_input_handling(&self, bus: &EventBus, state: &Arc<PluginStateRegistry>) {
583        use reovim_core::{
584            event_bus::core_events::RequestModeChange,
585            modd::{EditMode, ModeState},
586        };
587
588        let state_clone = Arc::clone(state);
589        bus.subscribe::<ExplorerConfirmInput, _>(100, move |_event, ctx| {
590            // Check if popup is visible
591            let popup_visible = state_clone
592                .with::<ExplorerState, _, _>(|s| s.is_popup_visible())
593                .unwrap_or(false);
594
595            if popup_visible {
596                // Close the popup
597                state_clone.with_mut::<ExplorerState, _, _>(|s| {
598                    s.close_popup();
599                });
600                ctx.request_render();
601                return EventResult::Handled;
602            }
603
604            // Check if in input mode
605            let in_input_mode = state_clone
606                .with::<ExplorerState, _, _>(|s| {
607                    !matches!(s.input_mode, crate::state::ExplorerInputMode::None)
608                })
609                .unwrap_or(false);
610
611            if in_input_mode {
612                // Confirm the input (create file, rename, etc.)
613                state_clone.with_mut::<ExplorerState, _, _>(|s| {
614                    let _ = s.confirm_input();
615                });
616
617                // Exit Interactor sub-mode back to normal explorer mode
618                let mode = ModeState::with_interactor_id_and_mode(COMPONENT_ID, EditMode::Normal);
619                ctx.emit(RequestModeChange { mode });
620            } else {
621                // Not in input mode - open the selected node (file/directory)
622                ctx.emit(ExplorerOpenNode);
623            }
624
625            ctx.request_render();
626            EventResult::Handled
627        });
628
629        let state_clone = Arc::clone(state);
630        bus.subscribe::<ExplorerCancelInput, _>(100, move |_event, ctx| {
631            // Check if popup is visible
632            let popup_visible = state_clone
633                .with::<ExplorerState, _, _>(|s| s.is_popup_visible())
634                .unwrap_or(false);
635
636            if popup_visible {
637                // Close the popup
638                state_clone.with_mut::<ExplorerState, _, _>(|s| {
639                    s.close_popup();
640                });
641                ctx.request_render();
642                return EventResult::Handled;
643            }
644
645            // Check if in input mode
646            let in_input_mode = state_clone
647                .with::<ExplorerState, _, _>(|s| {
648                    !matches!(s.input_mode, crate::state::ExplorerInputMode::None)
649                })
650                .unwrap_or(false);
651
652            if in_input_mode {
653                // Cancel the input
654                state_clone.with_mut::<ExplorerState, _, _>(|s| {
655                    s.cancel_input();
656                });
657
658                // Exit Interactor sub-mode back to normal explorer mode
659                let mode = ModeState::with_interactor_id_and_mode(COMPONENT_ID, EditMode::Normal);
660                ctx.emit(RequestModeChange { mode });
661            } else {
662                // Not in input mode - return focus to editor
663                ctx.emit(ExplorerFocusEditor);
664            }
665
666            ctx.request_render();
667            EventResult::Handled
668        });
669
670        subscribe_state!(bus, state, ExplorerInputChar, ExplorerState, |s, e| {
671            s.input_char(e.c);
672        });
673
674        subscribe_state!(bus, state, ExplorerInputBackspace, ExplorerState, |s| {
675            s.input_backspace();
676        });
677    }
678
679    /// Subscribe to visual selection mode events
680    fn subscribe_visual_mode(&self, bus: &EventBus, state: &Arc<PluginStateRegistry>) {
681        subscribe_state!(bus, state, ExplorerVisualMode, ExplorerState, |s| {
682            s.enter_visual_mode();
683        });
684
685        subscribe_state!(bus, state, ExplorerToggleSelect, ExplorerState, |s| {
686            s.toggle_select_current();
687        });
688
689        subscribe_state!(bus, state, ExplorerSelectAll, ExplorerState, |s| {
690            s.select_all();
691        });
692
693        subscribe_state!(bus, state, ExplorerExitVisual, ExplorerState, |s| {
694            s.exit_visual_mode();
695        });
696    }
697
698    /// Subscribe to focus and visibility events (toggle, close, focus editor)
699    fn subscribe_focus_visibility(&self, bus: &EventBus, state: &Arc<PluginStateRegistry>) {
700        use reovim_core::event_bus::core_events::RequestFocusChange;
701
702        // Toggle explorer visibility
703        let state_clone = Arc::clone(state);
704        bus.subscribe::<ExplorerToggle, _>(100, move |_event, ctx| {
705            tracing::info!("ExplorerPlugin: ExplorerToggle received");
706
707            let old_visible = state_clone
708                .with::<ExplorerState, _, _>(|e| e.visible)
709                .unwrap_or(false);
710
711            state_clone.with_mut::<ExplorerState, _, _>(|explorer| {
712                explorer.toggle_visibility();
713            });
714
715            let (new_visible, width) = state_clone
716                .with::<ExplorerState, _, _>(|e| (e.visible, e.width))
717                .unwrap_or((false, 0));
718
719            tracing::info!("ExplorerPlugin: Explorer toggled: {} -> {}", old_visible, new_visible);
720
721            // Update left panel width for blocking layout
722            if new_visible {
723                state_clone.set_left_panel_width(width);
724            } else {
725                state_clone.set_left_panel_width(0);
726            }
727
728            // Change focus based on visibility
729            if new_visible {
730                // Explorer is now visible - give it focus
731                ctx.emit(RequestFocusChange {
732                    target: COMPONENT_ID,
733                });
734                tracing::info!("ExplorerPlugin: Requesting focus change to explorer");
735            } else {
736                // Explorer is now hidden - return focus to editor
737                ctx.emit(RequestFocusChange {
738                    target: ComponentId::EDITOR,
739                });
740                tracing::info!("ExplorerPlugin: Requesting focus change to editor");
741            }
742
743            ctx.request_render();
744            tracing::info!("ExplorerPlugin: Render requested");
745            EventResult::Handled
746        });
747
748        // Close explorer and return focus to editor
749        let state_clone = Arc::clone(state);
750        bus.subscribe::<ExplorerClose, _>(100, move |_event, ctx| {
751            tracing::info!("ExplorerPlugin: ExplorerClose received");
752
753            state_clone.with_mut::<ExplorerState, _, _>(|explorer| {
754                explorer.visible = false;
755            });
756
757            // Clear left panel width since explorer is hidden
758            state_clone.set_left_panel_width(0);
759
760            ctx.emit(RequestFocusChange {
761                target: ComponentId::EDITOR,
762            });
763            tracing::info!("ExplorerPlugin: Requesting focus change to editor");
764
765            ctx.request_render();
766            EventResult::Handled
767        });
768
769        // Focus editor (without closing explorer)
770        bus.subscribe::<ExplorerFocusEditor, _>(100, move |_event, ctx| {
771            tracing::info!("ExplorerPlugin: ExplorerFocusEditor received");
772
773            ctx.emit(RequestFocusChange {
774                target: ComponentId::EDITOR,
775            });
776            tracing::info!("ExplorerPlugin: Requesting focus change to editor");
777
778            ctx.request_render();
779            EventResult::Handled
780        });
781    }
782
783    /// Subscribe to popup events (show info, close popup)
784    fn subscribe_popup(&self, bus: &EventBus, state: &Arc<PluginStateRegistry>) {
785        subscribe_state!(bus, state, ExplorerShowInfo, ExplorerState, |s| {
786            s.show_file_details();
787        });
788
789        subscribe_state!(bus, state, ExplorerClosePopup, ExplorerState, |s| {
790            s.close_popup();
791        });
792    }
793}
794
795/// Component ID for the explorer
796pub const COMPONENT_ID: ComponentId = ComponentId("explorer");
797
798#[allow(clippy::unused_self)]
799impl ExplorerPlugin {
800    fn register_display_info(&self, ctx: &mut PluginContext) {
801        use reovim_core::highlight::{Color, Style};
802
803        // Orange background for file explorer
804        let orange = Color::Rgb {
805            r: 255,
806            g: 153,
807            b: 51,
808        };
809        let fg = Color::Rgb {
810            r: 33,
811            g: 37,
812            b: 43,
813        };
814        let style = Style::new().fg(fg).bg(orange).bold();
815
816        ctx.display_info(COMPONENT_ID)
817            .default(" EXPLORER ", "󰙅 ", style)
818            .register();
819    }
820
821    fn register_navigation_commands(&self, ctx: &PluginContext) {
822        let _ = ctx.register_command(ExplorerCursorUp::new(1));
823        let _ = ctx.register_command(ExplorerCursorDown::new(1));
824        let _ = ctx.register_command(ExplorerPageUp);
825        let _ = ctx.register_command(ExplorerPageDown);
826        let _ = ctx.register_command(ExplorerGotoFirst);
827        let _ = ctx.register_command(ExplorerGotoLast);
828        let _ = ctx.register_command(ExplorerGoToParent);
829        let _ = ctx.register_command(ExplorerChangeRoot);
830    }
831
832    fn register_tree_commands(&self, ctx: &PluginContext) {
833        let _ = ctx.register_command(ExplorerToggle);
834        let _ = ctx.register_command(ExplorerToggleNode);
835        let _ = ctx.register_command(ExplorerOpenNode);
836        let _ = ctx.register_command(ExplorerCloseParent);
837        let _ = ctx.register_command(ExplorerRefresh);
838        let _ = ctx.register_command(ExplorerToggleHidden);
839        let _ = ctx.register_command(ExplorerToggleSizes);
840        let _ = ctx.register_command(ExplorerClose);
841        let _ = ctx.register_command(ExplorerFocusEditor);
842        let _ = ctx.register_command(ExplorerShowInfo);
843        let _ = ctx.register_command(ExplorerClosePopup);
844        let _ = ctx.register_command(ExplorerCopyPath);
845    }
846
847    fn register_file_commands(&self, ctx: &PluginContext) {
848        let _ = ctx.register_command(ExplorerCreateFile);
849        let _ = ctx.register_command(ExplorerCreateDir);
850        let _ = ctx.register_command(ExplorerRename);
851        let _ = ctx.register_command(ExplorerDelete);
852        let _ = ctx.register_command(ExplorerStartFilter);
853        let _ = ctx.register_command(ExplorerClearFilter);
854    }
855
856    fn register_clipboard_commands(&self, ctx: &PluginContext) {
857        let _ = ctx.register_command(ExplorerYank);
858        let _ = ctx.register_command(ExplorerCut);
859        let _ = ctx.register_command(ExplorerPaste);
860    }
861
862    fn register_visual_commands(&self, ctx: &PluginContext) {
863        let _ = ctx.register_command(ExplorerVisualMode);
864        let _ = ctx.register_command(ExplorerToggleSelect);
865        let _ = ctx.register_command(ExplorerSelectAll);
866        let _ = ctx.register_command(ExplorerExitVisual);
867    }
868
869    fn register_input_commands(&self, ctx: &PluginContext) {
870        let _ = ctx.register_command(ExplorerConfirmInput);
871        let _ = ctx.register_command(ExplorerCancelInput);
872        let _ = ctx.register_command(ExplorerInputBackspace);
873    }
874
875    fn register_keybindings(&self, ctx: &mut PluginContext) {
876        use reovim_core::bind::SubModeKind;
877
878        let editor_normal = KeymapScope::editor_normal();
879        let explorer_normal = KeymapScope::Component {
880            id: COMPONENT_ID,
881            mode: EditModeKind::Normal,
882        };
883        let explorer_interactor = KeymapScope::SubMode(SubModeKind::Interactor(COMPONENT_ID));
884
885        // Global keybinding: Space+e to toggle explorer
886        ctx.bind_key_scoped(
887            editor_normal,
888            keys![Space 'e'],
889            CommandRef::Registered(command_id::TOGGLE_EXPLORER),
890        );
891
892        // Explorer-specific keybindings (when explorer is focused)
893
894        // Navigation
895        ctx.bind_key_scoped(
896            explorer_normal.clone(),
897            keys!['j'],
898            CommandRef::Registered(command_id::CURSOR_DOWN),
899        );
900        ctx.bind_key_scoped(
901            explorer_normal.clone(),
902            keys!['k'],
903            CommandRef::Registered(command_id::CURSOR_UP),
904        );
905        ctx.bind_key_scoped(
906            explorer_normal.clone(),
907            keys![(Ctrl 'd')],
908            CommandRef::Registered(command_id::PAGE_DOWN),
909        );
910        ctx.bind_key_scoped(
911            explorer_normal.clone(),
912            keys![(Ctrl 'u')],
913            CommandRef::Registered(command_id::PAGE_UP),
914        );
915        ctx.bind_key_scoped(
916            explorer_normal.clone(),
917            keys!['g' 'g'],
918            CommandRef::Registered(command_id::GOTO_FIRST),
919        );
920        ctx.bind_key_scoped(
921            explorer_normal.clone(),
922            keys!['G'],
923            CommandRef::Registered(command_id::GOTO_LAST),
924        );
925        ctx.bind_key_scoped(
926            explorer_normal.clone(),
927            keys!['-'],
928            CommandRef::Registered(command_id::GO_TO_PARENT),
929        );
930        ctx.bind_key_scoped(
931            explorer_normal.clone(),
932            keys![Backspace],
933            CommandRef::Registered(command_id::GO_TO_PARENT),
934        );
935        ctx.bind_key_scoped(
936            explorer_normal.clone(),
937            keys!['.'],
938            CommandRef::Registered(command_id::CHANGE_ROOT),
939        );
940
941        // Tree operations / Input confirmation
942        // Enter: Confirm input if in input mode, otherwise open node
943        ctx.bind_key_scoped(
944            explorer_normal.clone(),
945            keys![Enter],
946            CommandRef::Registered(command_id::CONFIRM_INPUT),
947        );
948        ctx.bind_key_scoped(
949            explorer_normal.clone(),
950            keys!['l'],
951            CommandRef::Registered(command_id::OPEN_NODE),
952        );
953        ctx.bind_key_scoped(
954            explorer_normal.clone(),
955            keys!['h'],
956            CommandRef::Registered(command_id::CLOSE_PARENT),
957        );
958        ctx.bind_key_scoped(
959            explorer_normal.clone(),
960            keys![Space],
961            CommandRef::Registered(command_id::TOGGLE_NODE),
962        );
963        ctx.bind_key_scoped(
964            explorer_normal.clone(),
965            keys!['R'],
966            CommandRef::Registered(command_id::REFRESH),
967        );
968        ctx.bind_key_scoped(
969            explorer_normal.clone(),
970            keys!['H'],
971            CommandRef::Registered(command_id::TOGGLE_HIDDEN),
972        );
973        ctx.bind_key_scoped(
974            explorer_normal.clone(),
975            keys!['s'],
976            CommandRef::Registered(command_id::SHOW_INFO),
977        );
978        ctx.bind_key_scoped(
979            explorer_normal.clone(),
980            keys!['t'],
981            CommandRef::Registered(command_id::COPY_PATH),
982        );
983        ctx.bind_key_scoped(
984            explorer_normal.clone(),
985            keys!['q'],
986            CommandRef::Registered(command_id::CLOSE),
987        );
988        // Escape: Cancel input if in input mode, otherwise focus editor
989        ctx.bind_key_scoped(
990            explorer_normal.clone(),
991            keys![Escape],
992            CommandRef::Registered(command_id::CANCEL_INPUT),
993        );
994
995        // File operations
996        ctx.bind_key_scoped(
997            explorer_normal.clone(),
998            keys!['a'],
999            CommandRef::Registered(command_id::CREATE_FILE),
1000        );
1001        ctx.bind_key_scoped(
1002            explorer_normal.clone(),
1003            keys!['A'],
1004            CommandRef::Registered(command_id::CREATE_DIR),
1005        );
1006        ctx.bind_key_scoped(
1007            explorer_normal.clone(),
1008            keys!['r'],
1009            CommandRef::Registered(command_id::RENAME),
1010        );
1011        ctx.bind_key_scoped(
1012            explorer_normal.clone(),
1013            keys!['d'],
1014            CommandRef::Registered(command_id::DELETE),
1015        );
1016        ctx.bind_key_scoped(
1017            explorer_normal.clone(),
1018            keys!['/'],
1019            CommandRef::Registered(command_id::FILTER),
1020        );
1021        ctx.bind_key_scoped(
1022            explorer_normal.clone(),
1023            keys!['c'],
1024            CommandRef::Registered(command_id::CLEAR_FILTER),
1025        );
1026
1027        // Clipboard
1028        ctx.bind_key_scoped(
1029            explorer_normal.clone(),
1030            keys!['y'],
1031            CommandRef::Registered(command_id::YANK),
1032        );
1033        ctx.bind_key_scoped(
1034            explorer_normal.clone(),
1035            keys!['x'],
1036            CommandRef::Registered(command_id::CUT),
1037        );
1038        ctx.bind_key_scoped(
1039            explorer_normal.clone(),
1040            keys!['p'],
1041            CommandRef::Registered(command_id::PASTE),
1042        );
1043
1044        // Visual mode
1045        ctx.bind_key_scoped(
1046            explorer_normal.clone(),
1047            keys!['v'],
1048            CommandRef::Registered(command_id::VISUAL_MODE),
1049        );
1050
1051        // Interactor sub-mode keybindings (for input mode: CreateFile, Rename, etc.)
1052        // Enter: Confirm input
1053        ctx.bind_key_scoped(
1054            explorer_interactor.clone(),
1055            keys![Enter],
1056            CommandRef::Registered(command_id::CONFIRM_INPUT),
1057        );
1058        // Escape: Cancel input
1059        ctx.bind_key_scoped(
1060            explorer_interactor.clone(),
1061            keys![Escape],
1062            CommandRef::Registered(command_id::CANCEL_INPUT),
1063        );
1064        // Backspace: Delete character from input
1065        ctx.bind_key_scoped(
1066            explorer_interactor,
1067            keys![Backspace],
1068            CommandRef::Registered(command_id::INPUT_BACKSPACE),
1069        );
1070
1071        tracing::info!(
1072            "ExplorerPlugin: registered keybinding Space+e, explorer navigation, and interactor input"
1073        );
1074    }
1075}