fresh_core/
api.rs

1//! Plugin API: Safe interface for plugins to interact with the editor
2//!
3//! This module provides a safe, controlled API for plugins (Lua, WASM, etc.)
4//! to interact with the editor without direct access to internal state.
5//!
6//! # Type Safety Architecture
7//!
8//! Rust structs in this module serve as the **single source of truth** for the
9//! TypeScript plugin API. The type safety system works as follows:
10//!
11//! ```text
12//! Rust struct                  Generated TypeScript
13//! ───────────                  ────────────────────
14//! #[derive(TS, Deserialize)]   type ActionPopupOptions = {
15//! #[serde(deny_unknown_fields)]    id: string;
16//! struct ActionPopupOptions {      title: string;
17//!     id: String,                  message: string;
18//!     title: String,               actions: TsActionPopupAction[];
19//!     ...                      };
20//! }
21//! ```
22//!
23//! ## Key Patterns
24//!
25//! 1. **`#[derive(TS)]`** - Generates TypeScript type definitions via ts-rs
26//! 2. **`#[serde(deny_unknown_fields)]`** - Rejects typos/unknown fields at runtime
27//! 3. **`impl FromJs`** - Bridges rquickjs values to typed Rust structs
28//!
29//! ## Validation Layers
30//!
31//! | Layer                  | What it catches                          |
32//! |------------------------|------------------------------------------|
33//! | TypeScript compile     | Wrong field names, missing required fields |
34//! | Rust runtime (serde)   | Typos like `popup_id` instead of `id`    |
35//! | Rust compile           | Type mismatches in method signatures     |
36//!
37//! ## Limitations & Tradeoffs
38//!
39//! - **Manual parsing for complex types**: Some methods (e.g., `submitViewTransform`)
40//!   still use manual object parsing due to enum serialization complexity
41//! - **Two-step deserialization**: Complex nested structs may need
42//!   `rquickjs::Value → serde_json::Value → typed struct` due to rquickjs_serde limits
43//! - **Duplicate attributes**: Both `#[serde(...)]` and `#[ts(...)]` needed since
44//!   they control different things (runtime serialization vs compile-time codegen)
45
46use crate::command::{Command, Suggestion};
47use crate::file_explorer::FileExplorerDecoration;
48use crate::hooks::{HookCallback, HookRegistry};
49use crate::menu::{Menu, MenuItem};
50use crate::overlay::{OverlayHandle, OverlayNamespace};
51use crate::text_property::{TextProperty, TextPropertyEntry};
52use crate::BufferId;
53use crate::SplitId;
54use lsp_types;
55use serde::{Deserialize, Serialize};
56use serde_json::Value as JsonValue;
57use std::collections::HashMap;
58use std::ops::Range;
59use std::path::PathBuf;
60use std::sync::{Arc, RwLock};
61use ts_rs::TS;
62
63/// Minimal command registry for PluginApi.
64/// This is a stub that provides basic command storage for plugin use.
65/// The editor's full CommandRegistry lives in fresh-editor.
66pub struct CommandRegistry {
67    commands: std::sync::RwLock<Vec<Command>>,
68}
69
70impl CommandRegistry {
71    /// Create a new empty command registry
72    pub fn new() -> Self {
73        Self {
74            commands: std::sync::RwLock::new(Vec::new()),
75        }
76    }
77
78    /// Register a command
79    pub fn register(&self, command: Command) {
80        let mut commands = self.commands.write().unwrap();
81        commands.retain(|c| c.name != command.name);
82        commands.push(command);
83    }
84
85    /// Unregister a command by name  
86    pub fn unregister(&self, name: &str) {
87        let mut commands = self.commands.write().unwrap();
88        commands.retain(|c| c.name != name);
89    }
90}
91
92impl Default for CommandRegistry {
93    fn default() -> Self {
94        Self::new()
95    }
96}
97
98/// A callback ID for JavaScript promises in the plugin runtime.
99///
100/// This newtype distinguishes JS promise callbacks (resolved via `resolve_callback`)
101/// from Rust oneshot channel IDs (resolved via `send_plugin_response`).
102/// Using a newtype prevents accidentally mixing up these two callback mechanisms.
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
104#[ts(export)]
105pub struct JsCallbackId(pub u64);
106
107impl JsCallbackId {
108    /// Create a new JS callback ID
109    pub fn new(id: u64) -> Self {
110        Self(id)
111    }
112
113    /// Get the underlying u64 value
114    pub fn as_u64(self) -> u64 {
115        self.0
116    }
117}
118
119impl From<u64> for JsCallbackId {
120    fn from(id: u64) -> Self {
121        Self(id)
122    }
123}
124
125impl From<JsCallbackId> for u64 {
126    fn from(id: JsCallbackId) -> u64 {
127        id.0
128    }
129}
130
131impl std::fmt::Display for JsCallbackId {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        write!(f, "{}", self.0)
134    }
135}
136
137/// Result of creating a virtual buffer
138#[derive(Debug, Clone, Serialize, Deserialize, TS)]
139#[serde(rename_all = "camelCase")]
140#[ts(export, rename_all = "camelCase")]
141pub struct VirtualBufferResult {
142    /// The created buffer ID
143    #[ts(type = "number")]
144    pub buffer_id: u64,
145    /// The split ID (if created in a new split)
146    #[ts(type = "number | null")]
147    pub split_id: Option<u64>,
148}
149
150/// Response from the editor for async plugin operations
151#[derive(Debug, Clone, Serialize, Deserialize, TS)]
152#[ts(export)]
153pub enum PluginResponse {
154    /// Response to CreateVirtualBufferInSplit with the created buffer ID and split ID
155    VirtualBufferCreated {
156        request_id: u64,
157        buffer_id: BufferId,
158        split_id: Option<SplitId>,
159    },
160    /// Response to a plugin-initiated LSP request
161    LspRequest {
162        request_id: u64,
163        #[ts(type = "any")]
164        result: Result<JsonValue, String>,
165    },
166    /// Response to RequestHighlights
167    HighlightsComputed {
168        request_id: u64,
169        spans: Vec<TsHighlightSpan>,
170    },
171    /// Response to GetBufferText with the text content
172    BufferText {
173        request_id: u64,
174        text: Result<String, String>,
175    },
176    /// Response to GetLineStartPosition with the byte offset
177    LineStartPosition {
178        request_id: u64,
179        /// None if line is out of range, Some(offset) for valid line
180        position: Option<usize>,
181    },
182    /// Response to CreateCompositeBuffer with the created buffer ID
183    CompositeBufferCreated {
184        request_id: u64,
185        buffer_id: BufferId,
186    },
187}
188
189/// Messages sent from async plugin tasks to the synchronous main loop
190#[derive(Debug, Clone, Serialize, Deserialize, TS)]
191#[ts(export)]
192pub enum PluginAsyncMessage {
193    /// Plugin process completed with output
194    ProcessOutput {
195        /// Unique ID for this process
196        process_id: u64,
197        /// Standard output
198        stdout: String,
199        /// Standard error
200        stderr: String,
201        /// Exit code
202        exit_code: i32,
203    },
204    /// Plugin delay/timer completed
205    DelayComplete {
206        /// Callback ID to resolve
207        callback_id: u64,
208    },
209    /// Background process stdout data
210    ProcessStdout { process_id: u64, data: String },
211    /// Background process stderr data
212    ProcessStderr { process_id: u64, data: String },
213    /// Background process exited
214    ProcessExit {
215        process_id: u64,
216        callback_id: u64,
217        exit_code: i32,
218    },
219    /// Response for a plugin-initiated LSP request
220    LspResponse {
221        language: String,
222        request_id: u64,
223        #[ts(type = "any")]
224        result: Result<JsonValue, String>,
225    },
226    /// Generic plugin response (e.g., GetBufferText result)
227    PluginResponse(crate::api::PluginResponse),
228}
229
230/// Information about a cursor in the editor
231#[derive(Debug, Clone, Serialize, Deserialize, TS)]
232#[ts(export)]
233pub struct CursorInfo {
234    /// Byte position of the cursor
235    pub position: usize,
236    /// Selection range (if any)
237    #[cfg_attr(
238        feature = "plugins",
239        ts(type = "{ start: number; end: number } | null")
240    )]
241    pub selection: Option<Range<usize>>,
242}
243
244/// Specification for an action to execute, with optional repeat count
245#[derive(Debug, Clone, Serialize, Deserialize, TS)]
246#[serde(deny_unknown_fields)]
247#[ts(export)]
248pub struct ActionSpec {
249    /// Action name (e.g., "move_word_right", "delete_line")
250    pub action: String,
251    /// Number of times to repeat the action (default 1)
252    #[serde(default = "default_action_count")]
253    pub count: u32,
254}
255
256fn default_action_count() -> u32 {
257    1
258}
259
260/// Information about a buffer
261#[derive(Debug, Clone, Serialize, Deserialize, TS)]
262#[ts(export)]
263pub struct BufferInfo {
264    /// Buffer ID
265    #[ts(type = "number")]
266    pub id: BufferId,
267    /// File path (if any)
268    #[serde(serialize_with = "serialize_path")]
269    #[ts(type = "string")]
270    pub path: Option<PathBuf>,
271    /// Whether the buffer has been modified
272    pub modified: bool,
273    /// Length of buffer in bytes
274    pub length: usize,
275}
276
277fn serialize_path<S: serde::Serializer>(path: &Option<PathBuf>, s: S) -> Result<S::Ok, S::Error> {
278    s.serialize_str(
279        &path
280            .as_ref()
281            .map(|p| p.to_string_lossy().to_string())
282            .unwrap_or_default(),
283    )
284}
285
286/// Serialize ranges as [start, end] tuples for JS compatibility
287fn serialize_ranges_as_tuples<S>(ranges: &[Range<usize>], serializer: S) -> Result<S::Ok, S::Error>
288where
289    S: serde::Serializer,
290{
291    use serde::ser::SerializeSeq;
292    let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
293    for range in ranges {
294        seq.serialize_element(&(range.start, range.end))?;
295    }
296    seq.end()
297}
298
299/// Serialize optional ranges as [start, end] tuples for JS compatibility
300fn serialize_opt_ranges_as_tuples<S>(
301    ranges: &Option<Vec<Range<usize>>>,
302    serializer: S,
303) -> Result<S::Ok, S::Error>
304where
305    S: serde::Serializer,
306{
307    match ranges {
308        Some(ranges) => {
309            use serde::ser::SerializeSeq;
310            let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
311            for range in ranges {
312                seq.serialize_element(&(range.start, range.end))?;
313            }
314            seq.end()
315        }
316        None => serializer.serialize_none(),
317    }
318}
319
320/// Diff between current buffer content and last saved snapshot
321#[derive(Debug, Clone, Serialize, Deserialize, TS)]
322#[ts(export)]
323pub struct BufferSavedDiff {
324    pub equal: bool,
325    #[serde(serialize_with = "serialize_ranges_as_tuples")]
326    #[ts(type = "Array<[number, number]>")]
327    pub byte_ranges: Vec<Range<usize>>,
328    #[serde(serialize_with = "serialize_opt_ranges_as_tuples")]
329    #[ts(type = "Array<[number, number]> | null")]
330    pub line_ranges: Option<Vec<Range<usize>>>,
331}
332
333/// Information about the viewport
334#[derive(Debug, Clone, Serialize, Deserialize, TS)]
335#[serde(rename_all = "camelCase")]
336#[ts(export, rename_all = "camelCase")]
337pub struct ViewportInfo {
338    /// Byte position of the first visible line
339    pub top_byte: usize,
340    /// Left column offset (horizontal scroll)
341    pub left_column: usize,
342    /// Viewport width
343    pub width: u16,
344    /// Viewport height
345    pub height: u16,
346}
347
348/// Layout hints supplied by plugins (e.g., Compose mode)
349#[derive(Debug, Clone, Serialize, Deserialize, TS)]
350#[serde(rename_all = "camelCase")]
351#[ts(export, rename_all = "camelCase")]
352pub struct LayoutHints {
353    /// Optional compose width for centering/wrapping
354    pub compose_width: Option<u16>,
355    /// Optional column guides for aligned tables
356    pub column_guides: Option<Vec<u16>>,
357}
358
359// ============================================================================
360// Composite Buffer Configuration (for multi-buffer single-tab views)
361// ============================================================================
362
363/// Layout configuration for composite buffers
364#[derive(Debug, Clone, Serialize, Deserialize, TS)]
365#[serde(deny_unknown_fields)]
366#[ts(export, rename = "TsCompositeLayoutConfig")]
367pub struct CompositeLayoutConfig {
368    /// Layout type: "side-by-side", "stacked", or "unified"
369    #[serde(rename = "type")]
370    #[ts(rename = "type")]
371    pub layout_type: String,
372    /// Width ratios for side-by-side (e.g., [0.5, 0.5])
373    #[serde(default)]
374    pub ratios: Option<Vec<f32>>,
375    /// Show separator between panes
376    #[serde(default = "default_true", rename = "showSeparator")]
377    #[ts(rename = "showSeparator")]
378    pub show_separator: bool,
379    /// Spacing for stacked layout
380    #[serde(default)]
381    pub spacing: Option<u16>,
382}
383
384fn default_true() -> bool {
385    true
386}
387
388/// Source pane configuration for composite buffers
389#[derive(Debug, Clone, Serialize, Deserialize, TS)]
390#[serde(deny_unknown_fields)]
391#[ts(export, rename = "TsCompositeSourceConfig")]
392pub struct CompositeSourceConfig {
393    /// Buffer ID of the source buffer (required)
394    #[serde(rename = "bufferId")]
395    #[ts(rename = "bufferId")]
396    pub buffer_id: usize,
397    /// Label for this pane (e.g., "OLD", "NEW")
398    pub label: String,
399    /// Whether this pane is editable
400    #[serde(default)]
401    pub editable: bool,
402    /// Style configuration
403    #[serde(default)]
404    pub style: Option<CompositePaneStyle>,
405}
406
407/// Style configuration for a composite pane
408#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
409#[serde(deny_unknown_fields)]
410#[ts(export, rename = "TsCompositePaneStyle")]
411pub struct CompositePaneStyle {
412    /// Background color for added lines (RGB)
413    /// Using [u8; 3] instead of (u8, u8, u8) for better rquickjs_serde compatibility
414    #[serde(default, rename = "addBg")]
415    #[ts(rename = "addBg", type = "[number, number, number] | null")]
416    pub add_bg: Option<[u8; 3]>,
417    /// Background color for removed lines (RGB)
418    #[serde(default, rename = "removeBg")]
419    #[ts(rename = "removeBg", type = "[number, number, number] | null")]
420    pub remove_bg: Option<[u8; 3]>,
421    /// Background color for modified lines (RGB)
422    #[serde(default, rename = "modifyBg")]
423    #[ts(rename = "modifyBg", type = "[number, number, number] | null")]
424    pub modify_bg: Option<[u8; 3]>,
425    /// Gutter style: "line-numbers", "diff-markers", "both", or "none"
426    #[serde(default, rename = "gutterStyle")]
427    #[ts(rename = "gutterStyle")]
428    pub gutter_style: Option<String>,
429}
430
431/// Diff hunk for composite buffer alignment
432#[derive(Debug, Clone, Serialize, Deserialize, TS)]
433#[serde(deny_unknown_fields)]
434#[ts(export, rename = "TsCompositeHunk")]
435pub struct CompositeHunk {
436    /// Starting line in old buffer (0-indexed)
437    #[serde(rename = "oldStart")]
438    #[ts(rename = "oldStart")]
439    pub old_start: usize,
440    /// Number of lines in old buffer
441    #[serde(rename = "oldCount")]
442    #[ts(rename = "oldCount")]
443    pub old_count: usize,
444    /// Starting line in new buffer (0-indexed)
445    #[serde(rename = "newStart")]
446    #[ts(rename = "newStart")]
447    pub new_start: usize,
448    /// Number of lines in new buffer
449    #[serde(rename = "newCount")]
450    #[ts(rename = "newCount")]
451    pub new_count: usize,
452}
453
454/// Options for creating a composite buffer (used by plugin API)
455#[derive(Debug, Clone, Serialize, Deserialize, TS)]
456#[serde(deny_unknown_fields)]
457#[ts(export, rename = "TsCreateCompositeBufferOptions")]
458pub struct CreateCompositeBufferOptions {
459    /// Buffer name (displayed in tabs/title)
460    #[serde(default)]
461    pub name: String,
462    /// Mode for keybindings
463    #[serde(default)]
464    pub mode: String,
465    /// Layout configuration
466    pub layout: CompositeLayoutConfig,
467    /// Source pane configurations
468    pub sources: Vec<CompositeSourceConfig>,
469    /// Diff hunks for alignment (optional)
470    #[serde(default)]
471    pub hunks: Option<Vec<CompositeHunk>>,
472}
473
474/// Wire-format view token kind (serialized for plugin transforms)
475#[derive(Debug, Clone, Serialize, Deserialize, TS)]
476#[ts(export)]
477pub enum ViewTokenWireKind {
478    Text(String),
479    Newline,
480    Space,
481    /// Visual line break inserted by wrapping (not from source)
482    /// Always has source_offset: None
483    Break,
484    /// A single binary byte that should be rendered as <XX>
485    /// Used in binary file mode to ensure cursor positioning works correctly
486    /// (all 4 display chars of <XX> map to the same source byte)
487    BinaryByte(u8),
488}
489
490/// Styling for view tokens (used for injected annotations)
491///
492/// This allows plugins to specify styling for tokens that don't have a source
493/// mapping (sourceOffset: None), such as annotation headers in git blame.
494/// For tokens with sourceOffset: Some(_), syntax highlighting is applied instead.
495#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
496#[serde(deny_unknown_fields)]
497#[ts(export)]
498pub struct ViewTokenStyle {
499    /// Foreground color as RGB tuple
500    #[serde(default)]
501    #[ts(type = "[number, number, number] | null")]
502    pub fg: Option<(u8, u8, u8)>,
503    /// Background color as RGB tuple
504    #[serde(default)]
505    #[ts(type = "[number, number, number] | null")]
506    pub bg: Option<(u8, u8, u8)>,
507    /// Whether to render in bold
508    #[serde(default)]
509    pub bold: bool,
510    /// Whether to render in italic
511    #[serde(default)]
512    pub italic: bool,
513}
514
515/// Wire-format view token with optional source mapping and styling
516#[derive(Debug, Clone, Serialize, Deserialize, TS)]
517#[serde(deny_unknown_fields)]
518#[ts(export)]
519pub struct ViewTokenWire {
520    /// Source byte offset in the buffer. None for injected content (annotations).
521    #[ts(type = "number | null")]
522    pub source_offset: Option<usize>,
523    /// The token content
524    pub kind: ViewTokenWireKind,
525    /// Optional styling for injected content (only used when source_offset is None)
526    #[serde(default)]
527    #[ts(optional)]
528    pub style: Option<ViewTokenStyle>,
529}
530
531/// Transformed view stream payload (plugin-provided)
532#[derive(Debug, Clone, Serialize, Deserialize, TS)]
533#[ts(export)]
534pub struct ViewTransformPayload {
535    /// Byte range this transform applies to (viewport)
536    pub range: Range<usize>,
537    /// Tokens in wire format
538    pub tokens: Vec<ViewTokenWire>,
539    /// Layout hints
540    pub layout_hints: Option<LayoutHints>,
541}
542
543/// Snapshot of editor state for plugin queries
544/// This is updated by the editor on each loop iteration
545#[derive(Debug, Clone, Serialize, Deserialize, TS)]
546#[ts(export)]
547pub struct EditorStateSnapshot {
548    /// Currently active buffer ID
549    pub active_buffer_id: BufferId,
550    /// Currently active split ID
551    pub active_split_id: usize,
552    /// Information about all open buffers
553    pub buffers: HashMap<BufferId, BufferInfo>,
554    /// Diff vs last saved snapshot for each buffer (line counts may be unknown)
555    pub buffer_saved_diffs: HashMap<BufferId, BufferSavedDiff>,
556    /// Primary cursor position for the active buffer
557    pub primary_cursor: Option<CursorInfo>,
558    /// All cursor positions for the active buffer
559    pub all_cursors: Vec<CursorInfo>,
560    /// Viewport information for the active buffer
561    pub viewport: Option<ViewportInfo>,
562    /// Cursor positions per buffer (for buffers other than active)
563    pub buffer_cursor_positions: HashMap<BufferId, usize>,
564    /// Text properties per buffer (for virtual buffers with properties)
565    pub buffer_text_properties: HashMap<BufferId, Vec<TextProperty>>,
566    /// Selected text from the primary cursor (if any selection exists)
567    /// This is populated on each update to avoid needing full buffer access
568    pub selected_text: Option<String>,
569    /// Internal clipboard content (for plugins that need clipboard access)
570    pub clipboard: String,
571    /// Editor's working directory (for file operations and spawning processes)
572    pub working_dir: PathBuf,
573    /// LSP diagnostics per file URI
574    /// Maps file URI string to Vec of diagnostics for that file
575    #[ts(type = "any")]
576    pub diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
577    /// Runtime config as serde_json::Value (merged user config + defaults)
578    /// This is the runtime config, not just the user's config file
579    #[ts(type = "any")]
580    pub config: serde_json::Value,
581    /// User config as serde_json::Value (only what's in the user's config file)
582    /// Fields not present here are using default values
583    #[ts(type = "any")]
584    pub user_config: serde_json::Value,
585    /// Global editor mode for modal editing (e.g., "vi-normal", "vi-insert")
586    /// When set, this mode's keybindings take precedence over normal key handling
587    pub editor_mode: Option<String>,
588}
589
590impl EditorStateSnapshot {
591    pub fn new() -> Self {
592        Self {
593            active_buffer_id: BufferId(0),
594            active_split_id: 0,
595            buffers: HashMap::new(),
596            buffer_saved_diffs: HashMap::new(),
597            primary_cursor: None,
598            all_cursors: Vec::new(),
599            viewport: None,
600            buffer_cursor_positions: HashMap::new(),
601            buffer_text_properties: HashMap::new(),
602            selected_text: None,
603            clipboard: String::new(),
604            working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
605            diagnostics: HashMap::new(),
606            config: serde_json::Value::Null,
607            user_config: serde_json::Value::Null,
608            editor_mode: None,
609        }
610    }
611}
612
613impl Default for EditorStateSnapshot {
614    fn default() -> Self {
615        Self::new()
616    }
617}
618
619/// Position for inserting menu items or menus
620#[derive(Debug, Clone, Serialize, Deserialize, TS)]
621#[ts(export)]
622pub enum MenuPosition {
623    /// Add at the beginning
624    Top,
625    /// Add at the end
626    Bottom,
627    /// Add before a specific label
628    Before(String),
629    /// Add after a specific label
630    After(String),
631}
632
633/// Plugin command - allows plugins to send commands to the editor
634#[derive(Debug, Clone, Serialize, Deserialize, TS)]
635#[ts(export)]
636pub enum PluginCommand {
637    /// Insert text at a position in a buffer
638    InsertText {
639        buffer_id: BufferId,
640        position: usize,
641        text: String,
642    },
643
644    /// Delete a range of text from a buffer
645    DeleteRange {
646        buffer_id: BufferId,
647        range: Range<usize>,
648    },
649
650    /// Add an overlay to a buffer, returns handle via response channel
651    AddOverlay {
652        buffer_id: BufferId,
653        namespace: Option<OverlayNamespace>,
654        range: Range<usize>,
655        color: (u8, u8, u8),
656        bg_color: Option<(u8, u8, u8)>,
657        underline: bool,
658        bold: bool,
659        italic: bool,
660        extend_to_line_end: bool,
661    },
662
663    /// Remove an overlay by its opaque handle
664    RemoveOverlay {
665        buffer_id: BufferId,
666        handle: OverlayHandle,
667    },
668
669    /// Set status message
670    SetStatus { message: String },
671
672    /// Apply a theme by name
673    ApplyTheme { theme_name: String },
674
675    /// Reload configuration from file
676    /// After a plugin saves config changes, it should call this to reload the config
677    ReloadConfig,
678
679    /// Register a custom command
680    RegisterCommand { command: Command },
681
682    /// Unregister a command by name
683    UnregisterCommand { name: String },
684
685    /// Open a file in the editor (in background, without switching focus)
686    OpenFileInBackground { path: PathBuf },
687
688    /// Insert text at the current cursor position in the active buffer
689    InsertAtCursor { text: String },
690
691    /// Spawn an async process
692    SpawnProcess {
693        command: String,
694        args: Vec<String>,
695        cwd: Option<String>,
696        callback_id: JsCallbackId,
697    },
698
699    /// Delay/sleep for a duration (async, resolves callback when done)
700    Delay {
701        callback_id: JsCallbackId,
702        duration_ms: u64,
703    },
704
705    /// Spawn a long-running background process
706    /// Unlike SpawnProcess, this returns immediately with a process handle
707    /// and provides streaming output via hooks
708    SpawnBackgroundProcess {
709        /// Unique ID for this process (generated by plugin runtime)
710        process_id: u64,
711        /// Command to execute
712        command: String,
713        /// Arguments to pass
714        args: Vec<String>,
715        /// Working directory (optional)
716        cwd: Option<String>,
717        /// Callback ID to call when process exits
718        callback_id: JsCallbackId,
719    },
720
721    /// Kill a background process by ID
722    KillBackgroundProcess { process_id: u64 },
723
724    /// Wait for a process to complete and get its result
725    /// Used with processes started via SpawnProcess
726    SpawnProcessWait {
727        /// Process ID to wait for
728        process_id: u64,
729        /// Callback ID for async response
730        callback_id: JsCallbackId,
731    },
732
733    /// Set layout hints for a buffer/viewport
734    SetLayoutHints {
735        buffer_id: BufferId,
736        split_id: Option<SplitId>,
737        range: Range<usize>,
738        hints: LayoutHints,
739    },
740
741    /// Enable/disable line numbers for a buffer
742    SetLineNumbers { buffer_id: BufferId, enabled: bool },
743
744    /// Submit a transformed view stream for a viewport
745    SubmitViewTransform {
746        buffer_id: BufferId,
747        split_id: Option<SplitId>,
748        payload: ViewTransformPayload,
749    },
750
751    /// Clear view transform for a buffer/split (returns to normal rendering)
752    ClearViewTransform {
753        buffer_id: BufferId,
754        split_id: Option<SplitId>,
755    },
756
757    /// Remove all overlays from a buffer
758    ClearAllOverlays { buffer_id: BufferId },
759
760    /// Remove all overlays in a namespace
761    ClearNamespace {
762        buffer_id: BufferId,
763        namespace: OverlayNamespace,
764    },
765
766    /// Remove all overlays that overlap with a byte range
767    /// Used for targeted invalidation when content in a range changes
768    ClearOverlaysInRange {
769        buffer_id: BufferId,
770        start: usize,
771        end: usize,
772    },
773
774    /// Add virtual text (inline text that doesn't exist in the buffer)
775    /// Used for color swatches, type hints, parameter hints, etc.
776    AddVirtualText {
777        buffer_id: BufferId,
778        virtual_text_id: String,
779        position: usize,
780        text: String,
781        color: (u8, u8, u8),
782        use_bg: bool, // true = use color as background, false = use as foreground
783        before: bool, // true = before char, false = after char
784    },
785
786    /// Remove a virtual text by ID
787    RemoveVirtualText {
788        buffer_id: BufferId,
789        virtual_text_id: String,
790    },
791
792    /// Remove virtual texts whose ID starts with the given prefix
793    RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
794
795    /// Clear all virtual texts from a buffer
796    ClearVirtualTexts { buffer_id: BufferId },
797
798    /// Add a virtual LINE (full line above/below a position)
799    /// Used for git blame headers, code coverage, inline documentation, etc.
800    /// These lines do NOT show line numbers in the gutter.
801    AddVirtualLine {
802        buffer_id: BufferId,
803        /// Byte position to anchor the line to
804        position: usize,
805        /// Full line content to display
806        text: String,
807        /// Foreground color (RGB)
808        fg_color: (u8, u8, u8),
809        /// Background color (RGB), None = transparent
810        bg_color: Option<(u8, u8, u8)>,
811        /// true = above the line containing position, false = below
812        above: bool,
813        /// Namespace for bulk removal (e.g., "git-blame")
814        namespace: String,
815        /// Priority for ordering multiple lines at same position (higher = later)
816        priority: i32,
817    },
818
819    /// Clear all virtual texts in a namespace
820    /// This is the primary way to remove a plugin's virtual lines before updating them.
821    ClearVirtualTextNamespace {
822        buffer_id: BufferId,
823        namespace: String,
824    },
825
826    /// Refresh lines for a buffer (clear seen_lines cache to re-trigger lines_changed hook)
827    RefreshLines { buffer_id: BufferId },
828
829    /// Set a line indicator in the gutter's indicator column
830    /// Used for git gutter, breakpoints, bookmarks, etc.
831    SetLineIndicator {
832        buffer_id: BufferId,
833        /// Line number (0-indexed)
834        line: usize,
835        /// Namespace for grouping (e.g., "git-gutter", "breakpoints")
836        namespace: String,
837        /// Symbol to display (e.g., "│", "●", "★")
838        symbol: String,
839        /// Color as RGB tuple
840        color: (u8, u8, u8),
841        /// Priority for display when multiple indicators exist (higher wins)
842        priority: i32,
843    },
844
845    /// Clear all line indicators for a specific namespace
846    ClearLineIndicators {
847        buffer_id: BufferId,
848        /// Namespace to clear (e.g., "git-gutter")
849        namespace: String,
850    },
851
852    /// Set file explorer decorations for a namespace
853    SetFileExplorerDecorations {
854        /// Namespace for grouping (e.g., "git-status")
855        namespace: String,
856        /// Decorations to apply
857        decorations: Vec<FileExplorerDecoration>,
858    },
859
860    /// Clear file explorer decorations for a namespace
861    ClearFileExplorerDecorations {
862        /// Namespace to clear (e.g., "git-status")
863        namespace: String,
864    },
865
866    /// Open a file at a specific line and column
867    /// Line and column are 1-indexed to match git grep output
868    OpenFileAtLocation {
869        path: PathBuf,
870        line: Option<usize>,   // 1-indexed, None = go to start
871        column: Option<usize>, // 1-indexed, None = go to line start
872    },
873
874    /// Open a file in a specific split at a given line and column
875    /// Line and column are 1-indexed to match git grep output
876    OpenFileInSplit {
877        split_id: usize,
878        path: PathBuf,
879        line: Option<usize>,   // 1-indexed, None = go to start
880        column: Option<usize>, // 1-indexed, None = go to line start
881    },
882
883    /// Start a prompt (minibuffer) with a custom type identifier
884    /// This allows plugins to create interactive prompts
885    StartPrompt {
886        label: String,
887        prompt_type: String, // e.g., "git-grep", "git-find-file"
888    },
889
890    /// Start a prompt with pre-filled initial value
891    StartPromptWithInitial {
892        label: String,
893        prompt_type: String,
894        initial_value: String,
895    },
896
897    /// Start an async prompt that returns result via callback
898    /// The callback_id is used to resolve the promise when the prompt is confirmed or cancelled
899    StartPromptAsync {
900        label: String,
901        initial_value: String,
902        callback_id: JsCallbackId,
903    },
904
905    /// Update the suggestions list for the current prompt
906    /// Uses the editor's Suggestion type
907    SetPromptSuggestions { suggestions: Vec<Suggestion> },
908
909    /// Add a menu item to an existing menu
910    /// Add a menu item to an existing menu
911    AddMenuItem {
912        menu_label: String,
913        item: MenuItem,
914        position: MenuPosition,
915    },
916
917    /// Add a new top-level menu
918    AddMenu { menu: Menu, position: MenuPosition },
919
920    /// Remove a menu item from a menu
921    RemoveMenuItem {
922        menu_label: String,
923        item_label: String,
924    },
925
926    /// Remove a top-level menu
927    RemoveMenu { menu_label: String },
928
929    /// Create a new virtual buffer (not backed by a file)
930    CreateVirtualBuffer {
931        /// Display name (e.g., "*Diagnostics*")
932        name: String,
933        /// Mode name for buffer-local keybindings (e.g., "diagnostics-list")
934        mode: String,
935        /// Whether the buffer is read-only
936        read_only: bool,
937    },
938
939    /// Create a virtual buffer and set its content in one operation
940    /// This is preferred over CreateVirtualBuffer + SetVirtualBufferContent
941    /// because it doesn't require tracking the buffer ID
942    CreateVirtualBufferWithContent {
943        /// Display name (e.g., "*Diagnostics*")
944        name: String,
945        /// Mode name for buffer-local keybindings (e.g., "diagnostics-list")
946        mode: String,
947        /// Whether the buffer is read-only
948        read_only: bool,
949        /// Entries with text and embedded properties
950        entries: Vec<TextPropertyEntry>,
951        /// Whether to show line numbers in the gutter
952        show_line_numbers: bool,
953        /// Whether to show cursors in the buffer
954        show_cursors: bool,
955        /// Whether editing is disabled (blocks editing commands)
956        editing_disabled: bool,
957        /// Whether this buffer should be hidden from tabs (for composite source buffers)
958        hidden_from_tabs: bool,
959        /// Optional request ID for async response
960        request_id: Option<u64>,
961    },
962
963    /// Create a virtual buffer in a horizontal split
964    /// Opens the buffer in a new pane below the current one
965    CreateVirtualBufferInSplit {
966        /// Display name (e.g., "*Diagnostics*")
967        name: String,
968        /// Mode name for buffer-local keybindings (e.g., "diagnostics-list")
969        mode: String,
970        /// Whether the buffer is read-only
971        read_only: bool,
972        /// Entries with text and embedded properties
973        entries: Vec<TextPropertyEntry>,
974        /// Split ratio (0.0 to 1.0, where 0.5 = equal split)
975        ratio: f32,
976        /// Split direction ("horizontal" or "vertical"), default horizontal
977        direction: Option<String>,
978        /// Optional panel ID for idempotent operations (if panel exists, update content)
979        panel_id: Option<String>,
980        /// Whether to show line numbers in the buffer (default true)
981        show_line_numbers: bool,
982        /// Whether to show cursors in the buffer (default true)
983        show_cursors: bool,
984        /// Whether editing is disabled for this buffer (default false)
985        editing_disabled: bool,
986        /// Whether line wrapping is enabled for this split (None = use global setting)
987        line_wrap: Option<bool>,
988        /// Optional request ID for async response (if set, editor will send back buffer ID)
989        request_id: Option<u64>,
990    },
991
992    /// Set the content of a virtual buffer with text properties
993    SetVirtualBufferContent {
994        buffer_id: BufferId,
995        /// Entries with text and embedded properties
996        entries: Vec<TextPropertyEntry>,
997    },
998
999    /// Get text properties at the cursor position in a buffer
1000    GetTextPropertiesAtCursor { buffer_id: BufferId },
1001
1002    /// Define a buffer mode with keybindings
1003    DefineMode {
1004        name: String,
1005        parent: Option<String>,
1006        bindings: Vec<(String, String)>, // (key_string, command_name)
1007        read_only: bool,
1008    },
1009
1010    /// Switch the current split to display a buffer
1011    ShowBuffer { buffer_id: BufferId },
1012
1013    /// Create a virtual buffer in an existing split (replaces current buffer in that split)
1014    CreateVirtualBufferInExistingSplit {
1015        /// Display name (e.g., "*Commit Details*")
1016        name: String,
1017        /// Mode name for buffer-local keybindings
1018        mode: String,
1019        /// Whether the buffer is read-only
1020        read_only: bool,
1021        /// Entries with text and embedded properties
1022        entries: Vec<TextPropertyEntry>,
1023        /// Target split ID where the buffer should be displayed
1024        split_id: SplitId,
1025        /// Whether to show line numbers in the buffer (default true)
1026        show_line_numbers: bool,
1027        /// Whether to show cursors in the buffer (default true)
1028        show_cursors: bool,
1029        /// Whether editing is disabled for this buffer (default false)
1030        editing_disabled: bool,
1031        /// Whether line wrapping is enabled for this split (None = use global setting)
1032        line_wrap: Option<bool>,
1033        /// Optional request ID for async response
1034        request_id: Option<u64>,
1035    },
1036
1037    /// Close a buffer and remove it from all splits
1038    CloseBuffer { buffer_id: BufferId },
1039
1040    /// Create a composite buffer that displays multiple source buffers
1041    /// Used for side-by-side diff, unified diff, and 3-way merge views
1042    CreateCompositeBuffer {
1043        /// Display name (shown in tab bar)
1044        name: String,
1045        /// Mode name for keybindings (e.g., "diff-view")
1046        mode: String,
1047        /// Layout configuration
1048        layout: CompositeLayoutConfig,
1049        /// Source pane configurations
1050        sources: Vec<CompositeSourceConfig>,
1051        /// Diff hunks for line alignment (optional)
1052        hunks: Option<Vec<CompositeHunk>>,
1053        /// Request ID for async response
1054        request_id: Option<u64>,
1055    },
1056
1057    /// Update alignment for a composite buffer (e.g., after source edit)
1058    UpdateCompositeAlignment {
1059        buffer_id: BufferId,
1060        hunks: Vec<CompositeHunk>,
1061    },
1062
1063    /// Close a composite buffer
1064    CloseCompositeBuffer { buffer_id: BufferId },
1065
1066    /// Focus a specific split
1067    FocusSplit { split_id: SplitId },
1068
1069    /// Set the buffer displayed in a specific split
1070    SetSplitBuffer {
1071        split_id: SplitId,
1072        buffer_id: BufferId,
1073    },
1074
1075    /// Set the scroll position of a specific split
1076    SetSplitScroll { split_id: SplitId, top_byte: usize },
1077
1078    /// Request syntax highlights for a buffer range
1079    RequestHighlights {
1080        buffer_id: BufferId,
1081        range: Range<usize>,
1082        request_id: u64,
1083    },
1084
1085    /// Close a split (if not the last one)
1086    CloseSplit { split_id: SplitId },
1087
1088    /// Set the ratio of a split container
1089    SetSplitRatio {
1090        split_id: SplitId,
1091        /// Ratio between 0.0 and 1.0 (0.5 = equal split)
1092        ratio: f32,
1093    },
1094
1095    /// Distribute splits evenly - make all given splits equal size
1096    DistributeSplitsEvenly {
1097        /// Split IDs to distribute evenly
1098        split_ids: Vec<SplitId>,
1099    },
1100
1101    /// Set cursor position in a buffer (also scrolls viewport to show cursor)
1102    SetBufferCursor {
1103        buffer_id: BufferId,
1104        /// Byte offset position for the cursor
1105        position: usize,
1106    },
1107
1108    /// Send an arbitrary LSP request and return the raw JSON response
1109    SendLspRequest {
1110        language: String,
1111        method: String,
1112        #[ts(type = "any")]
1113        params: Option<JsonValue>,
1114        request_id: u64,
1115    },
1116
1117    /// Set the internal clipboard content
1118    SetClipboard { text: String },
1119
1120    /// Delete the current selection in the active buffer
1121    /// This deletes all selected text across all cursors
1122    DeleteSelection,
1123
1124    /// Set or unset a custom context
1125    /// Custom contexts are plugin-defined states that can be used to control command visibility
1126    /// For example, "config-editor" context could make config editor commands available
1127    SetContext {
1128        /// Context name (e.g., "config-editor")
1129        name: String,
1130        /// Whether the context is active
1131        active: bool,
1132    },
1133
1134    /// Set the hunks for the Review Diff tool
1135    SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1136
1137    /// Execute an editor action by name (e.g., "move_word_right", "delete_line")
1138    /// Used by vi mode plugin to run motions and calculate cursor ranges
1139    ExecuteAction {
1140        /// Action name (e.g., "move_word_right", "move_line_end")
1141        action_name: String,
1142    },
1143
1144    /// Execute multiple actions in sequence, each with an optional repeat count
1145    /// Used by vi mode for count prefix (e.g., "3dw" = delete 3 words)
1146    /// All actions execute atomically with no plugin roundtrips between them
1147    ExecuteActions {
1148        /// List of actions to execute in sequence
1149        actions: Vec<ActionSpec>,
1150    },
1151
1152    /// Get text from a buffer range (for yank operations)
1153    GetBufferText {
1154        /// Buffer ID
1155        buffer_id: BufferId,
1156        /// Start byte offset
1157        start: usize,
1158        /// End byte offset
1159        end: usize,
1160        /// Request ID for async response
1161        request_id: u64,
1162    },
1163
1164    /// Get byte offset of the start of a line (async)
1165    /// Line is 0-indexed (0 = first line)
1166    GetLineStartPosition {
1167        /// Buffer ID (0 for active buffer)
1168        buffer_id: BufferId,
1169        /// Line number (0-indexed)
1170        line: u32,
1171        /// Request ID for async response
1172        request_id: u64,
1173    },
1174
1175    /// Set the global editor mode (for modal editing like vi mode)
1176    /// When set, the mode's keybindings take precedence over normal editing
1177    SetEditorMode {
1178        /// Mode name (e.g., "vi-normal", "vi-insert") or None to clear
1179        mode: Option<String>,
1180    },
1181
1182    /// Show an action popup with buttons for user interaction
1183    /// When the user selects an action, the ActionPopupResult hook is fired
1184    ShowActionPopup {
1185        /// Unique identifier for the popup (used in ActionPopupResult)
1186        popup_id: String,
1187        /// Title text for the popup
1188        title: String,
1189        /// Body message (supports basic formatting)
1190        message: String,
1191        /// Action buttons to display
1192        actions: Vec<ActionPopupAction>,
1193    },
1194
1195    /// Disable LSP for a specific language and persist to config
1196    DisableLspForLanguage {
1197        /// The language to disable LSP for (e.g., "python", "rust")
1198        language: String,
1199    },
1200
1201    /// Set the workspace root URI for a specific language's LSP server
1202    /// This allows plugins to specify project roots (e.g., directory containing .csproj)
1203    /// If the LSP is already running, it will be restarted with the new root
1204    SetLspRootUri {
1205        /// The language to set root URI for (e.g., "csharp", "rust")
1206        language: String,
1207        /// The root URI (file:// URL format)
1208        uri: String,
1209    },
1210
1211    /// Create a scroll sync group for anchor-based synchronized scrolling
1212    /// Used for side-by-side diff views where two panes need to scroll together
1213    /// The plugin provides the group ID (must be unique per plugin)
1214    CreateScrollSyncGroup {
1215        /// Plugin-assigned group ID
1216        group_id: u32,
1217        /// The left (primary) split - scroll position is tracked in this split's line space
1218        left_split: SplitId,
1219        /// The right (secondary) split - position is derived from anchors
1220        right_split: SplitId,
1221    },
1222
1223    /// Set sync anchors for a scroll sync group
1224    /// Anchors map corresponding line numbers between left and right buffers
1225    SetScrollSyncAnchors {
1226        /// The group ID returned by CreateScrollSyncGroup
1227        group_id: u32,
1228        /// List of (left_line, right_line) pairs marking corresponding positions
1229        anchors: Vec<(usize, usize)>,
1230    },
1231
1232    /// Remove a scroll sync group
1233    RemoveScrollSyncGroup {
1234        /// The group ID returned by CreateScrollSyncGroup
1235        group_id: u32,
1236    },
1237
1238    /// Save a buffer to a specific file path
1239    /// Used by :w filename command to save unnamed buffers or save-as
1240    SaveBufferToPath {
1241        /// Buffer ID to save
1242        buffer_id: BufferId,
1243        /// Path to save to
1244        path: PathBuf,
1245    },
1246}
1247
1248/// Hunk status for Review Diff
1249#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1250#[ts(export)]
1251pub enum HunkStatus {
1252    Pending,
1253    Staged,
1254    Discarded,
1255}
1256
1257/// A high-level hunk directive for the Review Diff tool
1258#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1259#[ts(export)]
1260pub struct ReviewHunk {
1261    pub id: String,
1262    pub file: String,
1263    pub context_header: String,
1264    pub status: HunkStatus,
1265    /// 0-indexed line range in the base (HEAD) version
1266    pub base_range: Option<(usize, usize)>,
1267    /// 0-indexed line range in the modified (Working) version
1268    pub modified_range: Option<(usize, usize)>,
1269}
1270
1271/// Action button for action popups
1272#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1273#[serde(deny_unknown_fields)]
1274#[ts(export, rename = "TsActionPopupAction")]
1275pub struct ActionPopupAction {
1276    /// Unique action identifier (returned in ActionPopupResult)
1277    pub id: String,
1278    /// Display text for the button (can include command hints)
1279    pub label: String,
1280}
1281
1282/// Options for showActionPopup
1283#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1284#[serde(deny_unknown_fields)]
1285#[ts(export)]
1286pub struct ActionPopupOptions {
1287    /// Unique identifier for the popup (used in ActionPopupResult)
1288    pub id: String,
1289    /// Title text for the popup
1290    pub title: String,
1291    /// Body message (supports basic formatting)
1292    pub message: String,
1293    /// Action buttons to display
1294    pub actions: Vec<ActionPopupAction>,
1295}
1296
1297/// Syntax highlight span for a buffer range
1298#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1299#[ts(export)]
1300pub struct TsHighlightSpan {
1301    pub start: u32,
1302    pub end: u32,
1303    #[ts(type = "[number, number, number]")]
1304    pub color: (u8, u8, u8),
1305    pub bold: bool,
1306    pub italic: bool,
1307}
1308
1309/// Result from spawning a process with spawnProcess
1310#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1311#[ts(export)]
1312pub struct SpawnResult {
1313    /// Complete stdout as string
1314    pub stdout: String,
1315    /// Complete stderr as string
1316    pub stderr: String,
1317    /// Process exit code (0 usually means success, -1 if killed)
1318    pub exit_code: i32,
1319}
1320
1321/// Result from spawning a background process
1322#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1323#[ts(export)]
1324pub struct BackgroundProcessResult {
1325    /// Unique process ID for later reference
1326    #[ts(type = "number")]
1327    pub process_id: u64,
1328    /// Process exit code (0 usually means success, -1 if killed)
1329    /// Only present when the process has exited
1330    pub exit_code: i32,
1331}
1332
1333/// Entry for virtual buffer content with optional text properties (JS API version)
1334#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1335#[serde(deny_unknown_fields)]
1336#[ts(export, rename = "TextPropertyEntry")]
1337pub struct JsTextPropertyEntry {
1338    /// Text content for this entry
1339    pub text: String,
1340    /// Optional properties attached to this text (e.g., file path, line number)
1341    #[serde(default)]
1342    #[ts(optional, type = "Record<string, unknown>")]
1343    pub properties: Option<HashMap<String, JsonValue>>,
1344}
1345
1346/// Directory entry returned by readDir
1347#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1348#[ts(export)]
1349pub struct DirEntry {
1350    /// File/directory name
1351    pub name: String,
1352    /// True if this is a file
1353    pub is_file: bool,
1354    /// True if this is a directory
1355    pub is_dir: bool,
1356}
1357
1358/// Position in a document (line and character)
1359#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1360#[ts(export)]
1361pub struct JsPosition {
1362    /// Zero-indexed line number
1363    pub line: u32,
1364    /// Zero-indexed character offset
1365    pub character: u32,
1366}
1367
1368/// Range in a document (start and end positions)
1369#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1370#[ts(export)]
1371pub struct JsRange {
1372    /// Start position
1373    pub start: JsPosition,
1374    /// End position
1375    pub end: JsPosition,
1376}
1377
1378/// Diagnostic from LSP
1379#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1380#[ts(export)]
1381pub struct JsDiagnostic {
1382    /// Document URI
1383    pub uri: String,
1384    /// Diagnostic message
1385    pub message: String,
1386    /// Severity: 1=Error, 2=Warning, 3=Info, 4=Hint, null=unknown
1387    pub severity: Option<u8>,
1388    /// Range in the document
1389    pub range: JsRange,
1390    /// Source of the diagnostic (e.g., "typescript", "eslint")
1391    #[ts(optional)]
1392    pub source: Option<String>,
1393}
1394
1395/// Options for createVirtualBuffer
1396#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1397#[serde(deny_unknown_fields)]
1398#[ts(export)]
1399pub struct CreateVirtualBufferOptions {
1400    /// Buffer name (displayed in tabs/title)
1401    pub name: String,
1402    /// Mode for keybindings (e.g., "git-log", "search-results")
1403    #[serde(default)]
1404    #[ts(optional)]
1405    pub mode: Option<String>,
1406    /// Whether buffer is read-only (default: false)
1407    #[serde(default, rename = "readOnly")]
1408    #[ts(optional, rename = "readOnly")]
1409    pub read_only: Option<bool>,
1410    /// Show line numbers in gutter (default: false)
1411    #[serde(default, rename = "showLineNumbers")]
1412    #[ts(optional, rename = "showLineNumbers")]
1413    pub show_line_numbers: Option<bool>,
1414    /// Show cursor (default: true)
1415    #[serde(default, rename = "showCursors")]
1416    #[ts(optional, rename = "showCursors")]
1417    pub show_cursors: Option<bool>,
1418    /// Disable text editing (default: false)
1419    #[serde(default, rename = "editingDisabled")]
1420    #[ts(optional, rename = "editingDisabled")]
1421    pub editing_disabled: Option<bool>,
1422    /// Hide from tab bar (default: false)
1423    #[serde(default, rename = "hiddenFromTabs")]
1424    #[ts(optional, rename = "hiddenFromTabs")]
1425    pub hidden_from_tabs: Option<bool>,
1426    /// Initial content entries with optional properties
1427    #[serde(default)]
1428    #[ts(optional)]
1429    pub entries: Option<Vec<JsTextPropertyEntry>>,
1430}
1431
1432/// Options for createVirtualBufferInSplit
1433#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1434#[serde(deny_unknown_fields)]
1435#[ts(export)]
1436pub struct CreateVirtualBufferInSplitOptions {
1437    /// Buffer name (displayed in tabs/title)
1438    pub name: String,
1439    /// Mode for keybindings (e.g., "git-log", "search-results")
1440    #[serde(default)]
1441    #[ts(optional)]
1442    pub mode: Option<String>,
1443    /// Whether buffer is read-only (default: false)
1444    #[serde(default, rename = "readOnly")]
1445    #[ts(optional, rename = "readOnly")]
1446    pub read_only: Option<bool>,
1447    /// Split ratio 0.0-1.0 (default: 0.5)
1448    #[serde(default)]
1449    #[ts(optional)]
1450    pub ratio: Option<f32>,
1451    /// Split direction: "horizontal" or "vertical"
1452    #[serde(default)]
1453    #[ts(optional)]
1454    pub direction: Option<String>,
1455    /// Panel ID to split from
1456    #[serde(default, rename = "panelId")]
1457    #[ts(optional, rename = "panelId")]
1458    pub panel_id: Option<String>,
1459    /// Show line numbers in gutter (default: true)
1460    #[serde(default, rename = "showLineNumbers")]
1461    #[ts(optional, rename = "showLineNumbers")]
1462    pub show_line_numbers: Option<bool>,
1463    /// Show cursor (default: true)
1464    #[serde(default, rename = "showCursors")]
1465    #[ts(optional, rename = "showCursors")]
1466    pub show_cursors: Option<bool>,
1467    /// Disable text editing (default: false)
1468    #[serde(default, rename = "editingDisabled")]
1469    #[ts(optional, rename = "editingDisabled")]
1470    pub editing_disabled: Option<bool>,
1471    /// Enable line wrapping
1472    #[serde(default, rename = "lineWrap")]
1473    #[ts(optional, rename = "lineWrap")]
1474    pub line_wrap: Option<bool>,
1475    /// Initial content entries with optional properties
1476    #[serde(default)]
1477    #[ts(optional)]
1478    pub entries: Option<Vec<JsTextPropertyEntry>>,
1479}
1480
1481/// Options for createVirtualBufferInExistingSplit
1482#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1483#[serde(deny_unknown_fields)]
1484#[ts(export)]
1485pub struct CreateVirtualBufferInExistingSplitOptions {
1486    /// Buffer name (displayed in tabs/title)
1487    pub name: String,
1488    /// Target split ID (required)
1489    #[serde(rename = "splitId")]
1490    #[ts(rename = "splitId")]
1491    pub split_id: usize,
1492    /// Mode for keybindings (e.g., "git-log", "search-results")
1493    #[serde(default)]
1494    #[ts(optional)]
1495    pub mode: Option<String>,
1496    /// Whether buffer is read-only (default: false)
1497    #[serde(default, rename = "readOnly")]
1498    #[ts(optional, rename = "readOnly")]
1499    pub read_only: Option<bool>,
1500    /// Show line numbers in gutter (default: true)
1501    #[serde(default, rename = "showLineNumbers")]
1502    #[ts(optional, rename = "showLineNumbers")]
1503    pub show_line_numbers: Option<bool>,
1504    /// Show cursor (default: true)
1505    #[serde(default, rename = "showCursors")]
1506    #[ts(optional, rename = "showCursors")]
1507    pub show_cursors: Option<bool>,
1508    /// Disable text editing (default: false)
1509    #[serde(default, rename = "editingDisabled")]
1510    #[ts(optional, rename = "editingDisabled")]
1511    pub editing_disabled: Option<bool>,
1512    /// Enable line wrapping
1513    #[serde(default, rename = "lineWrap")]
1514    #[ts(optional, rename = "lineWrap")]
1515    pub line_wrap: Option<bool>,
1516    /// Initial content entries with optional properties
1517    #[serde(default)]
1518    #[ts(optional)]
1519    pub entries: Option<Vec<JsTextPropertyEntry>>,
1520}
1521
1522/// Result of getTextPropertiesAtCursor - array of property objects
1523///
1524/// Each element contains the properties from a text property span that overlaps
1525/// with the cursor position. Properties are dynamic key-value pairs set by plugins.
1526#[derive(Debug, Clone, Serialize, TS)]
1527#[ts(export, type = "Array<Record<string, unknown>>")]
1528pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
1529
1530// Implement FromJs for option types using rquickjs_serde
1531#[cfg(feature = "plugins")]
1532mod fromjs_impls {
1533    use super::*;
1534    use rquickjs::{Ctx, FromJs, Value};
1535
1536    impl<'js> FromJs<'js> for JsTextPropertyEntry {
1537        fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1538            rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1539                from: "object",
1540                to: "JsTextPropertyEntry",
1541                message: Some(e.to_string()),
1542            })
1543        }
1544    }
1545
1546    impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
1547        fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1548            rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1549                from: "object",
1550                to: "CreateVirtualBufferOptions",
1551                message: Some(e.to_string()),
1552            })
1553        }
1554    }
1555
1556    impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
1557        fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1558            rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1559                from: "object",
1560                to: "CreateVirtualBufferInSplitOptions",
1561                message: Some(e.to_string()),
1562            })
1563        }
1564    }
1565
1566    impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
1567        fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1568            rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1569                from: "object",
1570                to: "CreateVirtualBufferInExistingSplitOptions",
1571                message: Some(e.to_string()),
1572            })
1573        }
1574    }
1575
1576    impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
1577        fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
1578            rquickjs_serde::to_value(ctx.clone(), &self.0)
1579                .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
1580        }
1581    }
1582
1583    // === Additional input types for type-safe plugin API ===
1584
1585    impl<'js> FromJs<'js> for ActionSpec {
1586        fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1587            rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1588                from: "object",
1589                to: "ActionSpec",
1590                message: Some(e.to_string()),
1591            })
1592        }
1593    }
1594
1595    impl<'js> FromJs<'js> for ActionPopupAction {
1596        fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1597            rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1598                from: "object",
1599                to: "ActionPopupAction",
1600                message: Some(e.to_string()),
1601            })
1602        }
1603    }
1604
1605    impl<'js> FromJs<'js> for ActionPopupOptions {
1606        fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1607            rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1608                from: "object",
1609                to: "ActionPopupOptions",
1610                message: Some(e.to_string()),
1611            })
1612        }
1613    }
1614
1615    impl<'js> FromJs<'js> for ViewTokenWire {
1616        fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1617            rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1618                from: "object",
1619                to: "ViewTokenWire",
1620                message: Some(e.to_string()),
1621            })
1622        }
1623    }
1624
1625    impl<'js> FromJs<'js> for ViewTokenStyle {
1626        fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1627            rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1628                from: "object",
1629                to: "ViewTokenStyle",
1630                message: Some(e.to_string()),
1631            })
1632        }
1633    }
1634
1635    impl<'js> FromJs<'js> for LayoutHints {
1636        fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1637            rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1638                from: "object",
1639                to: "LayoutHints",
1640                message: Some(e.to_string()),
1641            })
1642        }
1643    }
1644
1645    impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
1646        fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1647            // Use two-step deserialization for complex nested structures
1648            let json: serde_json::Value =
1649                rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1650                    from: "object",
1651                    to: "CreateCompositeBufferOptions (json)",
1652                    message: Some(e.to_string()),
1653                })?;
1654            serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
1655                from: "json",
1656                to: "CreateCompositeBufferOptions",
1657                message: Some(e.to_string()),
1658            })
1659        }
1660    }
1661
1662    impl<'js> FromJs<'js> for CompositeHunk {
1663        fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1664            rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1665                from: "object",
1666                to: "CompositeHunk",
1667                message: Some(e.to_string()),
1668            })
1669        }
1670    }
1671}
1672
1673/// Plugin API context - provides safe access to editor functionality
1674pub struct PluginApi {
1675    /// Hook registry (shared with editor)
1676    hooks: Arc<RwLock<HookRegistry>>,
1677
1678    /// Command registry (shared with editor)
1679    commands: Arc<RwLock<CommandRegistry>>,
1680
1681    /// Command queue for sending commands to editor
1682    command_sender: std::sync::mpsc::Sender<PluginCommand>,
1683
1684    /// Snapshot of editor state (read-only for plugins)
1685    state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
1686}
1687
1688impl PluginApi {
1689    /// Create a new plugin API context
1690    pub fn new(
1691        hooks: Arc<RwLock<HookRegistry>>,
1692        commands: Arc<RwLock<CommandRegistry>>,
1693        command_sender: std::sync::mpsc::Sender<PluginCommand>,
1694        state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
1695    ) -> Self {
1696        Self {
1697            hooks,
1698            commands,
1699            command_sender,
1700            state_snapshot,
1701        }
1702    }
1703
1704    /// Register a hook callback
1705    pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
1706        let mut hooks = self.hooks.write().unwrap();
1707        hooks.add_hook(hook_name, callback);
1708    }
1709
1710    /// Remove all hooks for a specific name
1711    pub fn unregister_hooks(&self, hook_name: &str) {
1712        let mut hooks = self.hooks.write().unwrap();
1713        hooks.remove_hooks(hook_name);
1714    }
1715
1716    /// Register a command
1717    pub fn register_command(&self, command: Command) {
1718        let commands = self.commands.read().unwrap();
1719        commands.register(command);
1720    }
1721
1722    /// Unregister a command by name
1723    pub fn unregister_command(&self, name: &str) {
1724        let commands = self.commands.read().unwrap();
1725        commands.unregister(name);
1726    }
1727
1728    /// Send a command to the editor (async/non-blocking)
1729    pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
1730        self.command_sender
1731            .send(command)
1732            .map_err(|e| format!("Failed to send command: {}", e))
1733    }
1734
1735    /// Insert text at a position in a buffer
1736    pub fn insert_text(
1737        &self,
1738        buffer_id: BufferId,
1739        position: usize,
1740        text: String,
1741    ) -> Result<(), String> {
1742        self.send_command(PluginCommand::InsertText {
1743            buffer_id,
1744            position,
1745            text,
1746        })
1747    }
1748
1749    /// Delete a range of text from a buffer
1750    pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
1751        self.send_command(PluginCommand::DeleteRange { buffer_id, range })
1752    }
1753
1754    /// Add an overlay (decoration) to a buffer
1755    /// Returns an opaque handle that can be used to remove the overlay later
1756    #[allow(clippy::too_many_arguments)]
1757    pub fn add_overlay(
1758        &self,
1759        buffer_id: BufferId,
1760        namespace: Option<String>,
1761        range: Range<usize>,
1762        color: (u8, u8, u8),
1763        bg_color: Option<(u8, u8, u8)>,
1764        underline: bool,
1765        bold: bool,
1766        italic: bool,
1767        extend_to_line_end: bool,
1768    ) -> Result<(), String> {
1769        self.send_command(PluginCommand::AddOverlay {
1770            buffer_id,
1771            namespace: namespace.map(OverlayNamespace::from_string),
1772            range,
1773            color,
1774            bg_color,
1775            underline,
1776            bold,
1777            italic,
1778            extend_to_line_end,
1779        })
1780    }
1781
1782    /// Remove an overlay from a buffer by its handle
1783    pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
1784        self.send_command(PluginCommand::RemoveOverlay {
1785            buffer_id,
1786            handle: OverlayHandle::from_string(handle),
1787        })
1788    }
1789
1790    /// Clear all overlays in a namespace from a buffer
1791    pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
1792        self.send_command(PluginCommand::ClearNamespace {
1793            buffer_id,
1794            namespace: OverlayNamespace::from_string(namespace),
1795        })
1796    }
1797
1798    /// Clear all overlays that overlap with a byte range
1799    /// Used for targeted invalidation when content changes
1800    pub fn clear_overlays_in_range(
1801        &self,
1802        buffer_id: BufferId,
1803        start: usize,
1804        end: usize,
1805    ) -> Result<(), String> {
1806        self.send_command(PluginCommand::ClearOverlaysInRange {
1807            buffer_id,
1808            start,
1809            end,
1810        })
1811    }
1812
1813    /// Set the status message
1814    pub fn set_status(&self, message: String) -> Result<(), String> {
1815        self.send_command(PluginCommand::SetStatus { message })
1816    }
1817
1818    /// Open a file at a specific line and column (1-indexed)
1819    /// This is useful for jumping to locations from git grep, LSP definitions, etc.
1820    pub fn open_file_at_location(
1821        &self,
1822        path: PathBuf,
1823        line: Option<usize>,
1824        column: Option<usize>,
1825    ) -> Result<(), String> {
1826        self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
1827    }
1828
1829    /// Open a file in a specific split at a line and column
1830    ///
1831    /// Similar to open_file_at_location but targets a specific split pane.
1832    /// The split_id is the ID of the split pane to open the file in.
1833    pub fn open_file_in_split(
1834        &self,
1835        split_id: usize,
1836        path: PathBuf,
1837        line: Option<usize>,
1838        column: Option<usize>,
1839    ) -> Result<(), String> {
1840        self.send_command(PluginCommand::OpenFileInSplit {
1841            split_id,
1842            path,
1843            line,
1844            column,
1845        })
1846    }
1847
1848    /// Start a prompt (minibuffer) with a custom type identifier
1849    /// The prompt_type is used to filter hooks in plugin code
1850    pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
1851        self.send_command(PluginCommand::StartPrompt { label, prompt_type })
1852    }
1853
1854    /// Set the suggestions for the current prompt
1855    /// This updates the prompt's autocomplete/selection list
1856    pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
1857        self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
1858    }
1859
1860    /// Add a menu item to an existing menu
1861    pub fn add_menu_item(
1862        &self,
1863        menu_label: String,
1864        item: MenuItem,
1865        position: MenuPosition,
1866    ) -> Result<(), String> {
1867        self.send_command(PluginCommand::AddMenuItem {
1868            menu_label,
1869            item,
1870            position,
1871        })
1872    }
1873
1874    /// Add a new top-level menu
1875    pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
1876        self.send_command(PluginCommand::AddMenu { menu, position })
1877    }
1878
1879    /// Remove a menu item from a menu
1880    pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
1881        self.send_command(PluginCommand::RemoveMenuItem {
1882            menu_label,
1883            item_label,
1884        })
1885    }
1886
1887    /// Remove a top-level menu
1888    pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
1889        self.send_command(PluginCommand::RemoveMenu { menu_label })
1890    }
1891
1892    // === Virtual Buffer Methods ===
1893
1894    /// Create a new virtual buffer (not backed by a file)
1895    ///
1896    /// Virtual buffers are used for special displays like diagnostic lists,
1897    /// search results, etc. They have their own mode for keybindings.
1898    pub fn create_virtual_buffer(
1899        &self,
1900        name: String,
1901        mode: String,
1902        read_only: bool,
1903    ) -> Result<(), String> {
1904        self.send_command(PluginCommand::CreateVirtualBuffer {
1905            name,
1906            mode,
1907            read_only,
1908        })
1909    }
1910
1911    /// Create a virtual buffer and set its content in one operation
1912    ///
1913    /// This is the preferred way to create virtual buffers since it doesn't
1914    /// require tracking the buffer ID. The buffer is created and populated
1915    /// atomically.
1916    pub fn create_virtual_buffer_with_content(
1917        &self,
1918        name: String,
1919        mode: String,
1920        read_only: bool,
1921        entries: Vec<TextPropertyEntry>,
1922    ) -> Result<(), String> {
1923        self.send_command(PluginCommand::CreateVirtualBufferWithContent {
1924            name,
1925            mode,
1926            read_only,
1927            entries,
1928            show_line_numbers: true,
1929            show_cursors: true,
1930            editing_disabled: false,
1931            hidden_from_tabs: false,
1932            request_id: None,
1933        })
1934    }
1935
1936    /// Set the content of a virtual buffer with text properties
1937    ///
1938    /// Each entry contains text and metadata properties (e.g., source location).
1939    pub fn set_virtual_buffer_content(
1940        &self,
1941        buffer_id: BufferId,
1942        entries: Vec<TextPropertyEntry>,
1943    ) -> Result<(), String> {
1944        self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
1945    }
1946
1947    /// Get text properties at cursor position in a buffer
1948    ///
1949    /// This triggers a command that will make properties available to plugins.
1950    pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
1951        self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
1952    }
1953
1954    /// Define a buffer mode with keybindings
1955    ///
1956    /// Modes can inherit from parent modes (e.g., "diagnostics-list" inherits from "special").
1957    /// Bindings are specified as (key_string, command_name) pairs.
1958    pub fn define_mode(
1959        &self,
1960        name: String,
1961        parent: Option<String>,
1962        bindings: Vec<(String, String)>,
1963        read_only: bool,
1964    ) -> Result<(), String> {
1965        self.send_command(PluginCommand::DefineMode {
1966            name,
1967            parent,
1968            bindings,
1969            read_only,
1970        })
1971    }
1972
1973    /// Switch the current split to display a buffer
1974    pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
1975        self.send_command(PluginCommand::ShowBuffer { buffer_id })
1976    }
1977
1978    /// Set the scroll position of a specific split
1979    pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
1980        self.send_command(PluginCommand::SetSplitScroll {
1981            split_id: SplitId(split_id),
1982            top_byte,
1983        })
1984    }
1985
1986    /// Request syntax highlights for a buffer range
1987    pub fn get_highlights(
1988        &self,
1989        buffer_id: BufferId,
1990        range: Range<usize>,
1991        request_id: u64,
1992    ) -> Result<(), String> {
1993        self.send_command(PluginCommand::RequestHighlights {
1994            buffer_id,
1995            range,
1996            request_id,
1997        })
1998    }
1999
2000    // === Query Methods ===
2001
2002    /// Get the currently active buffer ID
2003    pub fn get_active_buffer_id(&self) -> BufferId {
2004        let snapshot = self.state_snapshot.read().unwrap();
2005        snapshot.active_buffer_id
2006    }
2007
2008    /// Get the currently active split ID
2009    pub fn get_active_split_id(&self) -> usize {
2010        let snapshot = self.state_snapshot.read().unwrap();
2011        snapshot.active_split_id
2012    }
2013
2014    /// Get information about a specific buffer
2015    pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
2016        let snapshot = self.state_snapshot.read().unwrap();
2017        snapshot.buffers.get(&buffer_id).cloned()
2018    }
2019
2020    /// Get all buffer IDs
2021    pub fn list_buffers(&self) -> Vec<BufferInfo> {
2022        let snapshot = self.state_snapshot.read().unwrap();
2023        snapshot.buffers.values().cloned().collect()
2024    }
2025
2026    /// Get primary cursor information for the active buffer
2027    pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
2028        let snapshot = self.state_snapshot.read().unwrap();
2029        snapshot.primary_cursor.clone()
2030    }
2031
2032    /// Get all cursor information for the active buffer
2033    pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
2034        let snapshot = self.state_snapshot.read().unwrap();
2035        snapshot.all_cursors.clone()
2036    }
2037
2038    /// Get viewport information for the active buffer
2039    pub fn get_viewport(&self) -> Option<ViewportInfo> {
2040        let snapshot = self.state_snapshot.read().unwrap();
2041        snapshot.viewport.clone()
2042    }
2043
2044    /// Get access to the state snapshot Arc (for internal use)
2045    pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
2046        Arc::clone(&self.state_snapshot)
2047    }
2048}
2049
2050impl Clone for PluginApi {
2051    fn clone(&self) -> Self {
2052        Self {
2053            hooks: Arc::clone(&self.hooks),
2054            commands: Arc::clone(&self.commands),
2055            command_sender: self.command_sender.clone(),
2056            state_snapshot: Arc::clone(&self.state_snapshot),
2057        }
2058    }
2059}
2060
2061#[cfg(test)]
2062mod tests {
2063    use super::*;
2064
2065    #[test]
2066    fn test_plugin_api_creation() {
2067        let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2068        let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2069        let (tx, _rx) = std::sync::mpsc::channel();
2070        let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2071
2072        let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2073
2074        // Should not panic
2075        let _clone = api.clone();
2076    }
2077
2078    #[test]
2079    fn test_register_hook() {
2080        let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2081        let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2082        let (tx, _rx) = std::sync::mpsc::channel();
2083        let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2084
2085        let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
2086
2087        api.register_hook("test-hook", Box::new(|_| true));
2088
2089        let hook_registry = hooks.read().unwrap();
2090        assert_eq!(hook_registry.hook_count("test-hook"), 1);
2091    }
2092
2093    #[test]
2094    fn test_send_command() {
2095        let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2096        let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2097        let (tx, rx) = std::sync::mpsc::channel();
2098        let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2099
2100        let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2101
2102        let result = api.insert_text(BufferId(1), 0, "test".to_string());
2103        assert!(result.is_ok());
2104
2105        // Verify command was sent
2106        let received = rx.try_recv();
2107        assert!(received.is_ok());
2108
2109        match received.unwrap() {
2110            PluginCommand::InsertText {
2111                buffer_id,
2112                position,
2113                text,
2114            } => {
2115                assert_eq!(buffer_id.0, 1);
2116                assert_eq!(position, 0);
2117                assert_eq!(text, "test");
2118            }
2119            _ => panic!("Wrong command type"),
2120        }
2121    }
2122
2123    #[test]
2124    fn test_add_overlay_command() {
2125        let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2126        let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2127        let (tx, rx) = std::sync::mpsc::channel();
2128        let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2129
2130        let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2131
2132        let result = api.add_overlay(
2133            BufferId(1),
2134            Some("test-overlay".to_string()),
2135            0..10,
2136            (255, 0, 0),
2137            None,
2138            true,
2139            false,
2140            false,
2141            false,
2142        );
2143        assert!(result.is_ok());
2144
2145        let received = rx.try_recv().unwrap();
2146        match received {
2147            PluginCommand::AddOverlay {
2148                buffer_id,
2149                namespace,
2150                range,
2151                color,
2152                bg_color,
2153                underline,
2154                bold,
2155                italic,
2156                extend_to_line_end,
2157            } => {
2158                assert_eq!(buffer_id.0, 1);
2159                assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
2160                assert_eq!(range, 0..10);
2161                assert_eq!(color, (255, 0, 0));
2162                assert_eq!(bg_color, None);
2163                assert!(underline);
2164                assert!(!bold);
2165                assert!(!italic);
2166                assert!(!extend_to_line_end);
2167            }
2168            _ => panic!("Wrong command type"),
2169        }
2170    }
2171
2172    #[test]
2173    fn test_set_status_command() {
2174        let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2175        let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2176        let (tx, rx) = std::sync::mpsc::channel();
2177        let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2178
2179        let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2180
2181        let result = api.set_status("Test status".to_string());
2182        assert!(result.is_ok());
2183
2184        let received = rx.try_recv().unwrap();
2185        match received {
2186            PluginCommand::SetStatus { message } => {
2187                assert_eq!(message, "Test status");
2188            }
2189            _ => panic!("Wrong command type"),
2190        }
2191    }
2192
2193    #[test]
2194    fn test_get_active_buffer_id() {
2195        let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2196        let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2197        let (tx, _rx) = std::sync::mpsc::channel();
2198        let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2199
2200        // Set active buffer to 5
2201        {
2202            let mut snapshot = state_snapshot.write().unwrap();
2203            snapshot.active_buffer_id = BufferId(5);
2204        }
2205
2206        let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2207
2208        let active_id = api.get_active_buffer_id();
2209        assert_eq!(active_id.0, 5);
2210    }
2211
2212    #[test]
2213    fn test_get_buffer_info() {
2214        let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2215        let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2216        let (tx, _rx) = std::sync::mpsc::channel();
2217        let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2218
2219        // Add buffer info
2220        {
2221            let mut snapshot = state_snapshot.write().unwrap();
2222            let buffer_info = BufferInfo {
2223                id: BufferId(1),
2224                path: Some(std::path::PathBuf::from("/test/file.txt")),
2225                modified: true,
2226                length: 100,
2227            };
2228            snapshot.buffers.insert(BufferId(1), buffer_info);
2229        }
2230
2231        let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2232
2233        let info = api.get_buffer_info(BufferId(1));
2234        assert!(info.is_some());
2235        let info = info.unwrap();
2236        assert_eq!(info.id.0, 1);
2237        assert_eq!(
2238            info.path.as_ref().unwrap().to_str().unwrap(),
2239            "/test/file.txt"
2240        );
2241        assert!(info.modified);
2242        assert_eq!(info.length, 100);
2243
2244        // Non-existent buffer
2245        let no_info = api.get_buffer_info(BufferId(999));
2246        assert!(no_info.is_none());
2247    }
2248
2249    #[test]
2250    fn test_list_buffers() {
2251        let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2252        let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2253        let (tx, _rx) = std::sync::mpsc::channel();
2254        let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2255
2256        // Add multiple buffers
2257        {
2258            let mut snapshot = state_snapshot.write().unwrap();
2259            snapshot.buffers.insert(
2260                BufferId(1),
2261                BufferInfo {
2262                    id: BufferId(1),
2263                    path: Some(std::path::PathBuf::from("/file1.txt")),
2264                    modified: false,
2265                    length: 50,
2266                },
2267            );
2268            snapshot.buffers.insert(
2269                BufferId(2),
2270                BufferInfo {
2271                    id: BufferId(2),
2272                    path: Some(std::path::PathBuf::from("/file2.txt")),
2273                    modified: true,
2274                    length: 100,
2275                },
2276            );
2277            snapshot.buffers.insert(
2278                BufferId(3),
2279                BufferInfo {
2280                    id: BufferId(3),
2281                    path: None,
2282                    modified: false,
2283                    length: 0,
2284                },
2285            );
2286        }
2287
2288        let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2289
2290        let buffers = api.list_buffers();
2291        assert_eq!(buffers.len(), 3);
2292
2293        // Verify all buffers are present
2294        assert!(buffers.iter().any(|b| b.id.0 == 1));
2295        assert!(buffers.iter().any(|b| b.id.0 == 2));
2296        assert!(buffers.iter().any(|b| b.id.0 == 3));
2297    }
2298
2299    #[test]
2300    fn test_get_primary_cursor() {
2301        let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2302        let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2303        let (tx, _rx) = std::sync::mpsc::channel();
2304        let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2305
2306        // Add cursor info
2307        {
2308            let mut snapshot = state_snapshot.write().unwrap();
2309            snapshot.primary_cursor = Some(CursorInfo {
2310                position: 42,
2311                selection: Some(10..42),
2312            });
2313        }
2314
2315        let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2316
2317        let cursor = api.get_primary_cursor();
2318        assert!(cursor.is_some());
2319        let cursor = cursor.unwrap();
2320        assert_eq!(cursor.position, 42);
2321        assert_eq!(cursor.selection, Some(10..42));
2322    }
2323
2324    #[test]
2325    fn test_get_all_cursors() {
2326        let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2327        let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2328        let (tx, _rx) = std::sync::mpsc::channel();
2329        let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2330
2331        // Add multiple cursors
2332        {
2333            let mut snapshot = state_snapshot.write().unwrap();
2334            snapshot.all_cursors = vec![
2335                CursorInfo {
2336                    position: 10,
2337                    selection: None,
2338                },
2339                CursorInfo {
2340                    position: 20,
2341                    selection: Some(15..20),
2342                },
2343                CursorInfo {
2344                    position: 30,
2345                    selection: Some(25..30),
2346                },
2347            ];
2348        }
2349
2350        let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2351
2352        let cursors = api.get_all_cursors();
2353        assert_eq!(cursors.len(), 3);
2354        assert_eq!(cursors[0].position, 10);
2355        assert_eq!(cursors[0].selection, None);
2356        assert_eq!(cursors[1].position, 20);
2357        assert_eq!(cursors[1].selection, Some(15..20));
2358        assert_eq!(cursors[2].position, 30);
2359        assert_eq!(cursors[2].selection, Some(25..30));
2360    }
2361
2362    #[test]
2363    fn test_get_viewport() {
2364        let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2365        let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2366        let (tx, _rx) = std::sync::mpsc::channel();
2367        let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2368
2369        // Add viewport info
2370        {
2371            let mut snapshot = state_snapshot.write().unwrap();
2372            snapshot.viewport = Some(ViewportInfo {
2373                top_byte: 100,
2374                left_column: 5,
2375                width: 80,
2376                height: 24,
2377            });
2378        }
2379
2380        let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2381
2382        let viewport = api.get_viewport();
2383        assert!(viewport.is_some());
2384        let viewport = viewport.unwrap();
2385        assert_eq!(viewport.top_byte, 100);
2386        assert_eq!(viewport.left_column, 5);
2387        assert_eq!(viewport.width, 80);
2388        assert_eq!(viewport.height, 24);
2389    }
2390
2391    #[test]
2392    fn test_composite_buffer_options_rejects_unknown_fields() {
2393        // Valid JSON with correct field names
2394        let valid_json = r#"{
2395            "name": "test",
2396            "mode": "diff",
2397            "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2398            "sources": [{"bufferId": 1, "label": "old"}]
2399        }"#;
2400        let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
2401        assert!(
2402            result.is_ok(),
2403            "Valid JSON should parse: {:?}",
2404            result.err()
2405        );
2406
2407        // Invalid JSON with unknown field (buffer_id instead of bufferId)
2408        let invalid_json = r#"{
2409            "name": "test",
2410            "mode": "diff",
2411            "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2412            "sources": [{"buffer_id": 1, "label": "old"}]
2413        }"#;
2414        let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
2415        assert!(
2416            result.is_err(),
2417            "JSON with unknown field should fail to parse"
2418        );
2419        let err = result.unwrap_err().to_string();
2420        assert!(
2421            err.contains("unknown field") || err.contains("buffer_id"),
2422            "Error should mention unknown field: {}",
2423            err
2424        );
2425    }
2426
2427    #[test]
2428    fn test_composite_hunk_rejects_unknown_fields() {
2429        // Valid JSON with correct field names
2430        let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
2431        let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
2432        assert!(
2433            result.is_ok(),
2434            "Valid JSON should parse: {:?}",
2435            result.err()
2436        );
2437
2438        // Invalid JSON with unknown field (old_start instead of oldStart)
2439        let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
2440        let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
2441        assert!(
2442            result.is_err(),
2443            "JSON with unknown field should fail to parse"
2444        );
2445        let err = result.unwrap_err().to_string();
2446        assert!(
2447            err.contains("unknown field") || err.contains("old_start"),
2448            "Error should mention unknown field: {}",
2449            err
2450        );
2451    }
2452}