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    LspRename,
328    LspHover,
329    LspSignatureHelp,
330    LspCodeActions,
331    LspRestart,
332    LspStop,
333    ToggleInlayHints,
334    ToggleMouseHover,
335
336    // View toggles
337    ToggleLineNumbers,
338    ToggleScrollSync,
339    ToggleMouseCapture,
340    ToggleDebugHighlights, // Debug mode: show highlight/overlay byte ranges
341    SetBackground,
342    SetBackgroundBlend,
343
344    // Buffer settings (per-buffer overrides)
345    SetTabSize,
346    SetLineEnding,
347    ToggleIndentationStyle,
348    ToggleTabIndicators,
349    ResetBufferSettings,
350
351    // Config operations
352    DumpConfig,
353
354    // Search and replace
355    Search,
356    FindInSelection,
357    FindNext,
358    FindPrevious,
359    FindSelectionNext,     // Quick find next occurrence of selection (Ctrl+F3)
360    FindSelectionPrevious, // Quick find previous occurrence of selection (Ctrl+Shift+F3)
361    Replace,
362    QueryReplace, // Interactive replace (y/n/!/q for each match)
363
364    // Menu navigation
365    MenuActivate,     // Open menu bar (Alt or F10)
366    MenuClose,        // Close menu (Esc)
367    MenuLeft,         // Navigate to previous menu
368    MenuRight,        // Navigate to next menu
369    MenuUp,           // Navigate to previous item in menu
370    MenuDown,         // Navigate to next item in menu
371    MenuExecute,      // Execute selected menu item (Enter)
372    MenuOpen(String), // Open a specific menu by name (e.g., "File", "Edit")
373
374    // Keybinding map switching
375    SwitchKeybindingMap(String), // Switch to a named keybinding map (e.g., "default", "emacs", "vscode")
376
377    // Plugin custom actions
378    PluginAction(String),
379
380    // Load the current buffer's contents as a plugin
381    LoadPluginFromBuffer,
382
383    // Settings operations
384    OpenSettings,        // Open the settings modal
385    CloseSettings,       // Close the settings modal
386    SettingsSave,        // Save settings changes
387    SettingsReset,       // Reset current setting to default
388    SettingsToggleFocus, // Toggle focus between category and settings panels
389    SettingsActivate,    // Activate/toggle the current setting
390    SettingsSearch,      // Start search in settings
391    SettingsHelp,        // Show settings help overlay
392    SettingsIncrement,   // Increment number value or next dropdown option
393    SettingsDecrement,   // Decrement number value or previous dropdown option
394
395    // Terminal operations
396    OpenTerminal,          // Open a new terminal in the current split
397    CloseTerminal,         // Close the current terminal
398    FocusTerminal,         // Focus the terminal buffer (if viewing terminal, focus input)
399    TerminalEscape,        // Escape from terminal mode back to editor
400    ToggleKeyboardCapture, // Toggle keyboard capture mode (all keys go to terminal)
401    TerminalPaste,         // Paste clipboard contents into terminal as a single batch
402
403    // Shell command operations
404    ShellCommand,        // Run shell command on buffer/selection, output to new buffer
405    ShellCommandReplace, // Run shell command on buffer/selection, replace content
406
407    // Case conversion
408    ToUpperCase, // Convert selection to uppercase
409    ToLowerCase, // Convert selection to lowercase
410
411    // Input calibration
412    CalibrateInput, // Open the input calibration wizard
413
414    // No-op
415    None,
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421
422    /// Every variant must round-trip through `to_when_clause` → `from_when_clause`
423    /// as the identity. This is the core contract these two functions implement.
424    #[test]
425    fn when_clause_is_a_roundtrip_for_every_variant() {
426        let variants = [
427            KeyContext::Global,
428            KeyContext::Normal,
429            KeyContext::Prompt,
430            KeyContext::Popup,
431            KeyContext::FileExplorer,
432            KeyContext::Menu,
433            KeyContext::Terminal,
434            KeyContext::Settings,
435            KeyContext::Mode("search-replace-list".into()),
436            KeyContext::Mode(String::new()),
437        ];
438        for ctx in &variants {
439            let clause = ctx.to_when_clause();
440            assert_eq!(
441                KeyContext::from_when_clause(&clause).as_ref(),
442                Some(ctx),
443                "roundtrip failed: {:?} → {:?}",
444                ctx,
445                clause
446            );
447        }
448    }
449
450    /// Non-canonical inputs the parser must also accept, plus invalid inputs
451    /// it must reject. Not covered by the roundtrip, since `to_when_clause`
452    /// only emits canonical forms.
453    #[test]
454    fn from_when_clause_handles_aliases_whitespace_and_rejects_unknown() {
455        // snake_case alias for fileExplorer
456        assert_eq!(
457            KeyContext::from_when_clause("file_explorer"),
458            Some(KeyContext::FileExplorer)
459        );
460        // Surrounding whitespace is trimmed
461        assert_eq!(
462            KeyContext::from_when_clause("  prompt  "),
463            Some(KeyContext::Prompt)
464        );
465        // Unknown / case-mismatched / empty → None
466        assert_eq!(KeyContext::from_when_clause("nonsense"), None);
467        assert_eq!(KeyContext::from_when_clause("GLOBAL"), None);
468        assert_eq!(KeyContext::from_when_clause(""), None);
469    }
470
471    /// `allows_text_input` is true iff the context is `Normal` or `Prompt`.
472    #[test]
473    fn allows_text_input_iff_normal_or_prompt() {
474        for ctx in [
475            KeyContext::Global,
476            KeyContext::Normal,
477            KeyContext::Prompt,
478            KeyContext::Popup,
479            KeyContext::FileExplorer,
480            KeyContext::Menu,
481            KeyContext::Terminal,
482            KeyContext::Settings,
483            KeyContext::Mode("foo".into()),
484        ] {
485            let expected = matches!(ctx, KeyContext::Normal | KeyContext::Prompt);
486            assert_eq!(
487                ctx.allows_text_input(),
488                expected,
489                "{:?} text-input expectation violated",
490                ctx
491            );
492        }
493    }
494}