1use 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 crate::TerminalId;
55use lsp_types;
56use serde::{Deserialize, Serialize};
57use serde_json::Value as JsonValue;
58use std::collections::HashMap;
59use std::ops::Range;
60use std::path::PathBuf;
61use std::sync::{Arc, RwLock};
62use ts_rs::TS;
63
64pub struct CommandRegistry {
68 commands: std::sync::RwLock<Vec<Command>>,
69}
70
71impl CommandRegistry {
72 pub fn new() -> Self {
74 Self {
75 commands: std::sync::RwLock::new(Vec::new()),
76 }
77 }
78
79 pub fn register(&self, command: Command) {
81 let mut commands = self.commands.write().unwrap();
82 commands.retain(|c| c.name != command.name);
83 commands.push(command);
84 }
85
86 pub fn unregister(&self, name: &str) {
88 let mut commands = self.commands.write().unwrap();
89 commands.retain(|c| c.name != name);
90 }
91}
92
93impl Default for CommandRegistry {
94 fn default() -> Self {
95 Self::new()
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
105#[ts(export)]
106pub struct JsCallbackId(pub u64);
107
108impl JsCallbackId {
109 pub fn new(id: u64) -> Self {
111 Self(id)
112 }
113
114 pub fn as_u64(self) -> u64 {
116 self.0
117 }
118}
119
120impl From<u64> for JsCallbackId {
121 fn from(id: u64) -> Self {
122 Self(id)
123 }
124}
125
126impl From<JsCallbackId> for u64 {
127 fn from(id: JsCallbackId) -> u64 {
128 id.0
129 }
130}
131
132impl std::fmt::Display for JsCallbackId {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 write!(f, "{}", self.0)
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize, TS)]
140#[serde(rename_all = "camelCase")]
141#[ts(export, rename_all = "camelCase")]
142pub struct TerminalResult {
143 #[ts(type = "number")]
145 pub buffer_id: u64,
146 #[ts(type = "number")]
148 pub terminal_id: u64,
149 #[ts(type = "number | null")]
151 pub split_id: Option<u64>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, TS)]
156#[serde(rename_all = "camelCase")]
157#[ts(export, rename_all = "camelCase")]
158pub struct VirtualBufferResult {
159 #[ts(type = "number")]
161 pub buffer_id: u64,
162 #[ts(type = "number | null")]
164 pub split_id: Option<u64>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, TS)]
169#[ts(export)]
170pub enum PluginResponse {
171 VirtualBufferCreated {
173 request_id: u64,
174 buffer_id: BufferId,
175 split_id: Option<SplitId>,
176 },
177 TerminalCreated {
179 request_id: u64,
180 buffer_id: BufferId,
181 terminal_id: TerminalId,
182 split_id: Option<SplitId>,
183 },
184 LspRequest {
186 request_id: u64,
187 #[ts(type = "any")]
188 result: Result<JsonValue, String>,
189 },
190 HighlightsComputed {
192 request_id: u64,
193 spans: Vec<TsHighlightSpan>,
194 },
195 BufferText {
197 request_id: u64,
198 text: Result<String, String>,
199 },
200 LineStartPosition {
202 request_id: u64,
203 position: Option<usize>,
205 },
206 LineEndPosition {
208 request_id: u64,
209 position: Option<usize>,
211 },
212 BufferLineCount {
214 request_id: u64,
215 count: Option<usize>,
217 },
218 CompositeBufferCreated {
220 request_id: u64,
221 buffer_id: BufferId,
222 },
223 SplitByLabel {
225 request_id: u64,
226 split_id: Option<SplitId>,
227 },
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize, TS)]
232#[ts(export)]
233pub enum PluginAsyncMessage {
234 ProcessOutput {
236 process_id: u64,
238 stdout: String,
240 stderr: String,
242 exit_code: i32,
244 },
245 DelayComplete {
247 callback_id: u64,
249 },
250 ProcessStdout { process_id: u64, data: String },
252 ProcessStderr { process_id: u64, data: String },
254 ProcessExit {
256 process_id: u64,
257 callback_id: u64,
258 exit_code: i32,
259 },
260 LspResponse {
262 language: String,
263 request_id: u64,
264 #[ts(type = "any")]
265 result: Result<JsonValue, String>,
266 },
267 PluginResponse(crate::api::PluginResponse),
269
270 GrepStreamingProgress {
272 search_id: u64,
274 matches_json: String,
276 },
277
278 GrepStreamingComplete {
280 search_id: u64,
282 callback_id: u64,
284 total_matches: usize,
286 truncated: bool,
288 },
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, TS)]
293#[ts(export)]
294pub struct CursorInfo {
295 pub position: usize,
297 #[cfg_attr(
299 feature = "plugins",
300 ts(type = "{ start: number; end: number } | null")
301 )]
302 pub selection: Option<Range<usize>>,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize, TS)]
307#[serde(deny_unknown_fields)]
308#[ts(export)]
309pub struct ActionSpec {
310 pub action: String,
312 #[serde(default = "default_action_count")]
314 pub count: u32,
315}
316
317fn default_action_count() -> u32 {
318 1
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize, TS)]
323#[ts(export)]
324pub struct BufferInfo {
325 #[ts(type = "number")]
327 pub id: BufferId,
328 #[serde(serialize_with = "serialize_path")]
330 #[ts(type = "string")]
331 pub path: Option<PathBuf>,
332 pub modified: bool,
334 pub length: usize,
336 pub is_virtual: bool,
338 pub view_mode: String,
340 pub is_composing_in_any_split: bool,
345 pub compose_width: Option<u16>,
347 pub language: String,
349}
350
351fn serialize_path<S: serde::Serializer>(path: &Option<PathBuf>, s: S) -> Result<S::Ok, S::Error> {
352 s.serialize_str(
353 &path
354 .as_ref()
355 .map(|p| p.to_string_lossy().to_string())
356 .unwrap_or_default(),
357 )
358}
359
360fn serialize_ranges_as_tuples<S>(ranges: &[Range<usize>], serializer: S) -> Result<S::Ok, S::Error>
362where
363 S: serde::Serializer,
364{
365 use serde::ser::SerializeSeq;
366 let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
367 for range in ranges {
368 seq.serialize_element(&(range.start, range.end))?;
369 }
370 seq.end()
371}
372
373fn serialize_opt_ranges_as_tuples<S>(
375 ranges: &Option<Vec<Range<usize>>>,
376 serializer: S,
377) -> Result<S::Ok, S::Error>
378where
379 S: serde::Serializer,
380{
381 match ranges {
382 Some(ranges) => {
383 use serde::ser::SerializeSeq;
384 let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
385 for range in ranges {
386 seq.serialize_element(&(range.start, range.end))?;
387 }
388 seq.end()
389 }
390 None => serializer.serialize_none(),
391 }
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize, TS)]
396#[ts(export)]
397pub struct BufferSavedDiff {
398 pub equal: bool,
399 #[serde(serialize_with = "serialize_ranges_as_tuples")]
400 #[ts(type = "Array<[number, number]>")]
401 pub byte_ranges: Vec<Range<usize>>,
402 #[serde(serialize_with = "serialize_opt_ranges_as_tuples")]
403 #[ts(type = "Array<[number, number]> | null")]
404 pub line_ranges: Option<Vec<Range<usize>>>,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize, TS)]
409#[serde(rename_all = "camelCase")]
410#[ts(export, rename_all = "camelCase")]
411pub struct ViewportInfo {
412 pub top_byte: usize,
414 pub top_line: Option<usize>,
416 pub left_column: usize,
418 pub width: u16,
420 pub height: u16,
422}
423
424#[derive(Debug, Clone, Serialize, Deserialize, TS)]
426#[serde(rename_all = "camelCase")]
427#[ts(export, rename_all = "camelCase")]
428pub struct LayoutHints {
429 #[ts(optional)]
431 pub compose_width: Option<u16>,
432 #[ts(optional)]
434 pub column_guides: Option<Vec<u16>>,
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize, TS)]
452#[serde(untagged)]
453#[ts(export)]
454pub enum OverlayColorSpec {
455 #[ts(type = "[number, number, number]")]
457 Rgb(u8, u8, u8),
458 ThemeKey(String),
460}
461
462impl OverlayColorSpec {
463 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
465 Self::Rgb(r, g, b)
466 }
467
468 pub fn theme_key(key: impl Into<String>) -> Self {
470 Self::ThemeKey(key.into())
471 }
472
473 pub fn as_rgb(&self) -> Option<(u8, u8, u8)> {
475 match self {
476 Self::Rgb(r, g, b) => Some((*r, *g, *b)),
477 Self::ThemeKey(_) => None,
478 }
479 }
480
481 pub fn as_theme_key(&self) -> Option<&str> {
483 match self {
484 Self::ThemeKey(key) => Some(key),
485 Self::Rgb(_, _, _) => None,
486 }
487 }
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize, TS)]
495#[serde(deny_unknown_fields, rename_all = "camelCase")]
496#[ts(export, rename_all = "camelCase")]
497#[derive(Default)]
498pub struct OverlayOptions {
499 #[serde(default, skip_serializing_if = "Option::is_none")]
501 pub fg: Option<OverlayColorSpec>,
502
503 #[serde(default, skip_serializing_if = "Option::is_none")]
505 pub bg: Option<OverlayColorSpec>,
506
507 #[serde(default)]
509 pub underline: bool,
510
511 #[serde(default)]
513 pub bold: bool,
514
515 #[serde(default)]
517 pub italic: bool,
518
519 #[serde(default)]
521 pub strikethrough: bool,
522
523 #[serde(default)]
525 pub extend_to_line_end: bool,
526
527 #[serde(default, skip_serializing_if = "Option::is_none")]
531 pub url: Option<String>,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize, TS)]
540#[serde(deny_unknown_fields)]
541#[ts(export, rename = "TsCompositeLayoutConfig")]
542pub struct CompositeLayoutConfig {
543 #[serde(rename = "type")]
545 #[ts(rename = "type")]
546 pub layout_type: String,
547 #[serde(default)]
549 #[ts(optional)]
550 pub ratios: Option<Vec<f32>>,
551 #[serde(default = "default_true", rename = "showSeparator")]
553 #[ts(rename = "showSeparator")]
554 pub show_separator: bool,
555 #[serde(default)]
557 #[ts(optional)]
558 pub spacing: Option<u16>,
559}
560
561fn default_true() -> bool {
562 true
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize, TS)]
567#[serde(deny_unknown_fields)]
568#[ts(export, rename = "TsCompositeSourceConfig")]
569pub struct CompositeSourceConfig {
570 #[serde(rename = "bufferId")]
572 #[ts(rename = "bufferId")]
573 pub buffer_id: usize,
574 pub label: String,
576 #[serde(default)]
578 pub editable: bool,
579 #[serde(default)]
581 pub style: Option<CompositePaneStyle>,
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
586#[serde(deny_unknown_fields)]
587#[ts(export, rename = "TsCompositePaneStyle")]
588pub struct CompositePaneStyle {
589 #[serde(default, rename = "addBg")]
592 #[ts(optional, rename = "addBg", type = "[number, number, number]")]
593 pub add_bg: Option<[u8; 3]>,
594 #[serde(default, rename = "removeBg")]
596 #[ts(optional, rename = "removeBg", type = "[number, number, number]")]
597 pub remove_bg: Option<[u8; 3]>,
598 #[serde(default, rename = "modifyBg")]
600 #[ts(optional, rename = "modifyBg", type = "[number, number, number]")]
601 pub modify_bg: Option<[u8; 3]>,
602 #[serde(default, rename = "gutterStyle")]
604 #[ts(optional, rename = "gutterStyle")]
605 pub gutter_style: Option<String>,
606}
607
608#[derive(Debug, Clone, Serialize, Deserialize, TS)]
610#[serde(deny_unknown_fields)]
611#[ts(export, rename = "TsCompositeHunk")]
612pub struct CompositeHunk {
613 #[serde(rename = "oldStart")]
615 #[ts(rename = "oldStart")]
616 pub old_start: usize,
617 #[serde(rename = "oldCount")]
619 #[ts(rename = "oldCount")]
620 pub old_count: usize,
621 #[serde(rename = "newStart")]
623 #[ts(rename = "newStart")]
624 pub new_start: usize,
625 #[serde(rename = "newCount")]
627 #[ts(rename = "newCount")]
628 pub new_count: usize,
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize, TS)]
633#[serde(deny_unknown_fields)]
634#[ts(export, rename = "TsCreateCompositeBufferOptions")]
635pub struct CreateCompositeBufferOptions {
636 #[serde(default)]
638 pub name: String,
639 #[serde(default)]
641 pub mode: String,
642 pub layout: CompositeLayoutConfig,
644 pub sources: Vec<CompositeSourceConfig>,
646 #[serde(default)]
648 pub hunks: Option<Vec<CompositeHunk>>,
649}
650
651#[derive(Debug, Clone, Serialize, Deserialize, TS)]
653#[ts(export)]
654pub enum ViewTokenWireKind {
655 Text(String),
656 Newline,
657 Space,
658 Break,
661 BinaryByte(u8),
665}
666
667#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
673#[serde(deny_unknown_fields)]
674#[ts(export)]
675pub struct ViewTokenStyle {
676 #[serde(default)]
678 #[ts(type = "[number, number, number] | null")]
679 pub fg: Option<(u8, u8, u8)>,
680 #[serde(default)]
682 #[ts(type = "[number, number, number] | null")]
683 pub bg: Option<(u8, u8, u8)>,
684 #[serde(default)]
686 pub bold: bool,
687 #[serde(default)]
689 pub italic: bool,
690}
691
692#[derive(Debug, Clone, Serialize, Deserialize, TS)]
694#[serde(deny_unknown_fields)]
695#[ts(export)]
696pub struct ViewTokenWire {
697 #[ts(type = "number | null")]
699 pub source_offset: Option<usize>,
700 pub kind: ViewTokenWireKind,
702 #[serde(default)]
704 #[ts(optional)]
705 pub style: Option<ViewTokenStyle>,
706}
707
708#[derive(Debug, Clone, Serialize, Deserialize, TS)]
710#[ts(export)]
711pub struct ViewTransformPayload {
712 pub range: Range<usize>,
714 pub tokens: Vec<ViewTokenWire>,
716 pub layout_hints: Option<LayoutHints>,
718}
719
720#[derive(Debug, Clone, Serialize, Deserialize, TS)]
723#[ts(export)]
724pub struct EditorStateSnapshot {
725 pub active_buffer_id: BufferId,
727 pub active_split_id: usize,
729 pub buffers: HashMap<BufferId, BufferInfo>,
731 pub buffer_saved_diffs: HashMap<BufferId, BufferSavedDiff>,
733 pub primary_cursor: Option<CursorInfo>,
735 pub all_cursors: Vec<CursorInfo>,
737 pub viewport: Option<ViewportInfo>,
739 pub buffer_cursor_positions: HashMap<BufferId, usize>,
741 pub buffer_text_properties: HashMap<BufferId, Vec<TextProperty>>,
743 pub selected_text: Option<String>,
746 pub clipboard: String,
748 pub working_dir: PathBuf,
750 #[ts(type = "any")]
753 pub diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
754 #[ts(type = "any")]
757 pub folding_ranges: HashMap<String, Vec<lsp_types::FoldingRange>>,
758 #[ts(type = "any")]
761 pub config: serde_json::Value,
762 #[ts(type = "any")]
765 pub user_config: serde_json::Value,
766 pub editor_mode: Option<String>,
769
770 #[ts(type = "any")]
774 pub plugin_view_states: HashMap<BufferId, HashMap<String, serde_json::Value>>,
775
776 #[serde(skip)]
779 #[ts(skip)]
780 pub plugin_view_states_split: usize,
781
782 #[serde(skip)]
785 #[ts(skip)]
786 pub keybinding_labels: HashMap<String, String>,
787
788 #[ts(type = "any")]
795 pub plugin_global_states: HashMap<String, HashMap<String, serde_json::Value>>,
796}
797
798impl EditorStateSnapshot {
799 pub fn new() -> Self {
800 Self {
801 active_buffer_id: BufferId(0),
802 active_split_id: 0,
803 buffers: HashMap::new(),
804 buffer_saved_diffs: HashMap::new(),
805 primary_cursor: None,
806 all_cursors: Vec::new(),
807 viewport: None,
808 buffer_cursor_positions: HashMap::new(),
809 buffer_text_properties: HashMap::new(),
810 selected_text: None,
811 clipboard: String::new(),
812 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
813 diagnostics: HashMap::new(),
814 folding_ranges: HashMap::new(),
815 config: serde_json::Value::Null,
816 user_config: serde_json::Value::Null,
817 editor_mode: None,
818 plugin_view_states: HashMap::new(),
819 plugin_view_states_split: 0,
820 keybinding_labels: HashMap::new(),
821 plugin_global_states: HashMap::new(),
822 }
823 }
824}
825
826impl Default for EditorStateSnapshot {
827 fn default() -> Self {
828 Self::new()
829 }
830}
831
832#[derive(Debug, Clone, Serialize, Deserialize, TS)]
834#[ts(export)]
835pub enum MenuPosition {
836 Top,
838 Bottom,
840 Before(String),
842 After(String),
844}
845
846#[derive(Debug, Clone, Serialize, Deserialize, TS)]
848#[ts(export)]
849pub enum PluginCommand {
850 InsertText {
852 buffer_id: BufferId,
853 position: usize,
854 text: String,
855 },
856
857 DeleteRange {
859 buffer_id: BufferId,
860 range: Range<usize>,
861 },
862
863 AddOverlay {
868 buffer_id: BufferId,
869 namespace: Option<OverlayNamespace>,
870 range: Range<usize>,
871 options: OverlayOptions,
873 },
874
875 RemoveOverlay {
877 buffer_id: BufferId,
878 handle: OverlayHandle,
879 },
880
881 SetStatus { message: String },
883
884 ApplyTheme { theme_name: String },
886
887 ReloadConfig,
890
891 RegisterCommand { command: Command },
893
894 UnregisterCommand { name: String },
896
897 OpenFileInBackground { path: PathBuf },
899
900 InsertAtCursor { text: String },
902
903 SpawnProcess {
905 command: String,
906 args: Vec<String>,
907 cwd: Option<String>,
908 callback_id: JsCallbackId,
909 },
910
911 Delay {
913 callback_id: JsCallbackId,
914 duration_ms: u64,
915 },
916
917 SpawnBackgroundProcess {
921 process_id: u64,
923 command: String,
925 args: Vec<String>,
927 cwd: Option<String>,
929 callback_id: JsCallbackId,
931 },
932
933 KillBackgroundProcess { process_id: u64 },
935
936 SpawnProcessWait {
939 process_id: u64,
941 callback_id: JsCallbackId,
943 },
944
945 SetLayoutHints {
947 buffer_id: BufferId,
948 split_id: Option<SplitId>,
949 range: Range<usize>,
950 hints: LayoutHints,
951 },
952
953 SetLineNumbers { buffer_id: BufferId, enabled: bool },
955
956 SetViewMode { buffer_id: BufferId, mode: String },
958
959 SetLineWrap {
961 buffer_id: BufferId,
962 split_id: Option<SplitId>,
963 enabled: bool,
964 },
965
966 SubmitViewTransform {
968 buffer_id: BufferId,
969 split_id: Option<SplitId>,
970 payload: ViewTransformPayload,
971 },
972
973 ClearViewTransform {
975 buffer_id: BufferId,
976 split_id: Option<SplitId>,
977 },
978
979 SetViewState {
982 buffer_id: BufferId,
983 key: String,
984 #[ts(type = "any")]
985 value: Option<serde_json::Value>,
986 },
987
988 SetGlobalState {
992 plugin_name: String,
993 key: String,
994 #[ts(type = "any")]
995 value: Option<serde_json::Value>,
996 },
997
998 ClearAllOverlays { buffer_id: BufferId },
1000
1001 ClearNamespace {
1003 buffer_id: BufferId,
1004 namespace: OverlayNamespace,
1005 },
1006
1007 ClearOverlaysInRange {
1010 buffer_id: BufferId,
1011 start: usize,
1012 end: usize,
1013 },
1014
1015 AddVirtualText {
1018 buffer_id: BufferId,
1019 virtual_text_id: String,
1020 position: usize,
1021 text: String,
1022 color: (u8, u8, u8),
1023 use_bg: bool, before: bool, },
1026
1027 RemoveVirtualText {
1029 buffer_id: BufferId,
1030 virtual_text_id: String,
1031 },
1032
1033 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
1035
1036 ClearVirtualTexts { buffer_id: BufferId },
1038
1039 AddVirtualLine {
1043 buffer_id: BufferId,
1044 position: usize,
1046 text: String,
1048 fg_color: (u8, u8, u8),
1050 bg_color: Option<(u8, u8, u8)>,
1052 above: bool,
1054 namespace: String,
1056 priority: i32,
1058 },
1059
1060 ClearVirtualTextNamespace {
1063 buffer_id: BufferId,
1064 namespace: String,
1065 },
1066
1067 AddConceal {
1070 buffer_id: BufferId,
1071 namespace: OverlayNamespace,
1073 start: usize,
1075 end: usize,
1076 replacement: Option<String>,
1078 },
1079
1080 ClearConcealNamespace {
1082 buffer_id: BufferId,
1083 namespace: OverlayNamespace,
1084 },
1085
1086 ClearConcealsInRange {
1089 buffer_id: BufferId,
1090 start: usize,
1091 end: usize,
1092 },
1093
1094 AddSoftBreak {
1098 buffer_id: BufferId,
1099 namespace: OverlayNamespace,
1101 position: usize,
1103 indent: u16,
1105 },
1106
1107 ClearSoftBreakNamespace {
1109 buffer_id: BufferId,
1110 namespace: OverlayNamespace,
1111 },
1112
1113 ClearSoftBreaksInRange {
1115 buffer_id: BufferId,
1116 start: usize,
1117 end: usize,
1118 },
1119
1120 RefreshLines { buffer_id: BufferId },
1122
1123 RefreshAllLines,
1127
1128 HookCompleted { hook_name: String },
1132
1133 SetLineIndicator {
1136 buffer_id: BufferId,
1137 line: usize,
1139 namespace: String,
1141 symbol: String,
1143 color: (u8, u8, u8),
1145 priority: i32,
1147 },
1148
1149 SetLineIndicators {
1152 buffer_id: BufferId,
1153 lines: Vec<usize>,
1155 namespace: String,
1157 symbol: String,
1159 color: (u8, u8, u8),
1161 priority: i32,
1163 },
1164
1165 ClearLineIndicators {
1167 buffer_id: BufferId,
1168 namespace: String,
1170 },
1171
1172 SetFileExplorerDecorations {
1174 namespace: String,
1176 decorations: Vec<FileExplorerDecoration>,
1178 },
1179
1180 ClearFileExplorerDecorations {
1182 namespace: String,
1184 },
1185
1186 OpenFileAtLocation {
1189 path: PathBuf,
1190 line: Option<usize>, column: Option<usize>, },
1193
1194 OpenFileInSplit {
1197 split_id: usize,
1198 path: PathBuf,
1199 line: Option<usize>, column: Option<usize>, },
1202
1203 StartPrompt {
1206 label: String,
1207 prompt_type: String, },
1209
1210 StartPromptWithInitial {
1212 label: String,
1213 prompt_type: String,
1214 initial_value: String,
1215 },
1216
1217 StartPromptAsync {
1220 label: String,
1221 initial_value: String,
1222 callback_id: JsCallbackId,
1223 },
1224
1225 SetPromptSuggestions { suggestions: Vec<Suggestion> },
1228
1229 SetPromptInputSync { sync: bool },
1231
1232 AddMenuItem {
1235 menu_label: String,
1236 item: MenuItem,
1237 position: MenuPosition,
1238 },
1239
1240 AddMenu { menu: Menu, position: MenuPosition },
1242
1243 RemoveMenuItem {
1245 menu_label: String,
1246 item_label: String,
1247 },
1248
1249 RemoveMenu { menu_label: String },
1251
1252 CreateVirtualBuffer {
1254 name: String,
1256 mode: String,
1258 read_only: bool,
1260 },
1261
1262 CreateVirtualBufferWithContent {
1266 name: String,
1268 mode: String,
1270 read_only: bool,
1272 entries: Vec<TextPropertyEntry>,
1274 show_line_numbers: bool,
1276 show_cursors: bool,
1278 editing_disabled: bool,
1280 hidden_from_tabs: bool,
1282 request_id: Option<u64>,
1284 },
1285
1286 CreateVirtualBufferInSplit {
1289 name: String,
1291 mode: String,
1293 read_only: bool,
1295 entries: Vec<TextPropertyEntry>,
1297 ratio: f32,
1299 direction: Option<String>,
1301 panel_id: Option<String>,
1303 show_line_numbers: bool,
1305 show_cursors: bool,
1307 editing_disabled: bool,
1309 line_wrap: Option<bool>,
1311 before: bool,
1313 request_id: Option<u64>,
1315 },
1316
1317 SetVirtualBufferContent {
1319 buffer_id: BufferId,
1320 entries: Vec<TextPropertyEntry>,
1322 },
1323
1324 GetTextPropertiesAtCursor { buffer_id: BufferId },
1326
1327 DefineMode {
1329 name: String,
1330 bindings: Vec<(String, String)>, read_only: bool,
1332 allow_text_input: bool,
1334 plugin_name: Option<String>,
1336 },
1337
1338 ShowBuffer { buffer_id: BufferId },
1340
1341 CreateVirtualBufferInExistingSplit {
1343 name: String,
1345 mode: String,
1347 read_only: bool,
1349 entries: Vec<TextPropertyEntry>,
1351 split_id: SplitId,
1353 show_line_numbers: bool,
1355 show_cursors: bool,
1357 editing_disabled: bool,
1359 line_wrap: Option<bool>,
1361 request_id: Option<u64>,
1363 },
1364
1365 CloseBuffer { buffer_id: BufferId },
1367
1368 CreateCompositeBuffer {
1371 name: String,
1373 mode: String,
1375 layout: CompositeLayoutConfig,
1377 sources: Vec<CompositeSourceConfig>,
1379 hunks: Option<Vec<CompositeHunk>>,
1381 request_id: Option<u64>,
1383 },
1384
1385 UpdateCompositeAlignment {
1387 buffer_id: BufferId,
1388 hunks: Vec<CompositeHunk>,
1389 },
1390
1391 CloseCompositeBuffer { buffer_id: BufferId },
1393
1394 FocusSplit { split_id: SplitId },
1396
1397 SetSplitBuffer {
1399 split_id: SplitId,
1400 buffer_id: BufferId,
1401 },
1402
1403 SetSplitScroll { split_id: SplitId, top_byte: usize },
1405
1406 RequestHighlights {
1408 buffer_id: BufferId,
1409 range: Range<usize>,
1410 request_id: u64,
1411 },
1412
1413 CloseSplit { split_id: SplitId },
1415
1416 SetSplitRatio {
1418 split_id: SplitId,
1419 ratio: f32,
1421 },
1422
1423 SetSplitLabel { split_id: SplitId, label: String },
1425
1426 ClearSplitLabel { split_id: SplitId },
1428
1429 GetSplitByLabel { label: String, request_id: u64 },
1431
1432 DistributeSplitsEvenly {
1434 split_ids: Vec<SplitId>,
1436 },
1437
1438 SetBufferCursor {
1440 buffer_id: BufferId,
1441 position: usize,
1443 },
1444
1445 SendLspRequest {
1447 language: String,
1448 method: String,
1449 #[ts(type = "any")]
1450 params: Option<JsonValue>,
1451 request_id: u64,
1452 },
1453
1454 SetClipboard { text: String },
1456
1457 DeleteSelection,
1460
1461 SetContext {
1465 name: String,
1467 active: bool,
1469 },
1470
1471 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1473
1474 ExecuteAction {
1477 action_name: String,
1479 },
1480
1481 ExecuteActions {
1485 actions: Vec<ActionSpec>,
1487 },
1488
1489 GetBufferText {
1491 buffer_id: BufferId,
1493 start: usize,
1495 end: usize,
1497 request_id: u64,
1499 },
1500
1501 GetLineStartPosition {
1504 buffer_id: BufferId,
1506 line: u32,
1508 request_id: u64,
1510 },
1511
1512 GetLineEndPosition {
1516 buffer_id: BufferId,
1518 line: u32,
1520 request_id: u64,
1522 },
1523
1524 GetBufferLineCount {
1526 buffer_id: BufferId,
1528 request_id: u64,
1530 },
1531
1532 ScrollToLineCenter {
1535 split_id: SplitId,
1537 buffer_id: BufferId,
1539 line: usize,
1541 },
1542
1543 SetEditorMode {
1546 mode: Option<String>,
1548 },
1549
1550 ShowActionPopup {
1553 popup_id: String,
1555 title: String,
1557 message: String,
1559 actions: Vec<ActionPopupAction>,
1561 },
1562
1563 DisableLspForLanguage {
1565 language: String,
1567 },
1568
1569 RestartLspForLanguage {
1571 language: String,
1573 },
1574
1575 SetLspRootUri {
1579 language: String,
1581 uri: String,
1583 },
1584
1585 CreateScrollSyncGroup {
1589 group_id: u32,
1591 left_split: SplitId,
1593 right_split: SplitId,
1595 },
1596
1597 SetScrollSyncAnchors {
1600 group_id: u32,
1602 anchors: Vec<(usize, usize)>,
1604 },
1605
1606 RemoveScrollSyncGroup {
1608 group_id: u32,
1610 },
1611
1612 SaveBufferToPath {
1615 buffer_id: BufferId,
1617 path: PathBuf,
1619 },
1620
1621 LoadPlugin {
1624 path: PathBuf,
1626 callback_id: JsCallbackId,
1628 },
1629
1630 UnloadPlugin {
1633 name: String,
1635 callback_id: JsCallbackId,
1637 },
1638
1639 ReloadPlugin {
1642 name: String,
1644 callback_id: JsCallbackId,
1646 },
1647
1648 ListPlugins {
1651 callback_id: JsCallbackId,
1653 },
1654
1655 ReloadThemes { apply_theme: Option<String> },
1659
1660 RegisterGrammar {
1663 language: String,
1665 grammar_path: String,
1667 extensions: Vec<String>,
1669 },
1670
1671 RegisterLanguageConfig {
1674 language: String,
1676 config: LanguagePackConfig,
1678 },
1679
1680 RegisterLspServer {
1683 language: String,
1685 config: LspServerPackConfig,
1687 },
1688
1689 ReloadGrammars { callback_id: JsCallbackId },
1693
1694 CreateTerminal {
1698 cwd: Option<String>,
1700 direction: Option<String>,
1702 ratio: Option<f32>,
1704 focus: Option<bool>,
1706 request_id: u64,
1708 },
1709
1710 SendTerminalInput {
1712 terminal_id: TerminalId,
1714 data: String,
1716 },
1717
1718 CloseTerminal {
1720 terminal_id: TerminalId,
1722 },
1723
1724 GrepProject {
1728 pattern: String,
1730 fixed_string: bool,
1732 case_sensitive: bool,
1734 max_results: usize,
1736 whole_words: bool,
1738 callback_id: JsCallbackId,
1740 },
1741
1742 GrepProjectStreaming {
1747 pattern: String,
1749 fixed_string: bool,
1751 case_sensitive: bool,
1753 max_results: usize,
1755 whole_words: bool,
1757 search_id: u64,
1759 callback_id: JsCallbackId,
1761 },
1762
1763 ReplaceInBuffer {
1767 file_path: PathBuf,
1769 matches: Vec<(usize, usize)>,
1771 replacement: String,
1773 callback_id: JsCallbackId,
1775 },
1776}
1777
1778impl PluginCommand {
1779 pub fn debug_variant_name(&self) -> String {
1781 let dbg = format!("{:?}", self);
1782 dbg.split([' ', '{', '(']).next().unwrap_or("?").to_string()
1783 }
1784}
1785
1786#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
1795#[serde(rename_all = "camelCase")]
1796#[ts(export)]
1797pub struct LanguagePackConfig {
1798 #[serde(default)]
1800 pub comment_prefix: Option<String>,
1801
1802 #[serde(default)]
1804 pub block_comment_start: Option<String>,
1805
1806 #[serde(default)]
1808 pub block_comment_end: Option<String>,
1809
1810 #[serde(default)]
1812 pub use_tabs: Option<bool>,
1813
1814 #[serde(default)]
1816 pub tab_size: Option<usize>,
1817
1818 #[serde(default)]
1820 pub auto_indent: Option<bool>,
1821
1822 #[serde(default)]
1825 pub show_whitespace_tabs: Option<bool>,
1826
1827 #[serde(default)]
1829 pub formatter: Option<FormatterPackConfig>,
1830}
1831
1832#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1834#[serde(rename_all = "camelCase")]
1835#[ts(export)]
1836pub struct FormatterPackConfig {
1837 pub command: String,
1839
1840 #[serde(default)]
1842 pub args: Vec<String>,
1843}
1844
1845#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1847#[serde(rename_all = "camelCase")]
1848#[ts(export)]
1849pub struct ProcessLimitsPackConfig {
1850 #[serde(default)]
1852 pub max_memory_percent: Option<u32>,
1853
1854 #[serde(default)]
1856 pub max_cpu_percent: Option<u32>,
1857
1858 #[serde(default)]
1860 pub enabled: Option<bool>,
1861}
1862
1863#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1865#[serde(rename_all = "camelCase")]
1866#[ts(export)]
1867pub struct LspServerPackConfig {
1868 pub command: String,
1870
1871 #[serde(default)]
1873 pub args: Vec<String>,
1874
1875 #[serde(default)]
1877 pub auto_start: Option<bool>,
1878
1879 #[serde(default)]
1881 #[ts(type = "Record<string, unknown> | null")]
1882 pub initialization_options: Option<JsonValue>,
1883
1884 #[serde(default)]
1886 pub process_limits: Option<ProcessLimitsPackConfig>,
1887}
1888
1889#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1891#[ts(export)]
1892pub enum HunkStatus {
1893 Pending,
1894 Staged,
1895 Discarded,
1896}
1897
1898#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1900#[ts(export)]
1901pub struct ReviewHunk {
1902 pub id: String,
1903 pub file: String,
1904 pub context_header: String,
1905 pub status: HunkStatus,
1906 pub base_range: Option<(usize, usize)>,
1908 pub modified_range: Option<(usize, usize)>,
1910}
1911
1912#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1914#[serde(deny_unknown_fields)]
1915#[ts(export, rename = "TsActionPopupAction")]
1916pub struct ActionPopupAction {
1917 pub id: String,
1919 pub label: String,
1921}
1922
1923#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1925#[serde(deny_unknown_fields)]
1926#[ts(export)]
1927pub struct ActionPopupOptions {
1928 pub id: String,
1930 pub title: String,
1932 pub message: String,
1934 pub actions: Vec<ActionPopupAction>,
1936}
1937
1938#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1940#[ts(export)]
1941pub struct TsHighlightSpan {
1942 pub start: u32,
1943 pub end: u32,
1944 #[ts(type = "[number, number, number]")]
1945 pub color: (u8, u8, u8),
1946 pub bold: bool,
1947 pub italic: bool,
1948}
1949
1950#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1952#[ts(export)]
1953pub struct SpawnResult {
1954 pub stdout: String,
1956 pub stderr: String,
1958 pub exit_code: i32,
1960}
1961
1962#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1964#[ts(export)]
1965pub struct BackgroundProcessResult {
1966 #[ts(type = "number")]
1968 pub process_id: u64,
1969 pub exit_code: i32,
1972}
1973
1974#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1976#[serde(rename_all = "camelCase")]
1977#[ts(export, rename_all = "camelCase")]
1978pub struct GrepMatch {
1979 pub file: String,
1981 #[ts(type = "number")]
1983 pub buffer_id: usize,
1984 #[ts(type = "number")]
1986 pub byte_offset: usize,
1987 #[ts(type = "number")]
1989 pub length: usize,
1990 #[ts(type = "number")]
1992 pub line: usize,
1993 #[ts(type = "number")]
1995 pub column: usize,
1996 pub context: String,
1998}
1999
2000#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2002#[serde(rename_all = "camelCase")]
2003#[ts(export, rename_all = "camelCase")]
2004pub struct ReplaceResult {
2005 #[ts(type = "number")]
2007 pub replacements: usize,
2008 #[ts(type = "number")]
2010 pub buffer_id: usize,
2011}
2012
2013#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2015#[serde(deny_unknown_fields, rename_all = "camelCase")]
2016#[ts(export, rename = "TextPropertyEntry", rename_all = "camelCase")]
2017pub struct JsTextPropertyEntry {
2018 pub text: String,
2020 #[serde(default)]
2022 #[ts(optional, type = "Record<string, unknown>")]
2023 pub properties: Option<HashMap<String, JsonValue>>,
2024 #[serde(default)]
2026 #[ts(optional, type = "Partial<OverlayOptions>")]
2027 pub style: Option<OverlayOptions>,
2028 #[serde(default)]
2030 #[ts(optional)]
2031 pub inline_overlays: Option<Vec<crate::text_property::InlineOverlay>>,
2032}
2033
2034#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2036#[ts(export)]
2037pub struct DirEntry {
2038 pub name: String,
2040 pub is_file: bool,
2042 pub is_dir: bool,
2044}
2045
2046#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2048#[ts(export)]
2049pub struct JsPosition {
2050 pub line: u32,
2052 pub character: u32,
2054}
2055
2056#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2058#[ts(export)]
2059pub struct JsRange {
2060 pub start: JsPosition,
2062 pub end: JsPosition,
2064}
2065
2066#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2068#[ts(export)]
2069pub struct JsDiagnostic {
2070 pub uri: String,
2072 pub message: String,
2074 pub severity: Option<u8>,
2076 pub range: JsRange,
2078 #[ts(optional)]
2080 pub source: Option<String>,
2081}
2082
2083#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2085#[serde(deny_unknown_fields)]
2086#[ts(export)]
2087pub struct CreateVirtualBufferOptions {
2088 pub name: String,
2090 #[serde(default)]
2092 #[ts(optional)]
2093 pub mode: Option<String>,
2094 #[serde(default, rename = "readOnly")]
2096 #[ts(optional, rename = "readOnly")]
2097 pub read_only: Option<bool>,
2098 #[serde(default, rename = "showLineNumbers")]
2100 #[ts(optional, rename = "showLineNumbers")]
2101 pub show_line_numbers: Option<bool>,
2102 #[serde(default, rename = "showCursors")]
2104 #[ts(optional, rename = "showCursors")]
2105 pub show_cursors: Option<bool>,
2106 #[serde(default, rename = "editingDisabled")]
2108 #[ts(optional, rename = "editingDisabled")]
2109 pub editing_disabled: Option<bool>,
2110 #[serde(default, rename = "hiddenFromTabs")]
2112 #[ts(optional, rename = "hiddenFromTabs")]
2113 pub hidden_from_tabs: Option<bool>,
2114 #[serde(default)]
2116 #[ts(optional)]
2117 pub entries: Option<Vec<JsTextPropertyEntry>>,
2118}
2119
2120#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2122#[serde(deny_unknown_fields)]
2123#[ts(export)]
2124pub struct CreateVirtualBufferInSplitOptions {
2125 pub name: String,
2127 #[serde(default)]
2129 #[ts(optional)]
2130 pub mode: Option<String>,
2131 #[serde(default, rename = "readOnly")]
2133 #[ts(optional, rename = "readOnly")]
2134 pub read_only: Option<bool>,
2135 #[serde(default)]
2137 #[ts(optional)]
2138 pub ratio: Option<f32>,
2139 #[serde(default)]
2141 #[ts(optional)]
2142 pub direction: Option<String>,
2143 #[serde(default, rename = "panelId")]
2145 #[ts(optional, rename = "panelId")]
2146 pub panel_id: Option<String>,
2147 #[serde(default, rename = "showLineNumbers")]
2149 #[ts(optional, rename = "showLineNumbers")]
2150 pub show_line_numbers: Option<bool>,
2151 #[serde(default, rename = "showCursors")]
2153 #[ts(optional, rename = "showCursors")]
2154 pub show_cursors: Option<bool>,
2155 #[serde(default, rename = "editingDisabled")]
2157 #[ts(optional, rename = "editingDisabled")]
2158 pub editing_disabled: Option<bool>,
2159 #[serde(default, rename = "lineWrap")]
2161 #[ts(optional, rename = "lineWrap")]
2162 pub line_wrap: Option<bool>,
2163 #[serde(default)]
2165 #[ts(optional)]
2166 pub before: Option<bool>,
2167 #[serde(default)]
2169 #[ts(optional)]
2170 pub entries: Option<Vec<JsTextPropertyEntry>>,
2171}
2172
2173#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2175#[serde(deny_unknown_fields)]
2176#[ts(export)]
2177pub struct CreateVirtualBufferInExistingSplitOptions {
2178 pub name: String,
2180 #[serde(rename = "splitId")]
2182 #[ts(rename = "splitId")]
2183 pub split_id: usize,
2184 #[serde(default)]
2186 #[ts(optional)]
2187 pub mode: Option<String>,
2188 #[serde(default, rename = "readOnly")]
2190 #[ts(optional, rename = "readOnly")]
2191 pub read_only: Option<bool>,
2192 #[serde(default, rename = "showLineNumbers")]
2194 #[ts(optional, rename = "showLineNumbers")]
2195 pub show_line_numbers: Option<bool>,
2196 #[serde(default, rename = "showCursors")]
2198 #[ts(optional, rename = "showCursors")]
2199 pub show_cursors: Option<bool>,
2200 #[serde(default, rename = "editingDisabled")]
2202 #[ts(optional, rename = "editingDisabled")]
2203 pub editing_disabled: Option<bool>,
2204 #[serde(default, rename = "lineWrap")]
2206 #[ts(optional, rename = "lineWrap")]
2207 pub line_wrap: Option<bool>,
2208 #[serde(default)]
2210 #[ts(optional)]
2211 pub entries: Option<Vec<JsTextPropertyEntry>>,
2212}
2213
2214#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2216#[serde(deny_unknown_fields)]
2217#[ts(export)]
2218pub struct CreateTerminalOptions {
2219 #[serde(default)]
2221 #[ts(optional)]
2222 pub cwd: Option<String>,
2223 #[serde(default)]
2225 #[ts(optional)]
2226 pub direction: Option<String>,
2227 #[serde(default)]
2229 #[ts(optional)]
2230 pub ratio: Option<f32>,
2231 #[serde(default)]
2233 #[ts(optional)]
2234 pub focus: Option<bool>,
2235}
2236
2237#[derive(Debug, Clone, Serialize, TS)]
2242#[ts(export, type = "Array<Record<string, unknown>>")]
2243pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
2244
2245#[cfg(feature = "plugins")]
2247mod fromjs_impls {
2248 use super::*;
2249 use rquickjs::{Ctx, FromJs, Value};
2250
2251 impl<'js> FromJs<'js> for JsTextPropertyEntry {
2252 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2253 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2254 from: "object",
2255 to: "JsTextPropertyEntry",
2256 message: Some(e.to_string()),
2257 })
2258 }
2259 }
2260
2261 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
2262 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2263 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2264 from: "object",
2265 to: "CreateVirtualBufferOptions",
2266 message: Some(e.to_string()),
2267 })
2268 }
2269 }
2270
2271 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
2272 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2273 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2274 from: "object",
2275 to: "CreateVirtualBufferInSplitOptions",
2276 message: Some(e.to_string()),
2277 })
2278 }
2279 }
2280
2281 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
2282 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2283 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2284 from: "object",
2285 to: "CreateVirtualBufferInExistingSplitOptions",
2286 message: Some(e.to_string()),
2287 })
2288 }
2289 }
2290
2291 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
2292 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
2293 rquickjs_serde::to_value(ctx.clone(), &self.0)
2294 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
2295 }
2296 }
2297
2298 impl<'js> FromJs<'js> for ActionSpec {
2301 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2302 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2303 from: "object",
2304 to: "ActionSpec",
2305 message: Some(e.to_string()),
2306 })
2307 }
2308 }
2309
2310 impl<'js> FromJs<'js> for ActionPopupAction {
2311 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2312 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2313 from: "object",
2314 to: "ActionPopupAction",
2315 message: Some(e.to_string()),
2316 })
2317 }
2318 }
2319
2320 impl<'js> FromJs<'js> for ActionPopupOptions {
2321 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2322 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2323 from: "object",
2324 to: "ActionPopupOptions",
2325 message: Some(e.to_string()),
2326 })
2327 }
2328 }
2329
2330 impl<'js> FromJs<'js> for ViewTokenWire {
2331 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2332 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2333 from: "object",
2334 to: "ViewTokenWire",
2335 message: Some(e.to_string()),
2336 })
2337 }
2338 }
2339
2340 impl<'js> FromJs<'js> for ViewTokenStyle {
2341 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2342 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2343 from: "object",
2344 to: "ViewTokenStyle",
2345 message: Some(e.to_string()),
2346 })
2347 }
2348 }
2349
2350 impl<'js> FromJs<'js> for LayoutHints {
2351 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2352 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2353 from: "object",
2354 to: "LayoutHints",
2355 message: Some(e.to_string()),
2356 })
2357 }
2358 }
2359
2360 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
2361 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2362 let json: serde_json::Value =
2364 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2365 from: "object",
2366 to: "CreateCompositeBufferOptions (json)",
2367 message: Some(e.to_string()),
2368 })?;
2369 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
2370 from: "json",
2371 to: "CreateCompositeBufferOptions",
2372 message: Some(e.to_string()),
2373 })
2374 }
2375 }
2376
2377 impl<'js> FromJs<'js> for CompositeHunk {
2378 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2379 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2380 from: "object",
2381 to: "CompositeHunk",
2382 message: Some(e.to_string()),
2383 })
2384 }
2385 }
2386
2387 impl<'js> FromJs<'js> for LanguagePackConfig {
2388 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2389 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2390 from: "object",
2391 to: "LanguagePackConfig",
2392 message: Some(e.to_string()),
2393 })
2394 }
2395 }
2396
2397 impl<'js> FromJs<'js> for LspServerPackConfig {
2398 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2399 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2400 from: "object",
2401 to: "LspServerPackConfig",
2402 message: Some(e.to_string()),
2403 })
2404 }
2405 }
2406
2407 impl<'js> FromJs<'js> for ProcessLimitsPackConfig {
2408 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2409 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2410 from: "object",
2411 to: "ProcessLimitsPackConfig",
2412 message: Some(e.to_string()),
2413 })
2414 }
2415 }
2416
2417 impl<'js> FromJs<'js> for CreateTerminalOptions {
2418 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2419 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2420 from: "object",
2421 to: "CreateTerminalOptions",
2422 message: Some(e.to_string()),
2423 })
2424 }
2425 }
2426}
2427
2428pub struct PluginApi {
2430 hooks: Arc<RwLock<HookRegistry>>,
2432
2433 commands: Arc<RwLock<CommandRegistry>>,
2435
2436 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2438
2439 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2441}
2442
2443impl PluginApi {
2444 pub fn new(
2446 hooks: Arc<RwLock<HookRegistry>>,
2447 commands: Arc<RwLock<CommandRegistry>>,
2448 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2449 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2450 ) -> Self {
2451 Self {
2452 hooks,
2453 commands,
2454 command_sender,
2455 state_snapshot,
2456 }
2457 }
2458
2459 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
2461 let mut hooks = self.hooks.write().unwrap();
2462 hooks.add_hook(hook_name, callback);
2463 }
2464
2465 pub fn unregister_hooks(&self, hook_name: &str) {
2467 let mut hooks = self.hooks.write().unwrap();
2468 hooks.remove_hooks(hook_name);
2469 }
2470
2471 pub fn register_command(&self, command: Command) {
2473 let commands = self.commands.read().unwrap();
2474 commands.register(command);
2475 }
2476
2477 pub fn unregister_command(&self, name: &str) {
2479 let commands = self.commands.read().unwrap();
2480 commands.unregister(name);
2481 }
2482
2483 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
2485 self.command_sender
2486 .send(command)
2487 .map_err(|e| format!("Failed to send command: {}", e))
2488 }
2489
2490 pub fn insert_text(
2492 &self,
2493 buffer_id: BufferId,
2494 position: usize,
2495 text: String,
2496 ) -> Result<(), String> {
2497 self.send_command(PluginCommand::InsertText {
2498 buffer_id,
2499 position,
2500 text,
2501 })
2502 }
2503
2504 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
2506 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
2507 }
2508
2509 pub fn add_overlay(
2517 &self,
2518 buffer_id: BufferId,
2519 namespace: Option<String>,
2520 range: Range<usize>,
2521 options: OverlayOptions,
2522 ) -> Result<(), String> {
2523 self.send_command(PluginCommand::AddOverlay {
2524 buffer_id,
2525 namespace: namespace.map(OverlayNamespace::from_string),
2526 range,
2527 options,
2528 })
2529 }
2530
2531 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
2533 self.send_command(PluginCommand::RemoveOverlay {
2534 buffer_id,
2535 handle: OverlayHandle::from_string(handle),
2536 })
2537 }
2538
2539 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
2541 self.send_command(PluginCommand::ClearNamespace {
2542 buffer_id,
2543 namespace: OverlayNamespace::from_string(namespace),
2544 })
2545 }
2546
2547 pub fn clear_overlays_in_range(
2550 &self,
2551 buffer_id: BufferId,
2552 start: usize,
2553 end: usize,
2554 ) -> Result<(), String> {
2555 self.send_command(PluginCommand::ClearOverlaysInRange {
2556 buffer_id,
2557 start,
2558 end,
2559 })
2560 }
2561
2562 pub fn set_status(&self, message: String) -> Result<(), String> {
2564 self.send_command(PluginCommand::SetStatus { message })
2565 }
2566
2567 pub fn open_file_at_location(
2570 &self,
2571 path: PathBuf,
2572 line: Option<usize>,
2573 column: Option<usize>,
2574 ) -> Result<(), String> {
2575 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
2576 }
2577
2578 pub fn open_file_in_split(
2583 &self,
2584 split_id: usize,
2585 path: PathBuf,
2586 line: Option<usize>,
2587 column: Option<usize>,
2588 ) -> Result<(), String> {
2589 self.send_command(PluginCommand::OpenFileInSplit {
2590 split_id,
2591 path,
2592 line,
2593 column,
2594 })
2595 }
2596
2597 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
2600 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
2601 }
2602
2603 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
2606 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
2607 }
2608
2609 pub fn set_prompt_input_sync(&self, sync: bool) -> Result<(), String> {
2611 self.send_command(PluginCommand::SetPromptInputSync { sync })
2612 }
2613
2614 pub fn add_menu_item(
2616 &self,
2617 menu_label: String,
2618 item: MenuItem,
2619 position: MenuPosition,
2620 ) -> Result<(), String> {
2621 self.send_command(PluginCommand::AddMenuItem {
2622 menu_label,
2623 item,
2624 position,
2625 })
2626 }
2627
2628 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
2630 self.send_command(PluginCommand::AddMenu { menu, position })
2631 }
2632
2633 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
2635 self.send_command(PluginCommand::RemoveMenuItem {
2636 menu_label,
2637 item_label,
2638 })
2639 }
2640
2641 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
2643 self.send_command(PluginCommand::RemoveMenu { menu_label })
2644 }
2645
2646 pub fn create_virtual_buffer(
2653 &self,
2654 name: String,
2655 mode: String,
2656 read_only: bool,
2657 ) -> Result<(), String> {
2658 self.send_command(PluginCommand::CreateVirtualBuffer {
2659 name,
2660 mode,
2661 read_only,
2662 })
2663 }
2664
2665 pub fn create_virtual_buffer_with_content(
2671 &self,
2672 name: String,
2673 mode: String,
2674 read_only: bool,
2675 entries: Vec<TextPropertyEntry>,
2676 ) -> Result<(), String> {
2677 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
2678 name,
2679 mode,
2680 read_only,
2681 entries,
2682 show_line_numbers: true,
2683 show_cursors: true,
2684 editing_disabled: false,
2685 hidden_from_tabs: false,
2686 request_id: None,
2687 })
2688 }
2689
2690 pub fn set_virtual_buffer_content(
2694 &self,
2695 buffer_id: BufferId,
2696 entries: Vec<TextPropertyEntry>,
2697 ) -> Result<(), String> {
2698 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
2699 }
2700
2701 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
2705 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
2706 }
2707
2708 pub fn define_mode(
2712 &self,
2713 name: String,
2714 bindings: Vec<(String, String)>,
2715 read_only: bool,
2716 allow_text_input: bool,
2717 ) -> Result<(), String> {
2718 self.send_command(PluginCommand::DefineMode {
2719 name,
2720 bindings,
2721 read_only,
2722 allow_text_input,
2723 plugin_name: None,
2724 })
2725 }
2726
2727 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
2729 self.send_command(PluginCommand::ShowBuffer { buffer_id })
2730 }
2731
2732 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
2734 self.send_command(PluginCommand::SetSplitScroll {
2735 split_id: SplitId(split_id),
2736 top_byte,
2737 })
2738 }
2739
2740 pub fn get_highlights(
2742 &self,
2743 buffer_id: BufferId,
2744 range: Range<usize>,
2745 request_id: u64,
2746 ) -> Result<(), String> {
2747 self.send_command(PluginCommand::RequestHighlights {
2748 buffer_id,
2749 range,
2750 request_id,
2751 })
2752 }
2753
2754 pub fn get_active_buffer_id(&self) -> BufferId {
2758 let snapshot = self.state_snapshot.read().unwrap();
2759 snapshot.active_buffer_id
2760 }
2761
2762 pub fn get_active_split_id(&self) -> usize {
2764 let snapshot = self.state_snapshot.read().unwrap();
2765 snapshot.active_split_id
2766 }
2767
2768 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
2770 let snapshot = self.state_snapshot.read().unwrap();
2771 snapshot.buffers.get(&buffer_id).cloned()
2772 }
2773
2774 pub fn list_buffers(&self) -> Vec<BufferInfo> {
2776 let snapshot = self.state_snapshot.read().unwrap();
2777 snapshot.buffers.values().cloned().collect()
2778 }
2779
2780 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
2782 let snapshot = self.state_snapshot.read().unwrap();
2783 snapshot.primary_cursor.clone()
2784 }
2785
2786 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
2788 let snapshot = self.state_snapshot.read().unwrap();
2789 snapshot.all_cursors.clone()
2790 }
2791
2792 pub fn get_viewport(&self) -> Option<ViewportInfo> {
2794 let snapshot = self.state_snapshot.read().unwrap();
2795 snapshot.viewport.clone()
2796 }
2797
2798 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
2800 Arc::clone(&self.state_snapshot)
2801 }
2802}
2803
2804impl Clone for PluginApi {
2805 fn clone(&self) -> Self {
2806 Self {
2807 hooks: Arc::clone(&self.hooks),
2808 commands: Arc::clone(&self.commands),
2809 command_sender: self.command_sender.clone(),
2810 state_snapshot: Arc::clone(&self.state_snapshot),
2811 }
2812 }
2813}
2814
2815#[cfg(test)]
2816mod tests {
2817 use super::*;
2818
2819 #[test]
2820 fn test_plugin_api_creation() {
2821 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2822 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2823 let (tx, _rx) = std::sync::mpsc::channel();
2824 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2825
2826 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2827
2828 let _clone = api.clone();
2830 }
2831
2832 #[test]
2833 fn test_register_hook() {
2834 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2835 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2836 let (tx, _rx) = std::sync::mpsc::channel();
2837 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2838
2839 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
2840
2841 api.register_hook("test-hook", Box::new(|_| true));
2842
2843 let hook_registry = hooks.read().unwrap();
2844 assert_eq!(hook_registry.hook_count("test-hook"), 1);
2845 }
2846
2847 #[test]
2848 fn test_send_command() {
2849 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2850 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2851 let (tx, rx) = std::sync::mpsc::channel();
2852 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2853
2854 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2855
2856 let result = api.insert_text(BufferId(1), 0, "test".to_string());
2857 assert!(result.is_ok());
2858
2859 let received = rx.try_recv();
2861 assert!(received.is_ok());
2862
2863 match received.unwrap() {
2864 PluginCommand::InsertText {
2865 buffer_id,
2866 position,
2867 text,
2868 } => {
2869 assert_eq!(buffer_id.0, 1);
2870 assert_eq!(position, 0);
2871 assert_eq!(text, "test");
2872 }
2873 _ => panic!("Wrong command type"),
2874 }
2875 }
2876
2877 #[test]
2878 fn test_add_overlay_command() {
2879 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2880 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2881 let (tx, rx) = std::sync::mpsc::channel();
2882 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2883
2884 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2885
2886 let result = api.add_overlay(
2887 BufferId(1),
2888 Some("test-overlay".to_string()),
2889 0..10,
2890 OverlayOptions {
2891 fg: Some(OverlayColorSpec::ThemeKey("ui.status_bar_fg".to_string())),
2892 bg: None,
2893 underline: true,
2894 bold: false,
2895 italic: false,
2896 strikethrough: false,
2897 extend_to_line_end: false,
2898 url: None,
2899 },
2900 );
2901 assert!(result.is_ok());
2902
2903 let received = rx.try_recv().unwrap();
2904 match received {
2905 PluginCommand::AddOverlay {
2906 buffer_id,
2907 namespace,
2908 range,
2909 options,
2910 } => {
2911 assert_eq!(buffer_id.0, 1);
2912 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
2913 assert_eq!(range, 0..10);
2914 assert!(matches!(
2915 options.fg,
2916 Some(OverlayColorSpec::ThemeKey(ref k)) if k == "ui.status_bar_fg"
2917 ));
2918 assert!(options.bg.is_none());
2919 assert!(options.underline);
2920 assert!(!options.bold);
2921 assert!(!options.italic);
2922 assert!(!options.extend_to_line_end);
2923 }
2924 _ => panic!("Wrong command type"),
2925 }
2926 }
2927
2928 #[test]
2929 fn test_set_status_command() {
2930 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2931 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2932 let (tx, rx) = std::sync::mpsc::channel();
2933 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2934
2935 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2936
2937 let result = api.set_status("Test status".to_string());
2938 assert!(result.is_ok());
2939
2940 let received = rx.try_recv().unwrap();
2941 match received {
2942 PluginCommand::SetStatus { message } => {
2943 assert_eq!(message, "Test status");
2944 }
2945 _ => panic!("Wrong command type"),
2946 }
2947 }
2948
2949 #[test]
2950 fn test_get_active_buffer_id() {
2951 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2952 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2953 let (tx, _rx) = std::sync::mpsc::channel();
2954 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2955
2956 {
2958 let mut snapshot = state_snapshot.write().unwrap();
2959 snapshot.active_buffer_id = BufferId(5);
2960 }
2961
2962 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2963
2964 let active_id = api.get_active_buffer_id();
2965 assert_eq!(active_id.0, 5);
2966 }
2967
2968 #[test]
2969 fn test_get_buffer_info() {
2970 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2971 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2972 let (tx, _rx) = std::sync::mpsc::channel();
2973 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2974
2975 {
2977 let mut snapshot = state_snapshot.write().unwrap();
2978 let buffer_info = BufferInfo {
2979 id: BufferId(1),
2980 path: Some(std::path::PathBuf::from("/test/file.txt")),
2981 modified: true,
2982 length: 100,
2983 is_virtual: false,
2984 view_mode: "source".to_string(),
2985 is_composing_in_any_split: false,
2986 compose_width: None,
2987 language: "text".to_string(),
2988 };
2989 snapshot.buffers.insert(BufferId(1), buffer_info);
2990 }
2991
2992 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2993
2994 let info = api.get_buffer_info(BufferId(1));
2995 assert!(info.is_some());
2996 let info = info.unwrap();
2997 assert_eq!(info.id.0, 1);
2998 assert_eq!(
2999 info.path.as_ref().unwrap().to_str().unwrap(),
3000 "/test/file.txt"
3001 );
3002 assert!(info.modified);
3003 assert_eq!(info.length, 100);
3004
3005 let no_info = api.get_buffer_info(BufferId(999));
3007 assert!(no_info.is_none());
3008 }
3009
3010 #[test]
3011 fn test_list_buffers() {
3012 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3013 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3014 let (tx, _rx) = std::sync::mpsc::channel();
3015 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3016
3017 {
3019 let mut snapshot = state_snapshot.write().unwrap();
3020 snapshot.buffers.insert(
3021 BufferId(1),
3022 BufferInfo {
3023 id: BufferId(1),
3024 path: Some(std::path::PathBuf::from("/file1.txt")),
3025 modified: false,
3026 length: 50,
3027 is_virtual: false,
3028 view_mode: "source".to_string(),
3029 is_composing_in_any_split: false,
3030 compose_width: None,
3031 language: "text".to_string(),
3032 },
3033 );
3034 snapshot.buffers.insert(
3035 BufferId(2),
3036 BufferInfo {
3037 id: BufferId(2),
3038 path: Some(std::path::PathBuf::from("/file2.txt")),
3039 modified: true,
3040 length: 100,
3041 is_virtual: false,
3042 view_mode: "source".to_string(),
3043 is_composing_in_any_split: false,
3044 compose_width: None,
3045 language: "text".to_string(),
3046 },
3047 );
3048 snapshot.buffers.insert(
3049 BufferId(3),
3050 BufferInfo {
3051 id: BufferId(3),
3052 path: None,
3053 modified: false,
3054 length: 0,
3055 is_virtual: true,
3056 view_mode: "source".to_string(),
3057 is_composing_in_any_split: false,
3058 compose_width: None,
3059 language: "text".to_string(),
3060 },
3061 );
3062 }
3063
3064 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3065
3066 let buffers = api.list_buffers();
3067 assert_eq!(buffers.len(), 3);
3068
3069 assert!(buffers.iter().any(|b| b.id.0 == 1));
3071 assert!(buffers.iter().any(|b| b.id.0 == 2));
3072 assert!(buffers.iter().any(|b| b.id.0 == 3));
3073 }
3074
3075 #[test]
3076 fn test_get_primary_cursor() {
3077 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3078 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3079 let (tx, _rx) = std::sync::mpsc::channel();
3080 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3081
3082 {
3084 let mut snapshot = state_snapshot.write().unwrap();
3085 snapshot.primary_cursor = Some(CursorInfo {
3086 position: 42,
3087 selection: Some(10..42),
3088 });
3089 }
3090
3091 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3092
3093 let cursor = api.get_primary_cursor();
3094 assert!(cursor.is_some());
3095 let cursor = cursor.unwrap();
3096 assert_eq!(cursor.position, 42);
3097 assert_eq!(cursor.selection, Some(10..42));
3098 }
3099
3100 #[test]
3101 fn test_get_all_cursors() {
3102 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3103 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3104 let (tx, _rx) = std::sync::mpsc::channel();
3105 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3106
3107 {
3109 let mut snapshot = state_snapshot.write().unwrap();
3110 snapshot.all_cursors = vec![
3111 CursorInfo {
3112 position: 10,
3113 selection: None,
3114 },
3115 CursorInfo {
3116 position: 20,
3117 selection: Some(15..20),
3118 },
3119 CursorInfo {
3120 position: 30,
3121 selection: Some(25..30),
3122 },
3123 ];
3124 }
3125
3126 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3127
3128 let cursors = api.get_all_cursors();
3129 assert_eq!(cursors.len(), 3);
3130 assert_eq!(cursors[0].position, 10);
3131 assert_eq!(cursors[0].selection, None);
3132 assert_eq!(cursors[1].position, 20);
3133 assert_eq!(cursors[1].selection, Some(15..20));
3134 assert_eq!(cursors[2].position, 30);
3135 assert_eq!(cursors[2].selection, Some(25..30));
3136 }
3137
3138 #[test]
3139 fn test_get_viewport() {
3140 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3141 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3142 let (tx, _rx) = std::sync::mpsc::channel();
3143 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3144
3145 {
3147 let mut snapshot = state_snapshot.write().unwrap();
3148 snapshot.viewport = Some(ViewportInfo {
3149 top_byte: 100,
3150 top_line: Some(5),
3151 left_column: 5,
3152 width: 80,
3153 height: 24,
3154 });
3155 }
3156
3157 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3158
3159 let viewport = api.get_viewport();
3160 assert!(viewport.is_some());
3161 let viewport = viewport.unwrap();
3162 assert_eq!(viewport.top_byte, 100);
3163 assert_eq!(viewport.left_column, 5);
3164 assert_eq!(viewport.width, 80);
3165 assert_eq!(viewport.height, 24);
3166 }
3167
3168 #[test]
3169 fn test_composite_buffer_options_rejects_unknown_fields() {
3170 let valid_json = r#"{
3172 "name": "test",
3173 "mode": "diff",
3174 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3175 "sources": [{"bufferId": 1, "label": "old"}]
3176 }"#;
3177 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
3178 assert!(
3179 result.is_ok(),
3180 "Valid JSON should parse: {:?}",
3181 result.err()
3182 );
3183
3184 let invalid_json = r#"{
3186 "name": "test",
3187 "mode": "diff",
3188 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3189 "sources": [{"buffer_id": 1, "label": "old"}]
3190 }"#;
3191 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
3192 assert!(
3193 result.is_err(),
3194 "JSON with unknown field should fail to parse"
3195 );
3196 let err = result.unwrap_err().to_string();
3197 assert!(
3198 err.contains("unknown field") || err.contains("buffer_id"),
3199 "Error should mention unknown field: {}",
3200 err
3201 );
3202 }
3203
3204 #[test]
3205 fn test_composite_hunk_rejects_unknown_fields() {
3206 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3208 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
3209 assert!(
3210 result.is_ok(),
3211 "Valid JSON should parse: {:?}",
3212 result.err()
3213 );
3214
3215 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3217 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
3218 assert!(
3219 result.is_err(),
3220 "JSON with unknown field should fail to parse"
3221 );
3222 let err = result.unwrap_err().to_string();
3223 assert!(
3224 err.contains("unknown field") || err.contains("old_start"),
3225 "Error should mention unknown field: {}",
3226 err
3227 );
3228 }
3229
3230 #[test]
3231 fn test_plugin_response_line_end_position() {
3232 let response = PluginResponse::LineEndPosition {
3233 request_id: 42,
3234 position: Some(100),
3235 };
3236 let json = serde_json::to_string(&response).unwrap();
3237 assert!(json.contains("LineEndPosition"));
3238 assert!(json.contains("42"));
3239 assert!(json.contains("100"));
3240
3241 let response_none = PluginResponse::LineEndPosition {
3243 request_id: 1,
3244 position: None,
3245 };
3246 let json_none = serde_json::to_string(&response_none).unwrap();
3247 assert!(json_none.contains("null"));
3248 }
3249
3250 #[test]
3251 fn test_plugin_response_buffer_line_count() {
3252 let response = PluginResponse::BufferLineCount {
3253 request_id: 99,
3254 count: Some(500),
3255 };
3256 let json = serde_json::to_string(&response).unwrap();
3257 assert!(json.contains("BufferLineCount"));
3258 assert!(json.contains("99"));
3259 assert!(json.contains("500"));
3260 }
3261
3262 #[test]
3263 fn test_plugin_command_get_line_end_position() {
3264 let command = PluginCommand::GetLineEndPosition {
3265 buffer_id: BufferId(1),
3266 line: 10,
3267 request_id: 123,
3268 };
3269 let json = serde_json::to_string(&command).unwrap();
3270 assert!(json.contains("GetLineEndPosition"));
3271 assert!(json.contains("10"));
3272 }
3273
3274 #[test]
3275 fn test_plugin_command_get_buffer_line_count() {
3276 let command = PluginCommand::GetBufferLineCount {
3277 buffer_id: BufferId(0),
3278 request_id: 456,
3279 };
3280 let json = serde_json::to_string(&command).unwrap();
3281 assert!(json.contains("GetBufferLineCount"));
3282 assert!(json.contains("456"));
3283 }
3284
3285 #[test]
3286 fn test_plugin_command_scroll_to_line_center() {
3287 let command = PluginCommand::ScrollToLineCenter {
3288 split_id: SplitId(1),
3289 buffer_id: BufferId(2),
3290 line: 50,
3291 };
3292 let json = serde_json::to_string(&command).unwrap();
3293 assert!(json.contains("ScrollToLineCenter"));
3294 assert!(json.contains("50"));
3295 }
3296}