Skip to main content

fresh_core/
action.rs

1use serde::{Deserialize, Serialize};
2
3/// Context in which a keybinding is active
4#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize, ts_rs::TS)]
5#[ts(export)]
6pub enum KeyContext {
7    /// Global bindings that work in all contexts (checked first with highest priority)
8    Global,
9    /// Normal editing mode
10    Normal,
11    /// Prompt/minibuffer is active
12    Prompt,
13    /// Popup window is visible
14    Popup,
15    /// File explorer has focus
16    FileExplorer,
17    /// Menu bar is active
18    Menu,
19    /// Terminal has focus
20    Terminal,
21    /// Settings modal is active
22    Settings,
23    /// Buffer-local mode context (e.g. "search-replace-list")
24    Mode(String),
25}
26
27impl KeyContext {
28    /// Check if a context should allow input
29    pub fn allows_text_input(&self) -> bool {
30        matches!(self, Self::Normal | Self::Prompt)
31    }
32
33    /// Parse context from a "when" string
34    pub fn from_when_clause(when: &str) -> Option<Self> {
35        let trimmed = when.trim();
36        if let Some(mode_name) = trimmed.strip_prefix("mode:") {
37            return Some(Self::Mode(mode_name.to_string()));
38        }
39        Some(match trimmed {
40            "global" => Self::Global,
41            "prompt" => Self::Prompt,
42            "popup" => Self::Popup,
43            "fileExplorer" | "file_explorer" => Self::FileExplorer,
44            "normal" => Self::Normal,
45            "menu" => Self::Menu,
46            "terminal" => Self::Terminal,
47            "settings" => Self::Settings,
48            _ => return None,
49        })
50    }
51
52    /// Convert context to "when" clause string
53    pub fn to_when_clause(&self) -> String {
54        match self {
55            Self::Global => "global".to_string(),
56            Self::Normal => "normal".to_string(),
57            Self::Prompt => "prompt".to_string(),
58            Self::Popup => "popup".to_string(),
59            Self::FileExplorer => "fileExplorer".to_string(),
60            Self::Menu => "menu".to_string(),
61            Self::Terminal => "terminal".to_string(),
62            Self::Settings => "settings".to_string(),
63            Self::Mode(name) => format!("mode:{}", name),
64        }
65    }
66}
67
68/// High-level actions that can be performed in the editor
69#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ts_rs::TS)]
70#[ts(export)]
71pub enum Action {
72    // Character input
73    InsertChar(char),
74    InsertNewline,
75    InsertTab,
76
77    // Basic movement
78    MoveLeft,
79    MoveRight,
80    MoveUp,
81    MoveDown,
82    MoveWordLeft,
83    MoveWordRight,
84    MoveLineStart,
85    MoveLineEnd,
86    MovePageUp,
87    MovePageDown,
88    MoveDocumentStart,
89    MoveDocumentEnd,
90
91    // Selection movement (extends selection while moving)
92    SelectLeft,
93    SelectRight,
94    SelectUp,
95    SelectDown,
96    SelectWordLeft,
97    SelectWordRight,
98    SelectLineStart,
99    SelectLineEnd,
100    SelectDocumentStart,
101    SelectDocumentEnd,
102    SelectPageUp,
103    SelectPageDown,
104    SelectAll,
105    SelectWord,
106    SelectLine,
107    ExpandSelection,
108
109    // Block/rectangular selection (column-wise)
110    BlockSelectLeft,
111    BlockSelectRight,
112    BlockSelectUp,
113    BlockSelectDown,
114
115    // Editing
116    DeleteBackward,
117    DeleteForward,
118    DeleteWordBackward,
119    DeleteWordForward,
120    DeleteLine,
121    DeleteToLineEnd,
122    DeleteToLineStart,
123    TransposeChars,
124    OpenLine,
125
126    // View
127    Recenter,
128
129    // Selection
130    SetMark,
131    CancelMark,
132    ClearMark,
133
134    // Clipboard
135    Copy,
136    CopyWithTheme(String),
137    Cut,
138    Paste,
139
140    // Vi-style yank (copy without selection, then restore cursor)
141    YankWordForward,
142    YankWordBackward,
143    YankToLineEnd,
144    YankToLineStart,
145
146    // Multi-cursor
147    AddCursorAbove,
148    AddCursorBelow,
149    AddCursorNextMatch,
150    RemoveSecondaryCursors,
151
152    // File operations
153    Save,
154    SaveAs,
155    Open,
156    SwitchProject,
157    New,
158    Close,
159    CloseTab,
160    Quit,
161    Revert,
162    ToggleAutoRevert,
163    FormatBuffer,
164
165    // Navigation
166    GotoLine,
167    ScanLineIndex,
168    GoToMatchingBracket,
169    JumpToNextError,
170    JumpToPreviousError,
171
172    // Smart editing
173    SmartHome,
174    DedentSelection,
175    ToggleComment,
176    /// Cycle through dabbrev completions (Emacs Alt+/ style).
177    /// Unlike popup-based completion, this inserts the best match directly
178    /// and cycles through alternatives on repeated invocations.
179    DabbrevExpand,
180
181    // Bookmarks
182    SetBookmark(char),
183    JumpToBookmark(char),
184    ClearBookmark(char),
185    ListBookmarks,
186
187    // Search options
188    ToggleSearchCaseSensitive,
189    ToggleSearchWholeWord,
190    ToggleSearchRegex,
191    ToggleSearchConfirmEach,
192
193    // Macros
194    StartMacroRecording,
195    StopMacroRecording,
196    PlayMacro(char),
197    ToggleMacroRecording(char),
198    ShowMacro(char),
199    ListMacros,
200    PromptRecordMacro,
201    PromptPlayMacro,
202    PlayLastMacro,
203
204    // Bookmarks (prompt-based)
205    PromptSetBookmark,
206    PromptJumpToBookmark,
207
208    // Undo/redo
209    Undo,
210    Redo,
211
212    // View
213    ScrollUp,
214    ScrollDown,
215    ShowHelp,
216    ShowKeyboardShortcuts,
217    ShowWarnings,
218    ShowLspStatus,
219    ClearWarnings,
220    CommandPalette,
221    ToggleLineWrap,
222    ToggleReadOnly,
223    TogglePageView,
224    SetPageWidth,
225    InspectThemeAtCursor,
226    SelectTheme,
227    SelectKeybindingMap,
228    SelectCursorStyle,
229    SelectLocale,
230
231    // Buffer/tab navigation
232    NextBuffer,
233    PrevBuffer,
234    SwitchToPreviousTab,
235    SwitchToTabByName,
236
237    // Tab scrolling
238    ScrollTabsLeft,
239    ScrollTabsRight,
240
241    // Position history navigation
242    NavigateBack,
243    NavigateForward,
244
245    // Split view operations
246    SplitHorizontal,
247    SplitVertical,
248    CloseSplit,
249    NextSplit,
250    PrevSplit,
251    IncreaseSplitSize,
252    DecreaseSplitSize,
253    ToggleMaximizeSplit,
254
255    // Prompt mode actions
256    PromptConfirm,
257    /// PromptConfirm with recorded text for macro playback
258    PromptConfirmWithText(String),
259    PromptCancel,
260    PromptBackspace,
261    PromptDelete,
262    PromptMoveLeft,
263    PromptMoveRight,
264    PromptMoveStart,
265    PromptMoveEnd,
266    PromptSelectPrev,
267    PromptSelectNext,
268    PromptPageUp,
269    PromptPageDown,
270    PromptAcceptSuggestion,
271    PromptMoveWordLeft,
272    PromptMoveWordRight,
273    // Advanced prompt editing (word operations, clipboard)
274    PromptDeleteWordForward,
275    PromptDeleteWordBackward,
276    PromptDeleteToLineEnd,
277    PromptCopy,
278    PromptCut,
279    PromptPaste,
280    // Prompt selection actions
281    PromptMoveLeftSelecting,
282    PromptMoveRightSelecting,
283    PromptMoveHomeSelecting,
284    PromptMoveEndSelecting,
285    PromptSelectWordLeft,
286    PromptSelectWordRight,
287    PromptSelectAll,
288
289    // File browser actions
290    FileBrowserToggleHidden,
291
292    // Popup mode actions
293    PopupSelectNext,
294    PopupSelectPrev,
295    PopupPageUp,
296    PopupPageDown,
297    PopupConfirm,
298    PopupCancel,
299
300    // File explorer operations
301    ToggleFileExplorer,
302    // Menu bar visibility
303    ToggleMenuBar,
304    // Tab bar visibility
305    ToggleTabBar,
306    FocusFileExplorer,
307    FocusEditor,
308    FileExplorerUp,
309    FileExplorerDown,
310    FileExplorerPageUp,
311    FileExplorerPageDown,
312    FileExplorerExpand,
313    FileExplorerCollapse,
314    FileExplorerOpen,
315    FileExplorerRefresh,
316    FileExplorerNewFile,
317    FileExplorerNewDirectory,
318    FileExplorerDelete,
319    FileExplorerRename,
320    FileExplorerToggleHidden,
321    FileExplorerToggleGitignored,
322
323    // LSP operations
324    LspCompletion,
325    LspGotoDefinition,
326    LspReferences,
327    LspImplementation,
328    LspRename,
329    LspHover,
330    LspSignatureHelp,
331    LspCodeActions,
332    LspRestart,
333    LspStop,
334    ToggleInlayHints,
335    ToggleMouseHover,
336
337    // View toggles
338    ToggleLineNumbers,
339    ToggleScrollSync,
340    ToggleMouseCapture,
341    ToggleDebugHighlights, // Debug mode: show highlight/overlay byte ranges
342    SetBackground,
343    SetBackgroundBlend,
344
345    // Buffer settings (per-buffer overrides)
346    SetTabSize,
347    SetLineEnding,
348    ToggleIndentationStyle,
349    ToggleTabIndicators,
350    ResetBufferSettings,
351
352    // Config operations
353    DumpConfig,
354
355    // Search and replace
356    Search,
357    FindInSelection,
358    FindNext,
359    FindPrevious,
360    FindSelectionNext,     // Quick find next occurrence of selection (Ctrl+F3)
361    FindSelectionPrevious, // Quick find previous occurrence of selection (Ctrl+Shift+F3)
362    Replace,
363    QueryReplace, // Interactive replace (y/n/!/q for each match)
364
365    // Menu navigation
366    MenuActivate,     // Open menu bar (Alt or F10)
367    MenuClose,        // Close menu (Esc)
368    MenuLeft,         // Navigate to previous menu
369    MenuRight,        // Navigate to next menu
370    MenuUp,           // Navigate to previous item in menu
371    MenuDown,         // Navigate to next item in menu
372    MenuExecute,      // Execute selected menu item (Enter)
373    MenuOpen(String), // Open a specific menu by name (e.g., "File", "Edit")
374
375    // Keybinding map switching
376    SwitchKeybindingMap(String), // Switch to a named keybinding map (e.g., "default", "emacs", "vscode")
377
378    // Plugin custom actions
379    PluginAction(String),
380
381    // Load the current buffer's contents as a plugin
382    LoadPluginFromBuffer,
383
384    // Settings operations
385    OpenSettings,        // Open the settings modal
386    CloseSettings,       // Close the settings modal
387    SettingsSave,        // Save settings changes
388    SettingsReset,       // Reset current setting to default
389    SettingsToggleFocus, // Toggle focus between category and settings panels
390    SettingsActivate,    // Activate/toggle the current setting
391    SettingsSearch,      // Start search in settings
392    SettingsHelp,        // Show settings help overlay
393    SettingsIncrement,   // Increment number value or next dropdown option
394    SettingsDecrement,   // Decrement number value or previous dropdown option
395
396    // Terminal operations
397    OpenTerminal,          // Open a new terminal in the current split
398    OpenTerminalRight,     // Open a new terminal in a split to the right (vertical split)
399    OpenTerminalBelow,     // Open a new terminal in a split below (horizontal split)
400    CloseTerminal,         // Close the current terminal
401    FocusTerminal,         // Focus the terminal buffer (if viewing terminal, focus input)
402    TerminalEscape,        // Escape from terminal mode back to editor
403    ToggleKeyboardCapture, // Toggle keyboard capture mode (all keys go to terminal)
404    TerminalPaste,         // Paste clipboard contents into terminal as a single batch
405
406    // Shell command operations
407    ShellCommand,        // Run shell command on buffer/selection, output to new buffer
408    ShellCommandReplace, // Run shell command on buffer/selection, replace content
409
410    // Case conversion
411    ToUpperCase, // Convert selection to uppercase
412    ToLowerCase, // Convert selection to lowercase
413
414    // Input calibration
415    CalibrateInput, // Open the input calibration wizard
416
417    // No-op
418    None,
419}
420
421#[cfg(test)]
422mod tests {
423    use super::*;
424
425    /// Every variant must round-trip through `to_when_clause` → `from_when_clause`
426    /// as the identity. This is the core contract these two functions implement.
427    #[test]
428    fn when_clause_is_a_roundtrip_for_every_variant() {
429        let variants = [
430            KeyContext::Global,
431            KeyContext::Normal,
432            KeyContext::Prompt,
433            KeyContext::Popup,
434            KeyContext::FileExplorer,
435            KeyContext::Menu,
436            KeyContext::Terminal,
437            KeyContext::Settings,
438            KeyContext::Mode("search-replace-list".into()),
439            KeyContext::Mode(String::new()),
440        ];
441        for ctx in &variants {
442            let clause = ctx.to_when_clause();
443            assert_eq!(
444                KeyContext::from_when_clause(&clause).as_ref(),
445                Some(ctx),
446                "roundtrip failed: {:?} → {:?}",
447                ctx,
448                clause
449            );
450        }
451    }
452
453    /// Non-canonical inputs the parser must also accept, plus invalid inputs
454    /// it must reject. Not covered by the roundtrip, since `to_when_clause`
455    /// only emits canonical forms.
456    #[test]
457    fn from_when_clause_handles_aliases_whitespace_and_rejects_unknown() {
458        // snake_case alias for fileExplorer
459        assert_eq!(
460            KeyContext::from_when_clause("file_explorer"),
461            Some(KeyContext::FileExplorer)
462        );
463        // Surrounding whitespace is trimmed
464        assert_eq!(
465            KeyContext::from_when_clause("  prompt  "),
466            Some(KeyContext::Prompt)
467        );
468        // Unknown / case-mismatched / empty → None
469        assert_eq!(KeyContext::from_when_clause("nonsense"), None);
470        assert_eq!(KeyContext::from_when_clause("GLOBAL"), None);
471        assert_eq!(KeyContext::from_when_clause(""), None);
472    }
473
474    /// `allows_text_input` is true iff the context is `Normal` or `Prompt`.
475    #[test]
476    fn allows_text_input_iff_normal_or_prompt() {
477        for ctx in [
478            KeyContext::Global,
479            KeyContext::Normal,
480            KeyContext::Prompt,
481            KeyContext::Popup,
482            KeyContext::FileExplorer,
483            KeyContext::Menu,
484            KeyContext::Terminal,
485            KeyContext::Settings,
486            KeyContext::Mode("foo".into()),
487        ] {
488            let expected = matches!(ctx, KeyContext::Normal | KeyContext::Prompt);
489            assert_eq!(
490                ctx.allows_text_input(),
491                expected,
492                "{:?} text-input expectation violated",
493                ctx
494            );
495        }
496    }
497}