Skip to main content

oo_ide/
operation.rs

1use std::borrow::Cow;
2use std::collections::HashMap;
3use std::path::PathBuf;
4
5use crate::prelude::*;
6
7use app_state::ScreenKind;
8use commands::ArgValue;
9use editor::position::Position;
10use crate::issue_registry::{IssueId, NewIssue, Severity};
11use crate::persistent_issues::PersistentNewIssue;
12use crate::task_registry::{TaskId, TaskKey, TaskStatus, TaskTrigger};
13
14#[derive(Debug, Clone)]
15pub enum ExtensionConfigOp {
16    MoveUp,
17    MoveDown,
18    Select(usize),
19    SetStatus(usize),
20}
21// View-local operations for FileSelector — defined here to avoid a circular
22// dependency between operation.rs and views/file_selector.rs.
23
24#[derive(Debug, Clone)]
25pub enum FileSelectorOp {
26    FilterInput(crate::widgets::input_field::InputFieldOp),
27    SwitchPane,
28    CollapseOrLeft,
29    ExpandOrRight,
30    ToggleDir,
31    /// Delivered by the async search task with the scored+sorted results.
32    /// The `generation` field is used to discard results from superseded
33    /// queries (user typed again before this result arrived).
34    SetResults {
35        generation: u64,
36        paths: Vec<PathBuf>,
37    },
38    /// Delivered by the async directory-read task when a lazy directory
39    /// expansion completes.  Each entry is `(absolute_path, is_dir)`.
40    DirLoaded {
41        path: PathBuf,
42        entries: Vec<(PathBuf, bool)>,
43    },
44}
45
46// View-local operations for LogView.
47
48#[derive(Debug, Clone)]
49pub enum LogViewOp {
50    FilterInput(crate::widgets::input_field::InputFieldOp),
51}
52
53// View-local operations for TaskArchiveView.
54
55#[derive(Debug, Clone)]
56pub enum TaskArchiveOp {
57    FilterInput(crate::widgets::input_field::InputFieldOp),
58    /// Open the log for the highlighted task.
59    OpenSelected,
60    /// Cancel the highlighted running task.
61    CancelSelected,
62    /// Sent by app.rs when the task list changes so the view can clamp its cursor.
63    TaskUpdated { new_len: usize },
64    /// Close the inline detail pane and return to the list.
65    CloseDetail,
66    /// Populate the inline detail pane with loaded log lines.
67    ShowDetail {
68        task_id: TaskId,
69        title:   String,
70        status:  TaskStatus,
71        lines:   Vec<String>,
72    },
73}
74
75// ---------------------------------------------------------------------------
76// View-local operations for IssueView.
77// ---------------------------------------------------------------------------
78
79/// View-local operations for crate::views::issue_view::IssueView.
80#[derive(Debug, Clone)]
81pub enum IssueViewOp {
82    FilterInput(crate::widgets::input_field::InputFieldOp),
83    /// Enter — open detail for the selected issue.
84    OpenSelected,
85    /// `r` — resolve the currently selected issue.
86    ResolveSelected,
87    /// `d` — dismiss the currently selected issue.
88    DismissSelected,
89    /// `h` — toggle visibility of dismissed/resolved issues.
90    ToggleShowDismissed,
91    /// `0`–`4` — filter by severity; `None` = show all.
92    SetSeverityFilter(Option<Severity>),
93    /// Esc in Detail mode — back to List.
94    CloseDetail,
95    /// Sent when IssueAdded / IssueRemoved / IssueUpdated arrive, so the view
96    /// can clamp its cursor.
97    RegistryChanged,
98}
99
100// View-local operations for CommandRunner — same pattern as FileSelectorOp.
101
102#[derive(Debug, Clone)]
103pub enum CommandSelectorOp {
104    FilterInput(crate::widgets::input_field::InputFieldOp),
105    SwitchPane,
106    CollapseOrLeft,
107    ExpandOrRight,
108    /// Confirm selection (Enter) from the selector pane.
109    Confirm,
110    /// In the arg-form view: move focus to next field.
111    ArgNext,
112    /// In the arg-form view: move focus to previous field.
113    ArgPrev,
114    /// In the arg-form view: set the focused field's value.
115    ArgChanged(String),
116    /// Cancel the arg form and go back to selector.
117    ArgCancel,
118}
119
120// ---------------------------------------------------------------------------
121// View-local operations for CommitWindow.
122// ---------------------------------------------------------------------------
123
124#[derive(Debug, Clone)]
125pub enum CommitOp {
126    // ── UI trigger ops (view → app → spawn_blocking) ─────────────────────
127    /// Switch between Commit and Diff tabs.
128    SwitchTab,
129    // Commit tab
130    /// Cycle focus: Staged → Unstaged → Message.
131    SwitchPane,
132    /// Stage or unstage the selected file.
133    ToggleFileStage,
134    /// Set the commit message text programmatically (e.g., clear or load a draft).
135    MessageChanged(String),
136    /// Keystroke editing ops for the message field.
137    MessageInsertChar(char),
138    MessageInsertNewline,
139    MessageBackspace,
140    MessageCursorLeft,
141    MessageCursorRight,
142    MessageCursorUp,
143    MessageCursorDown,
144    /// Open the new-branch input (clears any previous value).
145    BranchInputOpen,
146    /// Cancel the branch-name input without creating a branch.
147    BranchInputCancel,
148    /// Set the branch-name input text.
149    BranchChanged(String),
150    /// Confirm new branch from branch_input.
151    BranchCreate,
152    /// Open the branch selector popup (shows existing branches).
153    BranchSelectorOpen,
154    /// Close the branch selector popup without action.
155    BranchSelectorCancel,
156    /// Checkout the given branch (handled by app.rs with git ops).
157    BranchCheckout(String),
158    /// Execute commit (guarded: message must be non-empty).
159    Confirm,
160    /// Enter amend mode by loading the most recent commit message.
161    EnterAmendMode,
162    /// Confirm an amend (git commit --amend equivalent).
163    ConfirmAmend,
164    /// Cancel an in-progress amend — exit amend mode without committing.
165    CancelAmend,
166    // Diff tab
167    /// Toggle focus between file list and diff lines panels in the diff tab.
168    DiffFocusToggle,
169    /// Toggle staged_diff_lines for the selected diff line (local, no git call).
170    /// On a hunk header row this toggles fold instead.
171    ToggleLineStage,
172    /// Revert the selected diff line.
173    RevertLine,
174    /// Request diff for a file.
175    LoadDiff {
176        path: PathBuf,
177        staged: bool,
178    },
179    /// Async result ops (spawn_blocking → op_tx → handle_operation)
180    StatusLoaded {
181        staged: Vec<crate::vcs::StatusEntry>,
182        unstaged: Vec<crate::vcs::StatusEntry>,
183        branches: Vec<String>,
184        current_branch: String,
185    },
186    DiffLoaded {
187        path: PathBuf,
188        /// Whether this diff is staged (HEAD↔index) or unstaged (index↔workdir).
189        staged: bool,
190        lines: Vec<crate::vcs::DiffLine>,
191    },
192    /// Toggle or set amend mode in the view (delivered after async load).
193    AmendModeSet(bool, Option<String>),
194    GitOpSuccess(String),
195    GitOpError(String),
196    /// Async result: detected Git operation mode (rebase / merge / normal).
197    /// Delivered by the detection task spawned on CommitWindow open and refresh.
198    GitModeDetected {
199        mode: crate::vcs::GitMode,
200        has_conflicts: bool,
201        /// Pre-filled commit message for the current rebase step or merge.
202        message: String,
203    },
204    /// Abort an in-progress rebase (`git rebase --abort`).
205    AbortRebase,
206    /// Abort an in-progress merge (`git merge --abort`).
207    AbortMerge,
208}
209
210// LSP-related types and view-local operations for the editor.
211
212#[derive(Debug, Clone)]
213pub struct LspCompletionItem {
214    pub label: String,
215    pub kind: Option<u8>,
216    pub detail: Option<String>,
217    pub insert_text: Option<String>,
218}
219
220#[derive(Debug, Clone)]
221pub struct LspLocation {
222    pub path: PathBuf,
223    pub row: usize,
224    pub col: usize,
225}
226
227#[derive(Debug, Clone)]
228pub enum GoToTarget {
229    /// Request LSP definition at the current cursor (editor context).
230    LspRequest,
231    /// Concrete LSP location returned by server.
232    LspLocation(LspLocation),
233    /// Open file at line (0-based) and optional column.
234    FileLine {
235        path: std::path::PathBuf,
236        line: usize,
237        col: Option<usize>,
238    },
239    /// Placeholder for future diff/commit targets.
240    DiffLine {
241        repo: Option<std::path::PathBuf>,
242        commit: Option<String>,
243        path: std::path::PathBuf,
244        line: usize,
245    },
246    /// Symbol name with optional scope.
247    Symbol {
248        name: String,
249        scope: Option<String>,
250    },
251    /// Raw URI & range.
252    UriRange {
253        uri: String,
254        start_line: usize,
255        start_char: usize,
256    },
257}
258
259#[derive(Debug, Clone)]
260pub enum ClipOp {
261    /// Copy active selection (or current line if empty) to clipboard + history.
262    Copy,
263    /// Cut active selection (or current line if empty) to clipboard + history.
264    Cut,
265    /// Paste from system clipboard at cursor.
266    Paste,
267    /// Open the clipboard history picker dialog.
268    OpenHistory,
269    /// Navigate history picker up.
270    HistoryUp,
271    /// Navigate history picker down.
272    HistoryDown,
273    /// Paste the selected history item and close picker.
274    HistoryConfirm,
275    /// Close the history picker without pasting.
276    HistoryClose,
277}
278
279#[derive(Debug, Clone)]
280pub enum SelectionOp {
281    /// Extend active selection head (Shift+arrow).
282    Extend { head: Position },
283    /// Select the entire buffer.
284    SelectAll,
285    /// Clear selection (Esc).
286    Clear,
287}
288
289#[derive(Debug, Clone)]
290pub enum GoToLineOp {
291    /// Open the go-to-line bar (or focus it if already open).
292    Open,
293    /// Close the bar without committing.
294    Close,
295    /// Commit the current line number and close.
296    Confirm,
297    /// Feed a key to the line-number input field.
298    Input(crate::widgets::input_field::InputFieldOp),
299    /// Jump directly to a specific line (programmatic, e.g., from terminal link).
300    JumpTo { line: usize, column: usize },
301}
302
303#[derive(Debug, Clone)]
304pub enum SearchOp {
305    Open {
306        replace: bool,
307    },
308    Close,
309    QueryInput(crate::widgets::input_field::InputFieldOp),
310    ReplacementInput(crate::widgets::input_field::InputFieldOp),
311    ToggleIgnoreCase,
312    ToggleRegex,
313    ToggleSmartCase,
314    /// Toggle query ↔ replacement input focus.
315    FocusSwitch,
316    NextMatch,
317    PrevMatch,
318    ReplaceOne,
319    ReplaceAll,
320    /// Stream one project-wide file result into the editor search state.
321    /// Sent by the async project-search task for every file that contains matches.
322    /// `generation` is used to discard results from a superseded search.
323    AddProjectResult { file: PathBuf, result: MatchSpan, generation: u64 },
324    /// Clear all project-wide file results — sent before a new search starts.
325    /// `generation` identifies the search about to begin so the editor can
326    /// ignore any in-flight `AddProjectResult` ops from the previous search.
327    ClearProjectResults { generation: u64 },
328    /// Select a file in the project-search file list (0-based index into `files`).
329    SelectFile(usize),
330    /// Scroll the match-results panel by N rows (positive = down, negative = up).
331    ScrollMatchPanel(i8),
332    /// Keystroke routed to the include-glob filter field (Expanded mode only).
333    IncludeGlobInput(crate::widgets::input_field::InputFieldOp),
334    /// Keystroke routed to the exclude-glob filter field (Expanded mode only).
335    ExcludeGlobInput(crate::widgets::input_field::InputFieldOp),
336    /// Move cursor up in the search results tree.
337    TreeMoveUp,
338    /// Move cursor down in the search results tree.
339    TreeMoveDown,
340    /// Toggle expand/collapse on a file node, or activate a match entry.
341    TreeToggle,
342    /// Collapse current file node (or move to parent file when on a match row).
343    TreeCollapse,
344}
345
346#[derive(Debug, Clone)]
347pub enum LspOp {
348    CompletionResponse {
349        items: Vec<LspCompletionItem>,
350        trigger: Option<Position>,
351        version: Option<i32>,
352    },
353    /// Sent immediately when completion is triggered to show loading state
354    CompletionLoading {
355        trigger: Position,
356    },
357    HoverResponse(Option<String>),
358    CompletionMoveUp,
359    CompletionMoveDown,
360    CompletionConfirm,
361    CompletionDismiss,
362    HoverDismiss,
363}
364
365// ---------------------------------------------------------------------------
366// LSP rename
367// ---------------------------------------------------------------------------
368
369/// A single text replacement within one file, delivered as part of a
370/// workspace edit.  Positions are 0-based (line, UTF-16 character offset).
371#[derive(Debug, Clone)]
372pub struct LspTextEdit {
373    pub start_line: u32,
374    pub start_char: u32,
375    pub end_line: u32,
376    pub end_char: u32,
377    pub new_text: String,
378}
379
380/// All text edits for a single file, as part of a `WorkspaceEdit` result.
381#[derive(Debug, Clone)]
382pub struct LspFileEdit {
383    pub path: std::path::PathBuf,
384    pub edits: Vec<LspTextEdit>,
385}
386
387/// View-local operations for the editor rename prompt.
388#[derive(Debug, Clone)]
389pub enum LspRenameOp {
390    /// Open the rename prompt pre-filled with the current symbol name.
391    OpenPrompt { current_name: String },
392    /// Update the text input field.
393    Input(crate::widgets::input_field::InputFieldOp),
394    /// Close the prompt without renaming.
395    Dismiss,
396}
397// Project-wide search & replace
398// ---------------------------------------------------------------------------
399
400/// A single match span within a file line.
401#[derive(Debug, Clone)]
402pub struct MatchSpan {
403    /// 0-based line index.
404    pub line: usize,
405    /// Byte offset of match start within the line.
406    pub byte_start: usize,
407    /// Byte offset of match end within the line.
408    pub byte_end: usize,
409    /// Full text of the line (no newline).
410    pub line_text: String,
411}
412
413/// All matches found in one file.
414#[derive(Debug, Clone)]
415pub struct FileMatchResult {
416    pub path: PathBuf,
417    pub matches: Vec<MatchSpan>,
418}
419
420// ---------------------------------------------------------------------------
421// Git History Viewer view-local operations
422// ---------------------------------------------------------------------------
423
424#[derive(Debug, Clone)]
425pub enum GitHistoryOp {
426    /// Keystroke routed to the filter input field.
427    FilterInput(crate::widgets::input_field::InputFieldOp),
428    /// Async result: commits loaded for the current filter + generation.
429    CommitsLoaded {
430        generation: u64,
431        commits: Vec<crate::vcs::CommitInfo>,
432    },
433    /// User confirmed a revision selection (Enter on revision list).
434    /// Intercepted by app.rs which spawns file-list + diff loading.
435    SelectRevision,
436    /// Async result: file list for the selected revision.
437    FilesLoaded {
438        oid: String,
439        files: Vec<crate::vcs::FileChange>,
440    },
441    /// User navigated to or confirmed a file selection.
442    /// Intercepted by app.rs which spawns diff loading.
443    SelectFile,
444    /// Async result: diff for the selected revision + file.
445    DiffLoaded {
446        lines: Vec<crate::vcs::DiffLine>,
447    },
448    /// An async git task failed.
449    LoadError(String),
450}
451
452// ---------------------------------------------------------------------------
453// Branch Management View view-local operations
454// ---------------------------------------------------------------------------
455
456#[derive(Debug, Clone)]
457pub enum BranchViewOp {
458    /// Async result: branch lists loaded on view open.
459    BranchesLoaded {
460        local: Vec<String>,
461        remote: Vec<String>,
462        current: String,
463    },
464    /// Async error while loading branches.
465    LoadError(String),
466    /// User pressed Enter — intercepted by app.rs to spawn a checkout.
467    CheckoutBranch,
468    /// Async result: checkout succeeded; carries the new current branch name.
469    CheckoutCompleted(String),
470    /// Async result: checkout failed.
471    CheckoutError(String),
472    /// User pressed 'n' — open the new-branch name input.
473    NewBranchOpen,
474    /// Keystroke routed to the new-branch name input field.
475    NewBranchInput(crate::widgets::input_field::InputFieldOp),
476    /// User confirmed the new branch name (Enter while input is open).
477    /// Intercepted by app.rs to spawn a create+checkout task.
478    NewBranchConfirm,
479    /// User cancelled the new-branch input (Esc).
480    NewBranchCancel,
481    /// Async result: branch created and checked out.
482    CreateCompleted(String),
483    /// Async result: branch creation failed.
484    CreateError(String),
485    /// User pressed Delete (first press) — app.rs checks merge status and replies with
486    /// DeleteConfirmNeeded if required or performs immediate deletion.
487    DeleteRequested,
488    /// App replies that confirmation is required: (branch, merged)
489    DeleteConfirmNeeded { branch: String, merged: bool },
490    /// User pressed Delete again to confirm (view converts to this when pending matches).
491    DeleteConfirmed,
492    /// Async result: deletion succeeded — carries the deleted branch name.
493    DeleteCompleted(String),
494    /// Async deletion failed.
495    DeleteError(String),
496}
497
498// ---------------------------------------------------------------------------
499// Git Pull view-local operations
500// ---------------------------------------------------------------------------
501
502/// View-local operations for the Git Pull workflow view.
503///
504/// The pull flow is:
505/// 1. Fetch remote (`FetchCompleted` / `FetchError`)
506///    2a. Fast-forward if possible (`FastForwardCompleted` / `FastForwardError`)
507///    2b. Present rebase / merge choice; user navigates + confirms
508/// 3. Apply selected strategy (`RebaseCompleted` / `MergeCompleted` / errors)
509#[derive(Debug, Clone)]
510pub enum GitPullOp {
511    // ── Async results ────────────────────────────────────────────────────────
512
513    /// Fetch succeeded.
514    ///
515    /// `can_fast_forward` is `true` when HEAD is an ancestor of the upstream
516    /// ref — i.e., a fast-forward merge is safe and will be applied
517    /// automatically.  When `false`, the view shows the Rebase / Merge choice.
518    FetchCompleted {
519        can_fast_forward: bool,
520        /// Short name of the upstream tracking ref (e.g. `"origin/main"`).
521        upstream: String,
522        /// Name of the local branch being updated.
523        current_branch: String,
524    },
525    /// Fetch failed (network error, no remote configured, etc.).
526    FetchError(String),
527    /// Fast-forward completed successfully.
528    FastForwardCompleted,
529    /// Fast-forward failed (should be rare — upstream diverged since fetch).
530    FastForwardError(String),
531    /// Rebase on the upstream completed successfully.
532    RebaseCompleted,
533    /// Rebase failed (conflicts or other errors).
534    RebaseError(String),
535    /// Merge of the upstream completed successfully.
536    MergeCompleted,
537    /// Merge failed.
538    MergeError(String),
539
540    // ── User actions (choice prompt) ─────────────────────────────────────────
541
542    /// Move the highlighted choice up.
543    ChoiceUp,
544    /// Move the highlighted choice down.
545    ChoiceDown,
546    /// Confirm the currently highlighted choice (Rebase / Merge / Cancel).
547    /// Intercepted by `app.rs`; the view does not need to spawn tasks.
548    ChoiceConfirm,
549}
550
551// ---------------------------------------------------------------------------
552// Git History Editor view-local operations
553// ---------------------------------------------------------------------------
554
555#[derive(Debug, Clone)]
556pub enum HistoryEditorOp {
557    // --- Async results -------------------------------------------------------
558    /// Initial commit list loaded on view open.
559    CommitsLoaded(Vec<crate::vcs::CommitInfo>),
560    /// Async operation failed (generic).
561    OperationFailed(String),
562    /// A git operation (rebase / reset) completed successfully.
563    /// Carries the new commit list and optionally the pre-op SHA for undo recording.
564    /// `pre_sha` is `None` for undo operations (so they don't themselves get undone again).
565    OperationCompleted {
566        commits: Vec<crate::vcs::CommitInfo>,
567        pre_sha: Option<String>,
568        description: String,
569    },
570
571    // --- User actions (intercepted by app.rs) --------------------------------
572    /// `u` — move selected commit one step toward older commits.
573    MoveUp,
574    /// `d` — move selected commit one step toward newer commits.
575    MoveDown,
576    /// `Enter` — confirm pending move (run git rebase).
577    ConfirmMove,
578    /// `r` — open the commit message editor for rewording.
579    StartReword,
580    /// `s` — open the combined message editor for squashing with the commit at
581    /// the given index (the older commit; one index higher than cursor).
582    StartSquash { target_idx: usize },
583    /// `ctrl+s` in editor mode — apply the edited message (triggers git).
584    ConfirmEdit,
585    /// `Del` — drop the selected commit (may require a second press).
586    DeleteCommit,
587    /// `ctrl+z` — undo the last operation (runs `git reset --hard <sha>`).
588    UndoLast,
589
590    // --- Editor mode text input (emitted in EditorMode) ----------------------
591    /// Insert a single character into the message buffer.
592    EditorInsertChar(char),
593    /// Insert a newline into the message buffer.
594    EditorInsertNewline,
595    /// Backspace in the message buffer.
596    EditorBackspace,
597    /// Move cursor left in the message buffer.
598    EditorCursorLeft,
599    /// Move cursor right in the message buffer.
600    EditorCursorRight,
601}
602
603
604#[derive(Debug, Clone)]
605pub enum TerminalOp {
606    /// Raw bytes received from the PTY process (sent by the async read task).
607    Output { id: u64, data: Vec<u8> },
608    /// Completed styled lines produced from PTY output by the async read task.
609    ///
610    /// Sent alongside `Output` so the main thread can maintain a styled
611    /// scrollback buffer per tab (enabling state persistence and reuse of
612    /// `DiagnosticsExtractor::extract_from_line`).
613    AppendScrollback { id: u64, lines: Vec<crate::vt_parser::StyledLine> },
614    /// Open a new tab. `command` defaults to the configured shell if `None`.
615    NewTab { command: Option<String> },
616    /// Close a tab by ID. If it was the last tab, a new shell is spawned.
617    CloseTab { id: u64 },
618    /// Switch to the tab at the given index (clamped to valid range).
619    SwitchToTab { index: usize },
620    /// Switch to the next tab (wraps around).
621    NextTab,
622    /// Switch to the previous tab (wraps around).
623    PrevTab,
624    /// Reset scroll to the bottom of the active tab.
625    ScrollReset,
626    /// Enable/disable link highlight mode (triggered by holding Ctrl while using the mouse).
627    SetLinkHighlight { enabled: bool },
628    /// Open the context menu for the given tab.
629    OpenMenu { id: u64 },
630    /// Close the context menu without taking action.
631    CloseMenu,
632    /// Confirm the highlighted context-menu item.
633    MenuConfirm,
634    /// Open the inline rename input for the given tab directly.
635    OpenRename { id: u64 },
636    /// Clear the screen of the given tab (sends VT clear sequence to PTY).
637    ClearTab { id: u64 },
638    /// Set the rename input text.
639    RenameChanged(String),
640    /// Commit the rename.
641    RenameConfirm,
642    /// Cancel the rename.
643    RenameCancel,
644    /// Open the tab selector popup (keyboard/mouse-navigable list of all tabs).
645    OpenTabSelector,
646    /// Terminal dimensions changed — resize all PTY tabs.
647    Resize { cols: u16, rows: u16 },
648    /// The PTY child process has exited.
649    ProcessExited { id: u64 },
650}
651
652#[derive(Debug, Clone)]
653pub enum SearchReplaceOp {
654    //  Input field changes
655    QueryChanged(String),
656    ReplaceChanged(String),
657    IncludeGlobChanged(String),
658    ExcludeGlobChanged(String),
659    //  Toggle options
660    ToggleRegex,
661    ToggleCase,
662    ToggleSmartCase,
663    //  Tree navigation
664    TreeExpandOrEnter,
665    TreeCollapseOrLeave,
666    //  Replace actions
667    ReplaceCurrentFile,
668    ReplaceAll,
669    //  Undo / redo
670    Undo,
671    Redo,
672    //  Async results
673    SearchResultsReady {
674        results: Vec<FileMatchResult>,
675        generation: u64,
676    },
677    ReplaceComplete {
678        path: PathBuf,
679        replaced_count: usize,
680    },
681    AsyncError(String),
682    ProgressUpdate {
683        scanned: usize,
684        total: usize,
685    },
686}
687
688#[derive(Debug, Clone)]
689pub enum Operation {
690    // Buffer mutations
691    InsertText {
692        path: Option<PathBuf>,
693        #[allow(dead_code)]
694        cursor: Position,
695        text: String,
696    },
697    /// Bracketed paste (or fallback-heuristic detected paste) delivered to the
698    /// active editor. Unlike `InsertText`, the full multi-line string is inserted
699    /// at once via `insert_text_raw` and also pushed to the clipboard register.
700    PasteText {
701        path: Option<PathBuf>,
702        text: String,
703    },
704    DeleteText {
705        path: Option<PathBuf>,
706        #[allow(dead_code)]
707        cursor: Position,
708        #[allow(dead_code)]
709        len: usize,
710    },
711    DeleteForward {
712        path: Option<PathBuf>,
713        #[allow(dead_code)]
714        cursor: Position,
715    },
716    DeleteWordBackward {
717        path: Option<PathBuf>,
718    },
719    DeleteWordForward {
720        path: Option<PathBuf>,
721    },
722    IndentLines {
723        path: Option<PathBuf>,
724    },
725    UnindentLines {
726        path: Option<PathBuf>,
727    },
728    DeleteLine {
729        path: Option<PathBuf>,
730    },
731    /// Replace the text from `start` to `end` (same line, byte offsets) with `text`.
732    /// Used by CompletionConfirm to delete the filter prefix before inserting.
733    ReplaceRange {
734        path: Option<PathBuf>,
735        start: Position,
736        end: Position,
737        text: String,
738    },
739    MoveCursor {
740        path: Option<PathBuf>,
741        cursor: Position,
742    },
743    Undo {
744        path: Option<PathBuf>,
745    },
746    Redo {
747        path: Option<PathBuf>,
748    },
749    ToggleFold {
750        path: Option<PathBuf>,
751        line: usize,
752    },
753    ToggleMarker {
754        path: Option<PathBuf>,
755        line: usize,
756    },
757    ToggleWordWrap,
758
759    // File lifecycle
760    OpenFile {
761        path: PathBuf,
762    },
763    /// Navigate from the project search tree to a match.
764    /// Opens the file (if not already active) while preserving the search
765    /// state, then positions the cursor at the given line and column.
766    GoToProjectMatch {
767        file: PathBuf,
768        line: usize,
769        col: usize,
770    },
771    /// Open an external URL in the system default browser.
772    OpenUrl {
773        url: String,
774    },
775    SaveFile {
776        path: PathBuf,
777    },
778    ReloadFromDisk {
779        path: PathBuf,
780        accept_external: bool,
781        /// When true, suppress the "Reloaded from disk." status message.
782        silent: bool,
783    },
784
785    // App / navigation
786    SwitchScreen {
787        to: ScreenKind,
788    },
789    /// Close the currently active overlay, sub-view, or modal.
790    ///
791    /// The handler in `app.rs` walks a priority list to determine what to
792    /// close (completion menu, search bar, modal sub-views, etc.).
793    Close,
794    Quit,
795
796    // Focus navigation — handled by whichever view is active
797    Focus(crate::widgets::focusable::FocusOp),
798
799    // Generic list/tree/scroll navigation — dispatched to the active view.
800    // Each view interprets these according to its own focus state.
801    NavigateUp,
802    NavigateDown,
803    NavigatePageUp,
804    NavigatePageDown,
805    NavigateHome,
806    NavigateEnd,
807
808    // View-local ops that don't cross view boundaries
809    FileSelectorLocal(FileSelectorOp),
810    CommandSelectorLocal(CommandSelectorOp),
811    LogViewLocal(LogViewOp),
812    CommitLocal(CommitOp),
813    GitHistoryLocal(GitHistoryOp),
814    BranchViewLocal(BranchViewOp),
815    HistoryEditorLocal(HistoryEditorOp),
816    GitPullLocal(GitPullOp),
817    ExtensionConfig(ExtensionConfigOp),
818
819    // LSP trigger ops (handled in app.rs, forwarded to LspClient)
820    LspTriggerCompletion {
821        #[allow(dead_code)]
822        path: Option<PathBuf>,
823        /// The character(s) that triggered completion (for LSP context)
824        #[allow(dead_code)]
825        trigger_char: Option<String>,
826    },
827    LspTriggerHover {
828        #[allow(dead_code)]
829        path: Option<PathBuf>,
830        /// If `Some`, this trigger was generated by the idle debounce timer.
831        /// The value is the generation counter at spawn time; the handler
832        /// ignores this operation if the stored generation has advanced.
833        idle_generation: Option<u64>,
834    },
835    LspGoToDefinition {
836        #[allow(dead_code)]
837        path: Option<PathBuf>,
838    },
839    /// Result from a go-to-definition request, dispatched back into the queue.
840    LspGoToDefinitionResult(Vec<LspLocation>),
841    /// Generic go-to operation for cross-context navigation.
842    GoTo {
843        target: GoToTarget,
844    },
845    /// LSP server returned trigger characters for a language.
846    LspTriggerCharactersReceived {
847        lang_id: String,
848        trigger_chars: Vec<String>,
849    },
850    /// LSP editor-local ops (dropdown navigation, response delivery).
851    LspLocal(LspOp),
852    /// Trigger the LSP rename prompt for the symbol under the cursor.
853    LspTriggerRename {
854        #[allow(dead_code)]
855        path: Option<PathBuf>,
856    },
857    /// Sent by the rename prompt after the user confirms a new name.
858    LspTriggerRenameWith {
859        new_name: String,
860        /// Cursor position (line, col) at the time the prompt was opened.
861        row: u32,
862        col: u32,
863    },
864    /// Workspace edit returned by the server — apply across all affected files.
865    LspRenameResult(Vec<LspFileEdit>),
866    /// Editor-local rename prompt ops.
867    LspRenameLocal(LspRenameOp),
868    /// Search/replace bar ops (editor-local).
869    SearchLocal(SearchOp),
870    /// Go-to-line bar ops (editor-local).
871    GoToLineLocal(GoToLineOp),
872    /// Selection management ops (editor-local).
873    SelectionLocal(SelectionOp),
874    /// Clipboard ops (editor-local).
875    ClipboardLocal(ClipOp),
876    /// Project-wide search & replace ops.
877    SearchReplaceLocal(SearchReplaceOp),
878
879    /// Execute a registered command by id (with pre-filled args).
880    RunCommand {
881        id: crate::commands::CommandId,
882        args: HashMap<String, ArgValue>,
883    },
884
885    /// Open a generic context menu at `(x, y)` with the given items.
886    ///
887    /// Items are `(label, command_id)` pairs.  `app.rs` stores the menu on
888    /// [`crate::app_state::AppState`], intercepts input, and dispatches the selected command.
889    OpenContextMenu {
890        // Each item: (label, command_id, enabled_override)
891        // - enabled_override: `Some(bool)` to explicitly set enabled state,
892        //   or `None` to let the runtime compute a sensible default.
893        items: Vec<(String, crate::commands::CommandId, Option<bool>)>,
894        x: u16,
895        y: u16,
896    },
897
898    /// Close the currently open context menu without selecting any item.
899    CloseContextMenu,
900
901    // Escape hatch for plugins
902    #[allow(dead_code)]
903    Generic {
904        source: Cow<'static, str>,
905        name: Cow<'static, str>,
906        payload: HashMap<String, ArgValue>,
907    },
908
909    // Extension system
910    /// Broadcast a named event to all active extensions.
911    ExtensionEvent {
912        name: String,
913        payload: HashMap<String, String>,
914    },
915    /// Display a user-visible notification (shown in status bar).
916    ShowNotification {
917        message: String,
918        level: NotificationLevel,
919    },
920    /// Update the IDE status bar slot with custom text.
921    UpdateStatusBar {
922        text: String,
923        #[allow(unused)]
924        slot: StatusSlot,
925    },
926    /// Request a terminal be created running the given command.
927    CreateTerminal {
928        command: String,
929    },
930    /// Send raw bytes to the PTY of the terminal tab with the given ID.
931    TerminalInput {
932        id: u64,
933        data: Vec<u8>,
934    },
935    /// Terminal view-local operations.
936    TerminalLocal(TerminalOp),
937    /// Show a picker (like command palette) populated with arbitrary items.
938    ShowPicker {
939        title: String,
940        items: Vec<PickerItem>,
941    },
942
943    /// Deliver pre-computed syntax-highlight spans for an editor buffer.
944    ///
945    /// Sent by the background highlight task; handled in `app.rs`, which stores
946    /// the result in `crate::views::editor::EditorView::highlight_cache` when the version still
947    /// matches the live buffer.  The next frame then renders without blocking.
948    SetEditorHighlights {
949        path: Option<std::path::PathBuf>,
950        version: editor::buffer::Version,
951        start_line: usize,
952        generation: u64,
953        spans: Vec<Vec<editor::highlight::StyledSpan>>,
954    },
955
956    /// Incremental highlight update for multiple lines.
957    ///
958    /// Unlike SetEditorHighlights, this operation updates individual lines
959    /// without clearing existing highlights. This prevents highlights from
960    /// disappearing while typing or scrolling.
961    SetEditorHighlightsChunk {
962        path: Option<std::path::PathBuf>,
963        version: editor::buffer::Version,
964        generation: u64,
965        spans: Vec<(usize, Vec<editor::highlight::StyledSpan>)>,
966    },
967
968    /// Deliver git-diff change line numbers for the editor gutter.
969    ///
970    /// Sent by a background task after a file is opened or saved.
971    /// Contains the set of 0-based line indices that are added or modified
972    /// in the workdir relative to the index.
973    SetEditorGitChanges {
974        path: std::path::PathBuf,
975        /// 0-based line indices that have been added or modified.
976        changed_lines: std::collections::HashSet<usize>,
977    },
978
979    // -----------------------------------------------------------------------
980    // Issue registry — ephemeral commands + startup loads
981    // (no disk I/O; produced by async tasks and the persistent-load path)
982    // -----------------------------------------------------------------------
983
984    /// Add an issue to the registry (registry-only, no disk write).
985    ///
986    /// `issue.marker = Some(m)` → ephemeral (cleared via `ClearIssuesByMarker`).
987    /// `issue.marker = None`   → used only for startup loads by the Persistent
988    ///                           Component.  User-initiated persistent issues
989    ///                           must go through `PersistentIssueLocal(Add)`.
990    AddIssue { issue: NewIssue },
991
992    /// Remove an issue by ID (registry-only).  Emits `IssueRemoved` on success.
993    RemoveIssue { id: IssueId },
994
995    /// Mark an issue as resolved (registry-only).  Emits `IssueUpdated` on success.
996    ResolveIssue { id: IssueId },
997
998    /// Mark an issue as dismissed (registry-only).  Emits `IssueUpdated` on success.
999    DismissIssue { id: IssueId },
1000
1001    /// Remove all ephemeral issues belonging to `marker`.  Emits one
1002    /// `IssueRemoved` per removed issue.
1003    ClearIssuesByMarker { marker: String },
1004
1005    // -----------------------------------------------------------------------
1006    // Persistent issue operations — update registry AND write to disk
1007    // -----------------------------------------------------------------------
1008
1009    /// Persistent-issue commands: each variant updates the in-memory
1010    /// [`IssueRegistry`] **and** atomically rewrites `.oo/issues.yaml`.
1011    ///
1012    /// [`IssueRegistry`]: crate::issue_registry::IssueRegistry
1013    PersistentIssueLocal(PersistentIssueOp),
1014
1015    // -----------------------------------------------------------------------
1016    // Issue registry — events (enqueued by apply_operation after mutations)
1017    // -----------------------------------------------------------------------
1018
1019    /// An issue was added.
1020    IssueAdded { id: IssueId },
1021
1022    /// An issue was removed.
1023    IssueRemoved { id: IssueId },
1024
1025    /// An issue's `resolved` or `dismissed` flag was toggled.
1026    IssueUpdated { id: IssueId },
1027
1028    // -----------------------------------------------------------------------
1029    // Task system lifecycle events
1030    // -----------------------------------------------------------------------
1031
1032    /// A task was created and either started immediately or added to its queue.
1033    TaskScheduled(TaskId),
1034
1035    /// A task transitioned from `Pending` to `Running`.
1036    TaskStarted(TaskId),
1037
1038    /// A task reached a terminal status (`Success`, `Warning`, or `Error`).
1039    ///
1040    /// Sent by [`crate::task_executor::TaskExecutor`] when the spawned process
1041    /// exits.  Handled by `apply_operation` in `app.rs`, which calls
1042    /// [`crate::task_registry::TaskRegistry::mark_finished`] and starts the
1043    /// next queued task if one exists.
1044    TaskFinished {
1045        id: TaskId,
1046        status: TaskStatus,
1047    },
1048
1049    /// A task was cancelled (running or queued).
1050    TaskCancelled(TaskId),
1051
1052    // -----------------------------------------------------------------------
1053    // Task system commands
1054    // -----------------------------------------------------------------------
1055
1056    /// Schedule a new task.  Handled by `apply_operation`, which calls
1057    /// [`crate::task_registry::TaskRegistry::schedule_task`] and immediately
1058    /// spawns the executor if the queue was idle.
1059    ScheduleTask {
1060        key: TaskKey,
1061        trigger: TaskTrigger,
1062        /// The shell command to execute (passed through to the process).
1063        command: String,
1064    },
1065
1066    /// Cancel a running or queued task by its ID.  Handled by `apply_operation`,
1067    /// which calls [`crate::task_registry::TaskRegistry::cancel`] and starts the
1068    /// next queued task if one exists.
1069    CancelTask(TaskId),
1070
1071    /// Open the log view and pre-populate its filter with the task's label so
1072    /// the user can quickly inspect task-related entries.
1073    OpenLogForTask { task_id: TaskId },
1074
1075    /// Open the Task Archive View.
1076    OpenTaskArchive,
1077
1078    /// Delivered by app.rs after it asynchronously loads task output from the
1079    /// log store.  Switches to `TaskOutputView` and populates it with the lines.
1080    ShowTaskOutput {
1081        task_id: TaskId,
1082        title: String,
1083        status: TaskStatus,
1084        lines: Vec<String>,
1085    },
1086
1087    /// View-local ops for `crate::views::task_archive::TaskArchiveView`.
1088    TaskArchiveLocal(TaskArchiveOp),
1089
1090    /// Open the Issue List View.
1091    OpenIssueList,
1092
1093    /// View-local ops for `crate::views::issue_view::IssueView`.
1094    IssueViewLocal(IssueViewOp),
1095}
1096
1097// ---------------------------------------------------------------------------
1098// Persistent issue sub-operations
1099// ---------------------------------------------------------------------------
1100
1101/// Commands that manipulate **persistent** issues.
1102///
1103/// Each variant is handled by `apply_operation` in `app.rs`, which:
1104/// 1. Calls the corresponding [`IssueRegistry`] mutation method.
1105/// 2. Enqueues the matching `IssueAdded` / `IssueRemoved` / `IssueUpdated`
1106///    event operation.
1107/// 3. Spawns a fire-and-forget [`crate::persistent_issues::save_atomic`] task.
1108///
1109/// [`IssueRegistry`]: crate::issue_registry::IssueRegistry
1110#[derive(Debug, Clone)]
1111pub enum PersistentIssueOp {
1112    /// Add a new persistent issue and write the updated list to disk.
1113    Add { issue: PersistentNewIssue },
1114    /// Remove a persistent issue by ID and write the updated list to disk.
1115    Remove { id: IssueId },
1116    /// Mark a persistent issue as resolved and write the updated list to disk.
1117    Resolve { id: IssueId },
1118    /// Mark a persistent issue as dismissed and write the updated list to disk.
1119    Dismiss { id: IssueId },
1120}
1121
1122#[derive(Debug, Clone, PartialEq, Eq)]
1123pub enum NotificationLevel {
1124    Info,
1125    Warn,
1126    Error,
1127}
1128
1129#[derive(Debug, Clone, PartialEq, Eq)]
1130pub enum StatusSlot {
1131    Left,
1132    Center,
1133    Right,
1134}
1135
1136#[derive(Debug, Clone)]
1137#[allow(unused)]
1138pub struct PickerItem {
1139    #[allow(dead_code)]
1140    pub label: String,
1141    #[allow(dead_code)]
1142    pub value: String,
1143}
1144
1145#[derive(Debug, Clone)]
1146pub struct Event {
1147    /// Which subsystem produced the operation: "editor", "file_selector", "plugin.git"
1148    #[allow(dead_code)]
1149    pub source: Cow<'static, str>,
1150    #[allow(dead_code)]
1151    pub kind: EventKind,
1152}
1153
1154#[derive(Debug, Clone)]
1155pub enum EventKind {
1156    #[allow(dead_code)]
1157    Applied(Operation),
1158}
1159
1160impl Event {
1161    pub fn applied(source: impl Into<Cow<'static, str>>, op: Operation) -> Self {
1162        Self {
1163            source: source.into(),
1164            kind: EventKind::Applied(op),
1165        }
1166    }
1167}