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
373#[derive(Debug, Clone, Serialize, Deserialize, TS)]
375#[ts(export)]
376pub struct BufferSavedDiff {
377 pub equal: bool,
378 #[serde(serialize_with = "serialize_ranges_as_tuples")]
379 #[ts(type = "Array<[number, number]>")]
380 pub byte_ranges: Vec<Range<usize>>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize, TS)]
385#[serde(rename_all = "camelCase")]
386#[ts(export, rename_all = "camelCase")]
387pub struct ViewportInfo {
388 pub top_byte: usize,
390 pub top_line: Option<usize>,
392 pub left_column: usize,
394 pub width: u16,
396 pub height: u16,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize, TS)]
402#[serde(rename_all = "camelCase")]
403#[ts(export, rename_all = "camelCase")]
404pub struct LayoutHints {
405 #[ts(optional)]
407 pub compose_width: Option<u16>,
408 #[ts(optional)]
410 pub column_guides: Option<Vec<u16>>,
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize, TS)]
428#[serde(untagged)]
429#[ts(export)]
430pub enum OverlayColorSpec {
431 #[ts(type = "[number, number, number]")]
433 Rgb(u8, u8, u8),
434 ThemeKey(String),
436}
437
438impl OverlayColorSpec {
439 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
441 Self::Rgb(r, g, b)
442 }
443
444 pub fn theme_key(key: impl Into<String>) -> Self {
446 Self::ThemeKey(key.into())
447 }
448
449 pub fn as_rgb(&self) -> Option<(u8, u8, u8)> {
451 match self {
452 Self::Rgb(r, g, b) => Some((*r, *g, *b)),
453 Self::ThemeKey(_) => None,
454 }
455 }
456
457 pub fn as_theme_key(&self) -> Option<&str> {
459 match self {
460 Self::ThemeKey(key) => Some(key),
461 Self::Rgb(_, _, _) => None,
462 }
463 }
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize, TS)]
471#[serde(deny_unknown_fields, rename_all = "camelCase")]
472#[ts(export, rename_all = "camelCase")]
473#[derive(Default)]
474pub struct OverlayOptions {
475 #[serde(default, skip_serializing_if = "Option::is_none")]
477 pub fg: Option<OverlayColorSpec>,
478
479 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub bg: Option<OverlayColorSpec>,
482
483 #[serde(default)]
485 pub underline: bool,
486
487 #[serde(default)]
489 pub bold: bool,
490
491 #[serde(default)]
493 pub italic: bool,
494
495 #[serde(default)]
497 pub strikethrough: bool,
498
499 #[serde(default)]
501 pub extend_to_line_end: bool,
502
503 #[serde(default, skip_serializing_if = "Option::is_none")]
507 pub url: Option<String>,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize, TS)]
516#[serde(deny_unknown_fields)]
517#[ts(export, rename = "TsCompositeLayoutConfig")]
518pub struct CompositeLayoutConfig {
519 #[serde(rename = "type")]
521 #[ts(rename = "type")]
522 pub layout_type: String,
523 #[serde(default)]
525 #[ts(optional)]
526 pub ratios: Option<Vec<f32>>,
527 #[serde(default = "default_true", rename = "showSeparator")]
529 #[ts(rename = "showSeparator")]
530 pub show_separator: bool,
531 #[serde(default)]
533 #[ts(optional)]
534 pub spacing: Option<u16>,
535}
536
537fn default_true() -> bool {
538 true
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize, TS)]
543#[serde(deny_unknown_fields)]
544#[ts(export, rename = "TsCompositeSourceConfig")]
545pub struct CompositeSourceConfig {
546 #[serde(rename = "bufferId")]
548 #[ts(rename = "bufferId")]
549 pub buffer_id: usize,
550 pub label: String,
552 #[serde(default)]
554 pub editable: bool,
555 #[serde(default)]
557 pub style: Option<CompositePaneStyle>,
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
562#[serde(deny_unknown_fields)]
563#[ts(export, rename = "TsCompositePaneStyle")]
564pub struct CompositePaneStyle {
565 #[serde(default, rename = "addBg")]
568 #[ts(optional, rename = "addBg", type = "[number, number, number]")]
569 pub add_bg: Option<[u8; 3]>,
570 #[serde(default, rename = "removeBg")]
572 #[ts(optional, rename = "removeBg", type = "[number, number, number]")]
573 pub remove_bg: Option<[u8; 3]>,
574 #[serde(default, rename = "modifyBg")]
576 #[ts(optional, rename = "modifyBg", type = "[number, number, number]")]
577 pub modify_bg: Option<[u8; 3]>,
578 #[serde(default, rename = "gutterStyle")]
580 #[ts(optional, rename = "gutterStyle")]
581 pub gutter_style: Option<String>,
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize, TS)]
586#[serde(deny_unknown_fields)]
587#[ts(export, rename = "TsCompositeHunk")]
588pub struct CompositeHunk {
589 #[serde(rename = "oldStart")]
591 #[ts(rename = "oldStart")]
592 pub old_start: usize,
593 #[serde(rename = "oldCount")]
595 #[ts(rename = "oldCount")]
596 pub old_count: usize,
597 #[serde(rename = "newStart")]
599 #[ts(rename = "newStart")]
600 pub new_start: usize,
601 #[serde(rename = "newCount")]
603 #[ts(rename = "newCount")]
604 pub new_count: usize,
605}
606
607#[derive(Debug, Clone, Serialize, Deserialize, TS)]
609#[serde(deny_unknown_fields)]
610#[ts(export, rename = "TsCreateCompositeBufferOptions")]
611pub struct CreateCompositeBufferOptions {
612 #[serde(default)]
614 pub name: String,
615 #[serde(default)]
617 pub mode: String,
618 pub layout: CompositeLayoutConfig,
620 pub sources: Vec<CompositeSourceConfig>,
622 #[serde(default)]
624 pub hunks: Option<Vec<CompositeHunk>>,
625}
626
627#[derive(Debug, Clone, Serialize, Deserialize, TS)]
629#[ts(export)]
630pub enum ViewTokenWireKind {
631 Text(String),
632 Newline,
633 Space,
634 Break,
637 BinaryByte(u8),
641}
642
643#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
649#[serde(deny_unknown_fields)]
650#[ts(export)]
651pub struct ViewTokenStyle {
652 #[serde(default)]
654 #[ts(type = "[number, number, number] | null")]
655 pub fg: Option<(u8, u8, u8)>,
656 #[serde(default)]
658 #[ts(type = "[number, number, number] | null")]
659 pub bg: Option<(u8, u8, u8)>,
660 #[serde(default)]
662 pub bold: bool,
663 #[serde(default)]
665 pub italic: bool,
666}
667
668#[derive(Debug, Clone, Serialize, Deserialize, TS)]
670#[serde(deny_unknown_fields)]
671#[ts(export)]
672pub struct ViewTokenWire {
673 #[ts(type = "number | null")]
675 pub source_offset: Option<usize>,
676 pub kind: ViewTokenWireKind,
678 #[serde(default)]
680 #[ts(optional)]
681 pub style: Option<ViewTokenStyle>,
682}
683
684#[derive(Debug, Clone, Serialize, Deserialize, TS)]
686#[ts(export)]
687pub struct ViewTransformPayload {
688 pub range: Range<usize>,
690 pub tokens: Vec<ViewTokenWire>,
692 pub layout_hints: Option<LayoutHints>,
694}
695
696#[derive(Debug, Clone, Serialize, Deserialize, TS)]
699#[ts(export)]
700pub struct EditorStateSnapshot {
701 pub active_buffer_id: BufferId,
703 pub active_split_id: usize,
705 pub buffers: HashMap<BufferId, BufferInfo>,
707 pub buffer_saved_diffs: HashMap<BufferId, BufferSavedDiff>,
709 pub primary_cursor: Option<CursorInfo>,
711 pub all_cursors: Vec<CursorInfo>,
713 pub viewport: Option<ViewportInfo>,
715 pub buffer_cursor_positions: HashMap<BufferId, usize>,
717 pub buffer_text_properties: HashMap<BufferId, Vec<TextProperty>>,
719 pub selected_text: Option<String>,
722 pub clipboard: String,
724 pub working_dir: PathBuf,
726 #[ts(type = "any")]
729 pub diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
730 #[ts(type = "any")]
733 pub folding_ranges: HashMap<String, Vec<lsp_types::FoldingRange>>,
734 #[ts(type = "any")]
737 pub config: serde_json::Value,
738 #[ts(type = "any")]
741 pub user_config: serde_json::Value,
742 pub editor_mode: Option<String>,
745
746 #[ts(type = "any")]
750 pub plugin_view_states: HashMap<BufferId, HashMap<String, serde_json::Value>>,
751
752 #[serde(skip)]
755 #[ts(skip)]
756 pub plugin_view_states_split: usize,
757
758 #[serde(skip)]
761 #[ts(skip)]
762 pub keybinding_labels: HashMap<String, String>,
763
764 #[ts(type = "any")]
771 pub plugin_global_states: HashMap<String, HashMap<String, serde_json::Value>>,
772}
773
774impl EditorStateSnapshot {
775 pub fn new() -> Self {
776 Self {
777 active_buffer_id: BufferId(0),
778 active_split_id: 0,
779 buffers: HashMap::new(),
780 buffer_saved_diffs: HashMap::new(),
781 primary_cursor: None,
782 all_cursors: Vec::new(),
783 viewport: None,
784 buffer_cursor_positions: HashMap::new(),
785 buffer_text_properties: HashMap::new(),
786 selected_text: None,
787 clipboard: String::new(),
788 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
789 diagnostics: HashMap::new(),
790 folding_ranges: HashMap::new(),
791 config: serde_json::Value::Null,
792 user_config: serde_json::Value::Null,
793 editor_mode: None,
794 plugin_view_states: HashMap::new(),
795 plugin_view_states_split: 0,
796 keybinding_labels: HashMap::new(),
797 plugin_global_states: HashMap::new(),
798 }
799 }
800}
801
802impl Default for EditorStateSnapshot {
803 fn default() -> Self {
804 Self::new()
805 }
806}
807
808#[derive(Debug, Clone, Serialize, Deserialize, TS)]
810#[ts(export)]
811pub enum MenuPosition {
812 Top,
814 Bottom,
816 Before(String),
818 After(String),
820}
821
822#[derive(Debug, Clone, Serialize, Deserialize, TS)]
824#[ts(export)]
825pub enum PluginCommand {
826 InsertText {
828 buffer_id: BufferId,
829 position: usize,
830 text: String,
831 },
832
833 DeleteRange {
835 buffer_id: BufferId,
836 range: Range<usize>,
837 },
838
839 AddOverlay {
844 buffer_id: BufferId,
845 namespace: Option<OverlayNamespace>,
846 range: Range<usize>,
847 options: OverlayOptions,
849 },
850
851 RemoveOverlay {
853 buffer_id: BufferId,
854 handle: OverlayHandle,
855 },
856
857 SetStatus { message: String },
859
860 ApplyTheme { theme_name: String },
862
863 ReloadConfig,
866
867 RegisterCommand { command: Command },
869
870 UnregisterCommand { name: String },
872
873 OpenFileInBackground { path: PathBuf },
875
876 InsertAtCursor { text: String },
878
879 SpawnProcess {
881 command: String,
882 args: Vec<String>,
883 cwd: Option<String>,
884 callback_id: JsCallbackId,
885 },
886
887 Delay {
889 callback_id: JsCallbackId,
890 duration_ms: u64,
891 },
892
893 SpawnBackgroundProcess {
897 process_id: u64,
899 command: String,
901 args: Vec<String>,
903 cwd: Option<String>,
905 callback_id: JsCallbackId,
907 },
908
909 KillBackgroundProcess { process_id: u64 },
911
912 SpawnProcessWait {
915 process_id: u64,
917 callback_id: JsCallbackId,
919 },
920
921 SetLayoutHints {
923 buffer_id: BufferId,
924 split_id: Option<SplitId>,
925 range: Range<usize>,
926 hints: LayoutHints,
927 },
928
929 SetLineNumbers { buffer_id: BufferId, enabled: bool },
931
932 SetViewMode { buffer_id: BufferId, mode: String },
934
935 SetLineWrap {
937 buffer_id: BufferId,
938 split_id: Option<SplitId>,
939 enabled: bool,
940 },
941
942 SubmitViewTransform {
944 buffer_id: BufferId,
945 split_id: Option<SplitId>,
946 payload: ViewTransformPayload,
947 },
948
949 ClearViewTransform {
951 buffer_id: BufferId,
952 split_id: Option<SplitId>,
953 },
954
955 SetViewState {
958 buffer_id: BufferId,
959 key: String,
960 #[ts(type = "any")]
961 value: Option<serde_json::Value>,
962 },
963
964 SetGlobalState {
968 plugin_name: String,
969 key: String,
970 #[ts(type = "any")]
971 value: Option<serde_json::Value>,
972 },
973
974 ClearAllOverlays { buffer_id: BufferId },
976
977 ClearNamespace {
979 buffer_id: BufferId,
980 namespace: OverlayNamespace,
981 },
982
983 ClearOverlaysInRange {
986 buffer_id: BufferId,
987 start: usize,
988 end: usize,
989 },
990
991 AddVirtualText {
994 buffer_id: BufferId,
995 virtual_text_id: String,
996 position: usize,
997 text: String,
998 color: (u8, u8, u8),
999 use_bg: bool, before: bool, },
1002
1003 RemoveVirtualText {
1005 buffer_id: BufferId,
1006 virtual_text_id: String,
1007 },
1008
1009 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
1011
1012 ClearVirtualTexts { buffer_id: BufferId },
1014
1015 AddVirtualLine {
1019 buffer_id: BufferId,
1020 position: usize,
1022 text: String,
1024 fg_color: (u8, u8, u8),
1026 bg_color: Option<(u8, u8, u8)>,
1028 above: bool,
1030 namespace: String,
1032 priority: i32,
1034 },
1035
1036 ClearVirtualTextNamespace {
1039 buffer_id: BufferId,
1040 namespace: String,
1041 },
1042
1043 AddConceal {
1046 buffer_id: BufferId,
1047 namespace: OverlayNamespace,
1049 start: usize,
1051 end: usize,
1052 replacement: Option<String>,
1054 },
1055
1056 ClearConcealNamespace {
1058 buffer_id: BufferId,
1059 namespace: OverlayNamespace,
1060 },
1061
1062 ClearConcealsInRange {
1065 buffer_id: BufferId,
1066 start: usize,
1067 end: usize,
1068 },
1069
1070 AddSoftBreak {
1074 buffer_id: BufferId,
1075 namespace: OverlayNamespace,
1077 position: usize,
1079 indent: u16,
1081 },
1082
1083 ClearSoftBreakNamespace {
1085 buffer_id: BufferId,
1086 namespace: OverlayNamespace,
1087 },
1088
1089 ClearSoftBreaksInRange {
1091 buffer_id: BufferId,
1092 start: usize,
1093 end: usize,
1094 },
1095
1096 RefreshLines { buffer_id: BufferId },
1098
1099 RefreshAllLines,
1103
1104 HookCompleted { hook_name: String },
1108
1109 SetLineIndicator {
1112 buffer_id: BufferId,
1113 line: usize,
1115 namespace: String,
1117 symbol: String,
1119 color: (u8, u8, u8),
1121 priority: i32,
1123 },
1124
1125 SetLineIndicators {
1128 buffer_id: BufferId,
1129 lines: Vec<usize>,
1131 namespace: String,
1133 symbol: String,
1135 color: (u8, u8, u8),
1137 priority: i32,
1139 },
1140
1141 ClearLineIndicators {
1143 buffer_id: BufferId,
1144 namespace: String,
1146 },
1147
1148 SetFileExplorerDecorations {
1150 namespace: String,
1152 decorations: Vec<FileExplorerDecoration>,
1154 },
1155
1156 ClearFileExplorerDecorations {
1158 namespace: String,
1160 },
1161
1162 OpenFileAtLocation {
1165 path: PathBuf,
1166 line: Option<usize>, column: Option<usize>, },
1169
1170 OpenFileInSplit {
1173 split_id: usize,
1174 path: PathBuf,
1175 line: Option<usize>, column: Option<usize>, },
1178
1179 StartPrompt {
1182 label: String,
1183 prompt_type: String, },
1185
1186 StartPromptWithInitial {
1188 label: String,
1189 prompt_type: String,
1190 initial_value: String,
1191 },
1192
1193 StartPromptAsync {
1196 label: String,
1197 initial_value: String,
1198 callback_id: JsCallbackId,
1199 },
1200
1201 SetPromptSuggestions { suggestions: Vec<Suggestion> },
1204
1205 SetPromptInputSync { sync: bool },
1207
1208 AddMenuItem {
1211 menu_label: String,
1212 item: MenuItem,
1213 position: MenuPosition,
1214 },
1215
1216 AddMenu { menu: Menu, position: MenuPosition },
1218
1219 RemoveMenuItem {
1221 menu_label: String,
1222 item_label: String,
1223 },
1224
1225 RemoveMenu { menu_label: String },
1227
1228 CreateVirtualBuffer {
1230 name: String,
1232 mode: String,
1234 read_only: bool,
1236 },
1237
1238 CreateVirtualBufferWithContent {
1242 name: String,
1244 mode: String,
1246 read_only: bool,
1248 entries: Vec<TextPropertyEntry>,
1250 show_line_numbers: bool,
1252 show_cursors: bool,
1254 editing_disabled: bool,
1256 hidden_from_tabs: bool,
1258 request_id: Option<u64>,
1260 },
1261
1262 CreateVirtualBufferInSplit {
1265 name: String,
1267 mode: String,
1269 read_only: bool,
1271 entries: Vec<TextPropertyEntry>,
1273 ratio: f32,
1275 direction: Option<String>,
1277 panel_id: Option<String>,
1279 show_line_numbers: bool,
1281 show_cursors: bool,
1283 editing_disabled: bool,
1285 line_wrap: Option<bool>,
1287 before: bool,
1289 request_id: Option<u64>,
1291 },
1292
1293 SetVirtualBufferContent {
1295 buffer_id: BufferId,
1296 entries: Vec<TextPropertyEntry>,
1298 },
1299
1300 GetTextPropertiesAtCursor { buffer_id: BufferId },
1302
1303 DefineMode {
1305 name: String,
1306 bindings: Vec<(String, String)>, read_only: bool,
1308 allow_text_input: bool,
1310 plugin_name: Option<String>,
1312 },
1313
1314 ShowBuffer { buffer_id: BufferId },
1316
1317 CreateVirtualBufferInExistingSplit {
1319 name: String,
1321 mode: String,
1323 read_only: bool,
1325 entries: Vec<TextPropertyEntry>,
1327 split_id: SplitId,
1329 show_line_numbers: bool,
1331 show_cursors: bool,
1333 editing_disabled: bool,
1335 line_wrap: Option<bool>,
1337 request_id: Option<u64>,
1339 },
1340
1341 CloseBuffer { buffer_id: BufferId },
1343
1344 CreateCompositeBuffer {
1347 name: String,
1349 mode: String,
1351 layout: CompositeLayoutConfig,
1353 sources: Vec<CompositeSourceConfig>,
1355 hunks: Option<Vec<CompositeHunk>>,
1357 request_id: Option<u64>,
1359 },
1360
1361 UpdateCompositeAlignment {
1363 buffer_id: BufferId,
1364 hunks: Vec<CompositeHunk>,
1365 },
1366
1367 CloseCompositeBuffer { buffer_id: BufferId },
1369
1370 FocusSplit { split_id: SplitId },
1372
1373 SetSplitBuffer {
1375 split_id: SplitId,
1376 buffer_id: BufferId,
1377 },
1378
1379 SetSplitScroll { split_id: SplitId, top_byte: usize },
1381
1382 RequestHighlights {
1384 buffer_id: BufferId,
1385 range: Range<usize>,
1386 request_id: u64,
1387 },
1388
1389 CloseSplit { split_id: SplitId },
1391
1392 SetSplitRatio {
1394 split_id: SplitId,
1395 ratio: f32,
1397 },
1398
1399 SetSplitLabel { split_id: SplitId, label: String },
1401
1402 ClearSplitLabel { split_id: SplitId },
1404
1405 GetSplitByLabel { label: String, request_id: u64 },
1407
1408 DistributeSplitsEvenly {
1410 split_ids: Vec<SplitId>,
1412 },
1413
1414 SetBufferCursor {
1416 buffer_id: BufferId,
1417 position: usize,
1419 },
1420
1421 SendLspRequest {
1423 language: String,
1424 method: String,
1425 #[ts(type = "any")]
1426 params: Option<JsonValue>,
1427 request_id: u64,
1428 },
1429
1430 SetClipboard { text: String },
1432
1433 DeleteSelection,
1436
1437 SetContext {
1441 name: String,
1443 active: bool,
1445 },
1446
1447 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1449
1450 ExecuteAction {
1453 action_name: String,
1455 },
1456
1457 ExecuteActions {
1461 actions: Vec<ActionSpec>,
1463 },
1464
1465 GetBufferText {
1467 buffer_id: BufferId,
1469 start: usize,
1471 end: usize,
1473 request_id: u64,
1475 },
1476
1477 GetLineStartPosition {
1480 buffer_id: BufferId,
1482 line: u32,
1484 request_id: u64,
1486 },
1487
1488 GetLineEndPosition {
1492 buffer_id: BufferId,
1494 line: u32,
1496 request_id: u64,
1498 },
1499
1500 GetBufferLineCount {
1502 buffer_id: BufferId,
1504 request_id: u64,
1506 },
1507
1508 ScrollToLineCenter {
1511 split_id: SplitId,
1513 buffer_id: BufferId,
1515 line: usize,
1517 },
1518
1519 SetEditorMode {
1522 mode: Option<String>,
1524 },
1525
1526 ShowActionPopup {
1529 popup_id: String,
1531 title: String,
1533 message: String,
1535 actions: Vec<ActionPopupAction>,
1537 },
1538
1539 DisableLspForLanguage {
1541 language: String,
1543 },
1544
1545 RestartLspForLanguage {
1547 language: String,
1549 },
1550
1551 SetLspRootUri {
1555 language: String,
1557 uri: String,
1559 },
1560
1561 CreateScrollSyncGroup {
1565 group_id: u32,
1567 left_split: SplitId,
1569 right_split: SplitId,
1571 },
1572
1573 SetScrollSyncAnchors {
1576 group_id: u32,
1578 anchors: Vec<(usize, usize)>,
1580 },
1581
1582 RemoveScrollSyncGroup {
1584 group_id: u32,
1586 },
1587
1588 SaveBufferToPath {
1591 buffer_id: BufferId,
1593 path: PathBuf,
1595 },
1596
1597 LoadPlugin {
1600 path: PathBuf,
1602 callback_id: JsCallbackId,
1604 },
1605
1606 UnloadPlugin {
1609 name: String,
1611 callback_id: JsCallbackId,
1613 },
1614
1615 ReloadPlugin {
1618 name: String,
1620 callback_id: JsCallbackId,
1622 },
1623
1624 ListPlugins {
1627 callback_id: JsCallbackId,
1629 },
1630
1631 ReloadThemes { apply_theme: Option<String> },
1635
1636 RegisterGrammar {
1639 language: String,
1641 grammar_path: String,
1643 extensions: Vec<String>,
1645 },
1646
1647 RegisterLanguageConfig {
1650 language: String,
1652 config: LanguagePackConfig,
1654 },
1655
1656 RegisterLspServer {
1659 language: String,
1661 config: LspServerPackConfig,
1663 },
1664
1665 ReloadGrammars { callback_id: JsCallbackId },
1669
1670 CreateTerminal {
1674 cwd: Option<String>,
1676 direction: Option<String>,
1678 ratio: Option<f32>,
1680 focus: Option<bool>,
1682 request_id: u64,
1684 },
1685
1686 SendTerminalInput {
1688 terminal_id: TerminalId,
1690 data: String,
1692 },
1693
1694 CloseTerminal {
1696 terminal_id: TerminalId,
1698 },
1699
1700 GrepProject {
1704 pattern: String,
1706 fixed_string: bool,
1708 case_sensitive: bool,
1710 max_results: usize,
1712 whole_words: bool,
1714 callback_id: JsCallbackId,
1716 },
1717
1718 GrepProjectStreaming {
1723 pattern: String,
1725 fixed_string: bool,
1727 case_sensitive: bool,
1729 max_results: usize,
1731 whole_words: bool,
1733 search_id: u64,
1735 callback_id: JsCallbackId,
1737 },
1738
1739 ReplaceInBuffer {
1743 file_path: PathBuf,
1745 matches: Vec<(usize, usize)>,
1747 replacement: String,
1749 callback_id: JsCallbackId,
1751 },
1752}
1753
1754impl PluginCommand {
1755 pub fn debug_variant_name(&self) -> String {
1757 let dbg = format!("{:?}", self);
1758 dbg.split([' ', '{', '(']).next().unwrap_or("?").to_string()
1759 }
1760}
1761
1762#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
1771#[serde(rename_all = "camelCase")]
1772#[ts(export)]
1773pub struct LanguagePackConfig {
1774 #[serde(default)]
1776 pub comment_prefix: Option<String>,
1777
1778 #[serde(default)]
1780 pub block_comment_start: Option<String>,
1781
1782 #[serde(default)]
1784 pub block_comment_end: Option<String>,
1785
1786 #[serde(default)]
1788 pub use_tabs: Option<bool>,
1789
1790 #[serde(default)]
1792 pub tab_size: Option<usize>,
1793
1794 #[serde(default)]
1796 pub auto_indent: Option<bool>,
1797
1798 #[serde(default)]
1801 pub show_whitespace_tabs: Option<bool>,
1802
1803 #[serde(default)]
1805 pub formatter: Option<FormatterPackConfig>,
1806}
1807
1808#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1810#[serde(rename_all = "camelCase")]
1811#[ts(export)]
1812pub struct FormatterPackConfig {
1813 pub command: String,
1815
1816 #[serde(default)]
1818 pub args: Vec<String>,
1819}
1820
1821#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1823#[serde(rename_all = "camelCase")]
1824#[ts(export)]
1825pub struct ProcessLimitsPackConfig {
1826 #[serde(default)]
1828 pub max_memory_percent: Option<u32>,
1829
1830 #[serde(default)]
1832 pub max_cpu_percent: Option<u32>,
1833
1834 #[serde(default)]
1836 pub enabled: Option<bool>,
1837}
1838
1839#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1841#[serde(rename_all = "camelCase")]
1842#[ts(export)]
1843pub struct LspServerPackConfig {
1844 pub command: String,
1846
1847 #[serde(default)]
1849 pub args: Vec<String>,
1850
1851 #[serde(default)]
1853 pub auto_start: Option<bool>,
1854
1855 #[serde(default)]
1857 #[ts(type = "Record<string, unknown> | null")]
1858 pub initialization_options: Option<JsonValue>,
1859
1860 #[serde(default)]
1862 pub process_limits: Option<ProcessLimitsPackConfig>,
1863}
1864
1865#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1867#[ts(export)]
1868pub enum HunkStatus {
1869 Pending,
1870 Staged,
1871 Discarded,
1872}
1873
1874#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1876#[ts(export)]
1877pub struct ReviewHunk {
1878 pub id: String,
1879 pub file: String,
1880 pub context_header: String,
1881 pub status: HunkStatus,
1882 pub base_range: Option<(usize, usize)>,
1884 pub modified_range: Option<(usize, usize)>,
1886}
1887
1888#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1890#[serde(deny_unknown_fields)]
1891#[ts(export, rename = "TsActionPopupAction")]
1892pub struct ActionPopupAction {
1893 pub id: String,
1895 pub label: String,
1897}
1898
1899#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1901#[serde(deny_unknown_fields)]
1902#[ts(export)]
1903pub struct ActionPopupOptions {
1904 pub id: String,
1906 pub title: String,
1908 pub message: String,
1910 pub actions: Vec<ActionPopupAction>,
1912}
1913
1914#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1916#[ts(export)]
1917pub struct TsHighlightSpan {
1918 pub start: u32,
1919 pub end: u32,
1920 #[ts(type = "[number, number, number]")]
1921 pub color: (u8, u8, u8),
1922 pub bold: bool,
1923 pub italic: bool,
1924}
1925
1926#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1928#[ts(export)]
1929pub struct SpawnResult {
1930 pub stdout: String,
1932 pub stderr: String,
1934 pub exit_code: i32,
1936}
1937
1938#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1940#[ts(export)]
1941pub struct BackgroundProcessResult {
1942 #[ts(type = "number")]
1944 pub process_id: u64,
1945 pub exit_code: i32,
1948}
1949
1950#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1952#[serde(rename_all = "camelCase")]
1953#[ts(export, rename_all = "camelCase")]
1954pub struct GrepMatch {
1955 pub file: String,
1957 #[ts(type = "number")]
1959 pub buffer_id: usize,
1960 #[ts(type = "number")]
1962 pub byte_offset: usize,
1963 #[ts(type = "number")]
1965 pub length: usize,
1966 #[ts(type = "number")]
1968 pub line: usize,
1969 #[ts(type = "number")]
1971 pub column: usize,
1972 pub context: String,
1974}
1975
1976#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1978#[serde(rename_all = "camelCase")]
1979#[ts(export, rename_all = "camelCase")]
1980pub struct ReplaceResult {
1981 #[ts(type = "number")]
1983 pub replacements: usize,
1984 #[ts(type = "number")]
1986 pub buffer_id: usize,
1987}
1988
1989#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1991#[serde(deny_unknown_fields, rename_all = "camelCase")]
1992#[ts(export, rename = "TextPropertyEntry", rename_all = "camelCase")]
1993pub struct JsTextPropertyEntry {
1994 pub text: String,
1996 #[serde(default)]
1998 #[ts(optional, type = "Record<string, unknown>")]
1999 pub properties: Option<HashMap<String, JsonValue>>,
2000 #[serde(default)]
2002 #[ts(optional, type = "Partial<OverlayOptions>")]
2003 pub style: Option<OverlayOptions>,
2004 #[serde(default)]
2006 #[ts(optional)]
2007 pub inline_overlays: Option<Vec<crate::text_property::InlineOverlay>>,
2008}
2009
2010#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2012#[ts(export)]
2013pub struct DirEntry {
2014 pub name: String,
2016 pub is_file: bool,
2018 pub is_dir: bool,
2020}
2021
2022#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2024#[ts(export)]
2025pub struct JsPosition {
2026 pub line: u32,
2028 pub character: u32,
2030}
2031
2032#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2034#[ts(export)]
2035pub struct JsRange {
2036 pub start: JsPosition,
2038 pub end: JsPosition,
2040}
2041
2042#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2044#[ts(export)]
2045pub struct JsDiagnostic {
2046 pub uri: String,
2048 pub message: String,
2050 pub severity: Option<u8>,
2052 pub range: JsRange,
2054 #[ts(optional)]
2056 pub source: Option<String>,
2057}
2058
2059#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2061#[serde(deny_unknown_fields)]
2062#[ts(export)]
2063pub struct CreateVirtualBufferOptions {
2064 pub name: String,
2066 #[serde(default)]
2068 #[ts(optional)]
2069 pub mode: Option<String>,
2070 #[serde(default, rename = "readOnly")]
2072 #[ts(optional, rename = "readOnly")]
2073 pub read_only: Option<bool>,
2074 #[serde(default, rename = "showLineNumbers")]
2076 #[ts(optional, rename = "showLineNumbers")]
2077 pub show_line_numbers: Option<bool>,
2078 #[serde(default, rename = "showCursors")]
2080 #[ts(optional, rename = "showCursors")]
2081 pub show_cursors: Option<bool>,
2082 #[serde(default, rename = "editingDisabled")]
2084 #[ts(optional, rename = "editingDisabled")]
2085 pub editing_disabled: Option<bool>,
2086 #[serde(default, rename = "hiddenFromTabs")]
2088 #[ts(optional, rename = "hiddenFromTabs")]
2089 pub hidden_from_tabs: Option<bool>,
2090 #[serde(default)]
2092 #[ts(optional)]
2093 pub entries: Option<Vec<JsTextPropertyEntry>>,
2094}
2095
2096#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2098#[serde(deny_unknown_fields)]
2099#[ts(export)]
2100pub struct CreateVirtualBufferInSplitOptions {
2101 pub name: String,
2103 #[serde(default)]
2105 #[ts(optional)]
2106 pub mode: Option<String>,
2107 #[serde(default, rename = "readOnly")]
2109 #[ts(optional, rename = "readOnly")]
2110 pub read_only: Option<bool>,
2111 #[serde(default)]
2113 #[ts(optional)]
2114 pub ratio: Option<f32>,
2115 #[serde(default)]
2117 #[ts(optional)]
2118 pub direction: Option<String>,
2119 #[serde(default, rename = "panelId")]
2121 #[ts(optional, rename = "panelId")]
2122 pub panel_id: Option<String>,
2123 #[serde(default, rename = "showLineNumbers")]
2125 #[ts(optional, rename = "showLineNumbers")]
2126 pub show_line_numbers: Option<bool>,
2127 #[serde(default, rename = "showCursors")]
2129 #[ts(optional, rename = "showCursors")]
2130 pub show_cursors: Option<bool>,
2131 #[serde(default, rename = "editingDisabled")]
2133 #[ts(optional, rename = "editingDisabled")]
2134 pub editing_disabled: Option<bool>,
2135 #[serde(default, rename = "lineWrap")]
2137 #[ts(optional, rename = "lineWrap")]
2138 pub line_wrap: Option<bool>,
2139 #[serde(default)]
2141 #[ts(optional)]
2142 pub before: Option<bool>,
2143 #[serde(default)]
2145 #[ts(optional)]
2146 pub entries: Option<Vec<JsTextPropertyEntry>>,
2147}
2148
2149#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2151#[serde(deny_unknown_fields)]
2152#[ts(export)]
2153pub struct CreateVirtualBufferInExistingSplitOptions {
2154 pub name: String,
2156 #[serde(rename = "splitId")]
2158 #[ts(rename = "splitId")]
2159 pub split_id: usize,
2160 #[serde(default)]
2162 #[ts(optional)]
2163 pub mode: Option<String>,
2164 #[serde(default, rename = "readOnly")]
2166 #[ts(optional, rename = "readOnly")]
2167 pub read_only: Option<bool>,
2168 #[serde(default, rename = "showLineNumbers")]
2170 #[ts(optional, rename = "showLineNumbers")]
2171 pub show_line_numbers: Option<bool>,
2172 #[serde(default, rename = "showCursors")]
2174 #[ts(optional, rename = "showCursors")]
2175 pub show_cursors: Option<bool>,
2176 #[serde(default, rename = "editingDisabled")]
2178 #[ts(optional, rename = "editingDisabled")]
2179 pub editing_disabled: Option<bool>,
2180 #[serde(default, rename = "lineWrap")]
2182 #[ts(optional, rename = "lineWrap")]
2183 pub line_wrap: Option<bool>,
2184 #[serde(default)]
2186 #[ts(optional)]
2187 pub entries: Option<Vec<JsTextPropertyEntry>>,
2188}
2189
2190#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2192#[serde(deny_unknown_fields)]
2193#[ts(export)]
2194pub struct CreateTerminalOptions {
2195 #[serde(default)]
2197 #[ts(optional)]
2198 pub cwd: Option<String>,
2199 #[serde(default)]
2201 #[ts(optional)]
2202 pub direction: Option<String>,
2203 #[serde(default)]
2205 #[ts(optional)]
2206 pub ratio: Option<f32>,
2207 #[serde(default)]
2209 #[ts(optional)]
2210 pub focus: Option<bool>,
2211}
2212
2213#[derive(Debug, Clone, Serialize, TS)]
2218#[ts(export, type = "Array<Record<string, unknown>>")]
2219pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
2220
2221#[cfg(feature = "plugins")]
2223mod fromjs_impls {
2224 use super::*;
2225 use rquickjs::{Ctx, FromJs, Value};
2226
2227 impl<'js> FromJs<'js> for JsTextPropertyEntry {
2228 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2229 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2230 from: "object",
2231 to: "JsTextPropertyEntry",
2232 message: Some(e.to_string()),
2233 })
2234 }
2235 }
2236
2237 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
2238 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2239 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2240 from: "object",
2241 to: "CreateVirtualBufferOptions",
2242 message: Some(e.to_string()),
2243 })
2244 }
2245 }
2246
2247 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
2248 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2249 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2250 from: "object",
2251 to: "CreateVirtualBufferInSplitOptions",
2252 message: Some(e.to_string()),
2253 })
2254 }
2255 }
2256
2257 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
2258 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2259 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2260 from: "object",
2261 to: "CreateVirtualBufferInExistingSplitOptions",
2262 message: Some(e.to_string()),
2263 })
2264 }
2265 }
2266
2267 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
2268 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
2269 rquickjs_serde::to_value(ctx.clone(), &self.0)
2270 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
2271 }
2272 }
2273
2274 impl<'js> FromJs<'js> for ActionSpec {
2277 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2278 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2279 from: "object",
2280 to: "ActionSpec",
2281 message: Some(e.to_string()),
2282 })
2283 }
2284 }
2285
2286 impl<'js> FromJs<'js> for ActionPopupAction {
2287 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2288 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2289 from: "object",
2290 to: "ActionPopupAction",
2291 message: Some(e.to_string()),
2292 })
2293 }
2294 }
2295
2296 impl<'js> FromJs<'js> for ActionPopupOptions {
2297 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2298 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2299 from: "object",
2300 to: "ActionPopupOptions",
2301 message: Some(e.to_string()),
2302 })
2303 }
2304 }
2305
2306 impl<'js> FromJs<'js> for ViewTokenWire {
2307 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2308 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2309 from: "object",
2310 to: "ViewTokenWire",
2311 message: Some(e.to_string()),
2312 })
2313 }
2314 }
2315
2316 impl<'js> FromJs<'js> for ViewTokenStyle {
2317 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2318 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2319 from: "object",
2320 to: "ViewTokenStyle",
2321 message: Some(e.to_string()),
2322 })
2323 }
2324 }
2325
2326 impl<'js> FromJs<'js> for LayoutHints {
2327 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2328 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2329 from: "object",
2330 to: "LayoutHints",
2331 message: Some(e.to_string()),
2332 })
2333 }
2334 }
2335
2336 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
2337 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2338 let json: serde_json::Value =
2340 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2341 from: "object",
2342 to: "CreateCompositeBufferOptions (json)",
2343 message: Some(e.to_string()),
2344 })?;
2345 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
2346 from: "json",
2347 to: "CreateCompositeBufferOptions",
2348 message: Some(e.to_string()),
2349 })
2350 }
2351 }
2352
2353 impl<'js> FromJs<'js> for CompositeHunk {
2354 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2355 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2356 from: "object",
2357 to: "CompositeHunk",
2358 message: Some(e.to_string()),
2359 })
2360 }
2361 }
2362
2363 impl<'js> FromJs<'js> for LanguagePackConfig {
2364 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2365 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2366 from: "object",
2367 to: "LanguagePackConfig",
2368 message: Some(e.to_string()),
2369 })
2370 }
2371 }
2372
2373 impl<'js> FromJs<'js> for LspServerPackConfig {
2374 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2375 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2376 from: "object",
2377 to: "LspServerPackConfig",
2378 message: Some(e.to_string()),
2379 })
2380 }
2381 }
2382
2383 impl<'js> FromJs<'js> for ProcessLimitsPackConfig {
2384 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2385 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2386 from: "object",
2387 to: "ProcessLimitsPackConfig",
2388 message: Some(e.to_string()),
2389 })
2390 }
2391 }
2392
2393 impl<'js> FromJs<'js> for CreateTerminalOptions {
2394 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2395 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2396 from: "object",
2397 to: "CreateTerminalOptions",
2398 message: Some(e.to_string()),
2399 })
2400 }
2401 }
2402}
2403
2404pub struct PluginApi {
2406 hooks: Arc<RwLock<HookRegistry>>,
2408
2409 commands: Arc<RwLock<CommandRegistry>>,
2411
2412 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2414
2415 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2417}
2418
2419impl PluginApi {
2420 pub fn new(
2422 hooks: Arc<RwLock<HookRegistry>>,
2423 commands: Arc<RwLock<CommandRegistry>>,
2424 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2425 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2426 ) -> Self {
2427 Self {
2428 hooks,
2429 commands,
2430 command_sender,
2431 state_snapshot,
2432 }
2433 }
2434
2435 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
2437 let mut hooks = self.hooks.write().unwrap();
2438 hooks.add_hook(hook_name, callback);
2439 }
2440
2441 pub fn unregister_hooks(&self, hook_name: &str) {
2443 let mut hooks = self.hooks.write().unwrap();
2444 hooks.remove_hooks(hook_name);
2445 }
2446
2447 pub fn register_command(&self, command: Command) {
2449 let commands = self.commands.read().unwrap();
2450 commands.register(command);
2451 }
2452
2453 pub fn unregister_command(&self, name: &str) {
2455 let commands = self.commands.read().unwrap();
2456 commands.unregister(name);
2457 }
2458
2459 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
2461 self.command_sender
2462 .send(command)
2463 .map_err(|e| format!("Failed to send command: {}", e))
2464 }
2465
2466 pub fn insert_text(
2468 &self,
2469 buffer_id: BufferId,
2470 position: usize,
2471 text: String,
2472 ) -> Result<(), String> {
2473 self.send_command(PluginCommand::InsertText {
2474 buffer_id,
2475 position,
2476 text,
2477 })
2478 }
2479
2480 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
2482 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
2483 }
2484
2485 pub fn add_overlay(
2493 &self,
2494 buffer_id: BufferId,
2495 namespace: Option<String>,
2496 range: Range<usize>,
2497 options: OverlayOptions,
2498 ) -> Result<(), String> {
2499 self.send_command(PluginCommand::AddOverlay {
2500 buffer_id,
2501 namespace: namespace.map(OverlayNamespace::from_string),
2502 range,
2503 options,
2504 })
2505 }
2506
2507 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
2509 self.send_command(PluginCommand::RemoveOverlay {
2510 buffer_id,
2511 handle: OverlayHandle::from_string(handle),
2512 })
2513 }
2514
2515 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
2517 self.send_command(PluginCommand::ClearNamespace {
2518 buffer_id,
2519 namespace: OverlayNamespace::from_string(namespace),
2520 })
2521 }
2522
2523 pub fn clear_overlays_in_range(
2526 &self,
2527 buffer_id: BufferId,
2528 start: usize,
2529 end: usize,
2530 ) -> Result<(), String> {
2531 self.send_command(PluginCommand::ClearOverlaysInRange {
2532 buffer_id,
2533 start,
2534 end,
2535 })
2536 }
2537
2538 pub fn set_status(&self, message: String) -> Result<(), String> {
2540 self.send_command(PluginCommand::SetStatus { message })
2541 }
2542
2543 pub fn open_file_at_location(
2546 &self,
2547 path: PathBuf,
2548 line: Option<usize>,
2549 column: Option<usize>,
2550 ) -> Result<(), String> {
2551 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
2552 }
2553
2554 pub fn open_file_in_split(
2559 &self,
2560 split_id: usize,
2561 path: PathBuf,
2562 line: Option<usize>,
2563 column: Option<usize>,
2564 ) -> Result<(), String> {
2565 self.send_command(PluginCommand::OpenFileInSplit {
2566 split_id,
2567 path,
2568 line,
2569 column,
2570 })
2571 }
2572
2573 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
2576 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
2577 }
2578
2579 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
2582 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
2583 }
2584
2585 pub fn set_prompt_input_sync(&self, sync: bool) -> Result<(), String> {
2587 self.send_command(PluginCommand::SetPromptInputSync { sync })
2588 }
2589
2590 pub fn add_menu_item(
2592 &self,
2593 menu_label: String,
2594 item: MenuItem,
2595 position: MenuPosition,
2596 ) -> Result<(), String> {
2597 self.send_command(PluginCommand::AddMenuItem {
2598 menu_label,
2599 item,
2600 position,
2601 })
2602 }
2603
2604 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
2606 self.send_command(PluginCommand::AddMenu { menu, position })
2607 }
2608
2609 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
2611 self.send_command(PluginCommand::RemoveMenuItem {
2612 menu_label,
2613 item_label,
2614 })
2615 }
2616
2617 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
2619 self.send_command(PluginCommand::RemoveMenu { menu_label })
2620 }
2621
2622 pub fn create_virtual_buffer(
2629 &self,
2630 name: String,
2631 mode: String,
2632 read_only: bool,
2633 ) -> Result<(), String> {
2634 self.send_command(PluginCommand::CreateVirtualBuffer {
2635 name,
2636 mode,
2637 read_only,
2638 })
2639 }
2640
2641 pub fn create_virtual_buffer_with_content(
2647 &self,
2648 name: String,
2649 mode: String,
2650 read_only: bool,
2651 entries: Vec<TextPropertyEntry>,
2652 ) -> Result<(), String> {
2653 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
2654 name,
2655 mode,
2656 read_only,
2657 entries,
2658 show_line_numbers: true,
2659 show_cursors: true,
2660 editing_disabled: false,
2661 hidden_from_tabs: false,
2662 request_id: None,
2663 })
2664 }
2665
2666 pub fn set_virtual_buffer_content(
2670 &self,
2671 buffer_id: BufferId,
2672 entries: Vec<TextPropertyEntry>,
2673 ) -> Result<(), String> {
2674 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
2675 }
2676
2677 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
2681 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
2682 }
2683
2684 pub fn define_mode(
2688 &self,
2689 name: String,
2690 bindings: Vec<(String, String)>,
2691 read_only: bool,
2692 allow_text_input: bool,
2693 ) -> Result<(), String> {
2694 self.send_command(PluginCommand::DefineMode {
2695 name,
2696 bindings,
2697 read_only,
2698 allow_text_input,
2699 plugin_name: None,
2700 })
2701 }
2702
2703 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
2705 self.send_command(PluginCommand::ShowBuffer { buffer_id })
2706 }
2707
2708 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
2710 self.send_command(PluginCommand::SetSplitScroll {
2711 split_id: SplitId(split_id),
2712 top_byte,
2713 })
2714 }
2715
2716 pub fn get_highlights(
2718 &self,
2719 buffer_id: BufferId,
2720 range: Range<usize>,
2721 request_id: u64,
2722 ) -> Result<(), String> {
2723 self.send_command(PluginCommand::RequestHighlights {
2724 buffer_id,
2725 range,
2726 request_id,
2727 })
2728 }
2729
2730 pub fn get_active_buffer_id(&self) -> BufferId {
2734 let snapshot = self.state_snapshot.read().unwrap();
2735 snapshot.active_buffer_id
2736 }
2737
2738 pub fn get_active_split_id(&self) -> usize {
2740 let snapshot = self.state_snapshot.read().unwrap();
2741 snapshot.active_split_id
2742 }
2743
2744 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
2746 let snapshot = self.state_snapshot.read().unwrap();
2747 snapshot.buffers.get(&buffer_id).cloned()
2748 }
2749
2750 pub fn list_buffers(&self) -> Vec<BufferInfo> {
2752 let snapshot = self.state_snapshot.read().unwrap();
2753 snapshot.buffers.values().cloned().collect()
2754 }
2755
2756 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
2758 let snapshot = self.state_snapshot.read().unwrap();
2759 snapshot.primary_cursor.clone()
2760 }
2761
2762 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
2764 let snapshot = self.state_snapshot.read().unwrap();
2765 snapshot.all_cursors.clone()
2766 }
2767
2768 pub fn get_viewport(&self) -> Option<ViewportInfo> {
2770 let snapshot = self.state_snapshot.read().unwrap();
2771 snapshot.viewport.clone()
2772 }
2773
2774 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
2776 Arc::clone(&self.state_snapshot)
2777 }
2778}
2779
2780impl Clone for PluginApi {
2781 fn clone(&self) -> Self {
2782 Self {
2783 hooks: Arc::clone(&self.hooks),
2784 commands: Arc::clone(&self.commands),
2785 command_sender: self.command_sender.clone(),
2786 state_snapshot: Arc::clone(&self.state_snapshot),
2787 }
2788 }
2789}
2790
2791#[cfg(test)]
2792mod tests {
2793 use super::*;
2794
2795 #[test]
2796 fn test_plugin_api_creation() {
2797 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2798 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2799 let (tx, _rx) = std::sync::mpsc::channel();
2800 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2801
2802 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2803
2804 let _clone = api.clone();
2806 }
2807
2808 #[test]
2809 fn test_register_hook() {
2810 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2811 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2812 let (tx, _rx) = std::sync::mpsc::channel();
2813 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2814
2815 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
2816
2817 api.register_hook("test-hook", Box::new(|_| true));
2818
2819 let hook_registry = hooks.read().unwrap();
2820 assert_eq!(hook_registry.hook_count("test-hook"), 1);
2821 }
2822
2823 #[test]
2824 fn test_send_command() {
2825 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2826 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2827 let (tx, rx) = std::sync::mpsc::channel();
2828 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2829
2830 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2831
2832 let result = api.insert_text(BufferId(1), 0, "test".to_string());
2833 assert!(result.is_ok());
2834
2835 let received = rx.try_recv();
2837 assert!(received.is_ok());
2838
2839 match received.unwrap() {
2840 PluginCommand::InsertText {
2841 buffer_id,
2842 position,
2843 text,
2844 } => {
2845 assert_eq!(buffer_id.0, 1);
2846 assert_eq!(position, 0);
2847 assert_eq!(text, "test");
2848 }
2849 _ => panic!("Wrong command type"),
2850 }
2851 }
2852
2853 #[test]
2854 fn test_add_overlay_command() {
2855 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2856 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2857 let (tx, rx) = std::sync::mpsc::channel();
2858 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2859
2860 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2861
2862 let result = api.add_overlay(
2863 BufferId(1),
2864 Some("test-overlay".to_string()),
2865 0..10,
2866 OverlayOptions {
2867 fg: Some(OverlayColorSpec::ThemeKey("ui.status_bar_fg".to_string())),
2868 bg: None,
2869 underline: true,
2870 bold: false,
2871 italic: false,
2872 strikethrough: false,
2873 extend_to_line_end: false,
2874 url: None,
2875 },
2876 );
2877 assert!(result.is_ok());
2878
2879 let received = rx.try_recv().unwrap();
2880 match received {
2881 PluginCommand::AddOverlay {
2882 buffer_id,
2883 namespace,
2884 range,
2885 options,
2886 } => {
2887 assert_eq!(buffer_id.0, 1);
2888 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
2889 assert_eq!(range, 0..10);
2890 assert!(matches!(
2891 options.fg,
2892 Some(OverlayColorSpec::ThemeKey(ref k)) if k == "ui.status_bar_fg"
2893 ));
2894 assert!(options.bg.is_none());
2895 assert!(options.underline);
2896 assert!(!options.bold);
2897 assert!(!options.italic);
2898 assert!(!options.extend_to_line_end);
2899 }
2900 _ => panic!("Wrong command type"),
2901 }
2902 }
2903
2904 #[test]
2905 fn test_set_status_command() {
2906 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2907 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2908 let (tx, rx) = std::sync::mpsc::channel();
2909 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2910
2911 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2912
2913 let result = api.set_status("Test status".to_string());
2914 assert!(result.is_ok());
2915
2916 let received = rx.try_recv().unwrap();
2917 match received {
2918 PluginCommand::SetStatus { message } => {
2919 assert_eq!(message, "Test status");
2920 }
2921 _ => panic!("Wrong command type"),
2922 }
2923 }
2924
2925 #[test]
2926 fn test_get_active_buffer_id() {
2927 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2928 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2929 let (tx, _rx) = std::sync::mpsc::channel();
2930 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2931
2932 {
2934 let mut snapshot = state_snapshot.write().unwrap();
2935 snapshot.active_buffer_id = BufferId(5);
2936 }
2937
2938 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2939
2940 let active_id = api.get_active_buffer_id();
2941 assert_eq!(active_id.0, 5);
2942 }
2943
2944 #[test]
2945 fn test_get_buffer_info() {
2946 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2947 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2948 let (tx, _rx) = std::sync::mpsc::channel();
2949 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2950
2951 {
2953 let mut snapshot = state_snapshot.write().unwrap();
2954 let buffer_info = BufferInfo {
2955 id: BufferId(1),
2956 path: Some(std::path::PathBuf::from("/test/file.txt")),
2957 modified: true,
2958 length: 100,
2959 is_virtual: false,
2960 view_mode: "source".to_string(),
2961 is_composing_in_any_split: false,
2962 compose_width: None,
2963 language: "text".to_string(),
2964 };
2965 snapshot.buffers.insert(BufferId(1), buffer_info);
2966 }
2967
2968 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2969
2970 let info = api.get_buffer_info(BufferId(1));
2971 assert!(info.is_some());
2972 let info = info.unwrap();
2973 assert_eq!(info.id.0, 1);
2974 assert_eq!(
2975 info.path.as_ref().unwrap().to_str().unwrap(),
2976 "/test/file.txt"
2977 );
2978 assert!(info.modified);
2979 assert_eq!(info.length, 100);
2980
2981 let no_info = api.get_buffer_info(BufferId(999));
2983 assert!(no_info.is_none());
2984 }
2985
2986 #[test]
2987 fn test_list_buffers() {
2988 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2989 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2990 let (tx, _rx) = std::sync::mpsc::channel();
2991 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2992
2993 {
2995 let mut snapshot = state_snapshot.write().unwrap();
2996 snapshot.buffers.insert(
2997 BufferId(1),
2998 BufferInfo {
2999 id: BufferId(1),
3000 path: Some(std::path::PathBuf::from("/file1.txt")),
3001 modified: false,
3002 length: 50,
3003 is_virtual: false,
3004 view_mode: "source".to_string(),
3005 is_composing_in_any_split: false,
3006 compose_width: None,
3007 language: "text".to_string(),
3008 },
3009 );
3010 snapshot.buffers.insert(
3011 BufferId(2),
3012 BufferInfo {
3013 id: BufferId(2),
3014 path: Some(std::path::PathBuf::from("/file2.txt")),
3015 modified: true,
3016 length: 100,
3017 is_virtual: false,
3018 view_mode: "source".to_string(),
3019 is_composing_in_any_split: false,
3020 compose_width: None,
3021 language: "text".to_string(),
3022 },
3023 );
3024 snapshot.buffers.insert(
3025 BufferId(3),
3026 BufferInfo {
3027 id: BufferId(3),
3028 path: None,
3029 modified: false,
3030 length: 0,
3031 is_virtual: true,
3032 view_mode: "source".to_string(),
3033 is_composing_in_any_split: false,
3034 compose_width: None,
3035 language: "text".to_string(),
3036 },
3037 );
3038 }
3039
3040 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3041
3042 let buffers = api.list_buffers();
3043 assert_eq!(buffers.len(), 3);
3044
3045 assert!(buffers.iter().any(|b| b.id.0 == 1));
3047 assert!(buffers.iter().any(|b| b.id.0 == 2));
3048 assert!(buffers.iter().any(|b| b.id.0 == 3));
3049 }
3050
3051 #[test]
3052 fn test_get_primary_cursor() {
3053 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3054 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3055 let (tx, _rx) = std::sync::mpsc::channel();
3056 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3057
3058 {
3060 let mut snapshot = state_snapshot.write().unwrap();
3061 snapshot.primary_cursor = Some(CursorInfo {
3062 position: 42,
3063 selection: Some(10..42),
3064 });
3065 }
3066
3067 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3068
3069 let cursor = api.get_primary_cursor();
3070 assert!(cursor.is_some());
3071 let cursor = cursor.unwrap();
3072 assert_eq!(cursor.position, 42);
3073 assert_eq!(cursor.selection, Some(10..42));
3074 }
3075
3076 #[test]
3077 fn test_get_all_cursors() {
3078 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3079 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3080 let (tx, _rx) = std::sync::mpsc::channel();
3081 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3082
3083 {
3085 let mut snapshot = state_snapshot.write().unwrap();
3086 snapshot.all_cursors = vec![
3087 CursorInfo {
3088 position: 10,
3089 selection: None,
3090 },
3091 CursorInfo {
3092 position: 20,
3093 selection: Some(15..20),
3094 },
3095 CursorInfo {
3096 position: 30,
3097 selection: Some(25..30),
3098 },
3099 ];
3100 }
3101
3102 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3103
3104 let cursors = api.get_all_cursors();
3105 assert_eq!(cursors.len(), 3);
3106 assert_eq!(cursors[0].position, 10);
3107 assert_eq!(cursors[0].selection, None);
3108 assert_eq!(cursors[1].position, 20);
3109 assert_eq!(cursors[1].selection, Some(15..20));
3110 assert_eq!(cursors[2].position, 30);
3111 assert_eq!(cursors[2].selection, Some(25..30));
3112 }
3113
3114 #[test]
3115 fn test_get_viewport() {
3116 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3117 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3118 let (tx, _rx) = std::sync::mpsc::channel();
3119 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3120
3121 {
3123 let mut snapshot = state_snapshot.write().unwrap();
3124 snapshot.viewport = Some(ViewportInfo {
3125 top_byte: 100,
3126 top_line: Some(5),
3127 left_column: 5,
3128 width: 80,
3129 height: 24,
3130 });
3131 }
3132
3133 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3134
3135 let viewport = api.get_viewport();
3136 assert!(viewport.is_some());
3137 let viewport = viewport.unwrap();
3138 assert_eq!(viewport.top_byte, 100);
3139 assert_eq!(viewport.left_column, 5);
3140 assert_eq!(viewport.width, 80);
3141 assert_eq!(viewport.height, 24);
3142 }
3143
3144 #[test]
3145 fn test_composite_buffer_options_rejects_unknown_fields() {
3146 let valid_json = r#"{
3148 "name": "test",
3149 "mode": "diff",
3150 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3151 "sources": [{"bufferId": 1, "label": "old"}]
3152 }"#;
3153 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
3154 assert!(
3155 result.is_ok(),
3156 "Valid JSON should parse: {:?}",
3157 result.err()
3158 );
3159
3160 let invalid_json = r#"{
3162 "name": "test",
3163 "mode": "diff",
3164 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3165 "sources": [{"buffer_id": 1, "label": "old"}]
3166 }"#;
3167 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
3168 assert!(
3169 result.is_err(),
3170 "JSON with unknown field should fail to parse"
3171 );
3172 let err = result.unwrap_err().to_string();
3173 assert!(
3174 err.contains("unknown field") || err.contains("buffer_id"),
3175 "Error should mention unknown field: {}",
3176 err
3177 );
3178 }
3179
3180 #[test]
3181 fn test_composite_hunk_rejects_unknown_fields() {
3182 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3184 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
3185 assert!(
3186 result.is_ok(),
3187 "Valid JSON should parse: {:?}",
3188 result.err()
3189 );
3190
3191 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3193 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
3194 assert!(
3195 result.is_err(),
3196 "JSON with unknown field should fail to parse"
3197 );
3198 let err = result.unwrap_err().to_string();
3199 assert!(
3200 err.contains("unknown field") || err.contains("old_start"),
3201 "Error should mention unknown field: {}",
3202 err
3203 );
3204 }
3205
3206 #[test]
3207 fn test_plugin_response_line_end_position() {
3208 let response = PluginResponse::LineEndPosition {
3209 request_id: 42,
3210 position: Some(100),
3211 };
3212 let json = serde_json::to_string(&response).unwrap();
3213 assert!(json.contains("LineEndPosition"));
3214 assert!(json.contains("42"));
3215 assert!(json.contains("100"));
3216
3217 let response_none = PluginResponse::LineEndPosition {
3219 request_id: 1,
3220 position: None,
3221 };
3222 let json_none = serde_json::to_string(&response_none).unwrap();
3223 assert!(json_none.contains("null"));
3224 }
3225
3226 #[test]
3227 fn test_plugin_response_buffer_line_count() {
3228 let response = PluginResponse::BufferLineCount {
3229 request_id: 99,
3230 count: Some(500),
3231 };
3232 let json = serde_json::to_string(&response).unwrap();
3233 assert!(json.contains("BufferLineCount"));
3234 assert!(json.contains("99"));
3235 assert!(json.contains("500"));
3236 }
3237
3238 #[test]
3239 fn test_plugin_command_get_line_end_position() {
3240 let command = PluginCommand::GetLineEndPosition {
3241 buffer_id: BufferId(1),
3242 line: 10,
3243 request_id: 123,
3244 };
3245 let json = serde_json::to_string(&command).unwrap();
3246 assert!(json.contains("GetLineEndPosition"));
3247 assert!(json.contains("10"));
3248 }
3249
3250 #[test]
3251 fn test_plugin_command_get_buffer_line_count() {
3252 let command = PluginCommand::GetBufferLineCount {
3253 buffer_id: BufferId(0),
3254 request_id: 456,
3255 };
3256 let json = serde_json::to_string(&command).unwrap();
3257 assert!(json.contains("GetBufferLineCount"));
3258 assert!(json.contains("456"));
3259 }
3260
3261 #[test]
3262 fn test_plugin_command_scroll_to_line_center() {
3263 let command = PluginCommand::ScrollToLineCenter {
3264 split_id: SplitId(1),
3265 buffer_id: BufferId(2),
3266 line: 50,
3267 };
3268 let json = serde_json::to_string(&command).unwrap();
3269 assert!(json.contains("ScrollToLineCenter"));
3270 assert!(json.contains("50"));
3271 }
3272}