Skip to main content

fresh/app/
help_actions.rs

1//! Help-buffer orchestrators.
2//!
3//! `open_help_manual` and `open_keyboard_shortcuts` create read-only
4//! virtual buffers populated with the manual text or keybinding listing.
5//! Both check for an existing help buffer first to avoid duplicates.
6//! All bodies live on `impl Window` — they operate on this window's
7//! buffer storage and use window-side `create_virtual_buffer` +
8//! `set_active_buffer`.
9
10use crossterm::event::{KeyCode, KeyModifiers};
11
12use super::help;
13use crate::app::window::Window;
14use crate::app::Editor;
15use crate::input::buffer_mode::BufferMode;
16use crate::input::keybindings::{Action, KeyContext};
17
18/// Mode name carried on the help/manual virtual buffers. Referenced
19/// both when creating the buffer (so input dispatch resolves keys
20/// against this context) and when installing the close binding.
21pub(super) const HELP_PANEL_MODE: &str = "special";
22
23impl Editor {
24    /// Register the built-in `"special"` buffer mode used by the help
25    /// manual and keyboard-shortcuts viewers. Both viewers document
26    /// "Press q to close" inline, so we bind `q -> close_tab` here.
27    /// `inherit_normal_bindings` keeps cursor motion / copy / search
28    /// usable even though `editing_disabled` blocks edits.
29    ///
30    /// Idempotent: clears any prior binding for this mode context
31    /// before installing, so it's safe to call on every viewer open
32    /// (also restores the binding after a config reload, which
33    /// reinitializes `KeybindingResolver`).
34    ///
35    /// Avoids calling into `handle_define_mode` because that path
36    /// lives under `#[cfg(feature = "plugins")]`; we only need the
37    /// keybinding + mode-registry slice here, which is always built.
38    pub(super) fn ensure_help_panel_mode_registered(&mut self) {
39        let mode_ctx = KeyContext::Mode(HELP_PANEL_MODE.to_string());
40        {
41            let mut kb = self.keybindings.write().unwrap();
42            kb.clear_plugin_defaults_for_mode(HELP_PANEL_MODE);
43            kb.set_mode_inherits_normal_bindings(HELP_PANEL_MODE, true);
44            kb.load_plugin_default(
45                mode_ctx,
46                KeyCode::Char('q'),
47                KeyModifiers::NONE,
48                Action::CloseTab,
49            );
50        }
51        self.mode_registry.register(
52            BufferMode::new(HELP_PANEL_MODE)
53                .with_read_only(true)
54                .with_inherit_normal_bindings(true),
55        );
56    }
57}
58
59impl Window {
60    /// Open the built-in help manual in a read-only buffer.
61    ///
62    /// If a help manual buffer already exists, switch to it instead of
63    /// creating a new one.
64    pub fn open_help_manual(&mut self) {
65        // Check if help buffer already exists.
66        let existing_buffer = self
67            .buffer_metadata
68            .iter()
69            .find(|(_, m)| m.display_name == help::HELP_MANUAL_BUFFER_NAME)
70            .map(|(id, _)| *id);
71
72        if let Some(buffer_id) = existing_buffer {
73            self.set_active_buffer(buffer_id);
74            return;
75        }
76
77        // Create new help buffer with "special" mode (has 'q' to close).
78        let buffer_id = self.create_virtual_buffer(
79            help::HELP_MANUAL_BUFFER_NAME.to_string(),
80            HELP_PANEL_MODE.to_string(),
81            true,
82        );
83
84        if let Some(state) = self.buffers.get_mut(&buffer_id) {
85            state.buffer.insert(0, help::HELP_MANUAL_CONTENT);
86            state.buffer.clear_modified();
87            state.editing_disabled = true;
88            state.margins.configure_for_line_numbers(false);
89        }
90
91        self.set_active_buffer(buffer_id);
92    }
93
94    /// Open the keyboard shortcuts viewer in a read-only buffer.
95    ///
96    /// If a keyboard shortcuts buffer already exists, switch to it
97    /// instead of creating a new one. The shortcuts are dynamically
98    /// generated from the current keybindings configuration.
99    pub fn open_keyboard_shortcuts(&mut self) {
100        let existing_buffer = self
101            .buffer_metadata
102            .iter()
103            .find(|(_, m)| m.display_name == help::KEYBOARD_SHORTCUTS_BUFFER_NAME)
104            .map(|(id, _)| *id);
105
106        if let Some(buffer_id) = existing_buffer {
107            self.set_active_buffer(buffer_id);
108            return;
109        }
110
111        // Get all keybindings from this window's resources.
112        let bindings = self
113            .resources
114            .keybindings
115            .read()
116            .unwrap()
117            .get_all_bindings();
118
119        // Format the keybindings as readable text.
120        let mut content = String::from("Keyboard Shortcuts\n");
121        content.push_str("==================\n\n");
122        content.push_str("Press 'q' to close this buffer.\n\n");
123
124        let mut current_context = String::new();
125        for (key, action) in &bindings {
126            let (context, action_name) = if let Some(bracket_end) = action.find("] ") {
127                let ctx = &action[1..bracket_end];
128                let name = &action[bracket_end + 2..];
129                (ctx.to_string(), name.to_string())
130            } else {
131                ("Normal".to_string(), action.clone())
132            };
133
134            if context != current_context {
135                if !current_context.is_empty() {
136                    content.push('\n');
137                }
138                content.push_str(&format!("── {} Mode ──\n\n", context));
139                current_context = context;
140            }
141
142            content.push_str(&format!("  {:20} {}\n", key, action_name));
143        }
144
145        let buffer_id = self.create_virtual_buffer(
146            help::KEYBOARD_SHORTCUTS_BUFFER_NAME.to_string(),
147            HELP_PANEL_MODE.to_string(),
148            true,
149        );
150
151        if let Some(state) = self.buffers.get_mut(&buffer_id) {
152            state.buffer.insert(0, &content);
153            state.buffer.clear_modified();
154            state.editing_disabled = true;
155            state.margins.configure_for_line_numbers(false);
156        }
157
158        self.set_active_buffer(buffer_id);
159    }
160}