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
271#[derive(Debug, Clone, Serialize, Deserialize, TS)]
273#[ts(export)]
274pub struct CursorInfo {
275 pub position: usize,
277 #[cfg_attr(
279 feature = "plugins",
280 ts(type = "{ start: number; end: number } | null")
281 )]
282 pub selection: Option<Range<usize>>,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize, TS)]
287#[serde(deny_unknown_fields)]
288#[ts(export)]
289pub struct ActionSpec {
290 pub action: String,
292 #[serde(default = "default_action_count")]
294 pub count: u32,
295}
296
297fn default_action_count() -> u32 {
298 1
299}
300
301#[derive(Debug, Clone, Serialize, Deserialize, TS)]
303#[ts(export)]
304pub struct BufferInfo {
305 #[ts(type = "number")]
307 pub id: BufferId,
308 #[serde(serialize_with = "serialize_path")]
310 #[ts(type = "string")]
311 pub path: Option<PathBuf>,
312 pub modified: bool,
314 pub length: usize,
316 pub is_virtual: bool,
318 pub view_mode: String,
320 pub is_composing_in_any_split: bool,
325 pub compose_width: Option<u16>,
327}
328
329fn serialize_path<S: serde::Serializer>(path: &Option<PathBuf>, s: S) -> Result<S::Ok, S::Error> {
330 s.serialize_str(
331 &path
332 .as_ref()
333 .map(|p| p.to_string_lossy().to_string())
334 .unwrap_or_default(),
335 )
336}
337
338fn serialize_ranges_as_tuples<S>(ranges: &[Range<usize>], serializer: S) -> Result<S::Ok, S::Error>
340where
341 S: serde::Serializer,
342{
343 use serde::ser::SerializeSeq;
344 let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
345 for range in ranges {
346 seq.serialize_element(&(range.start, range.end))?;
347 }
348 seq.end()
349}
350
351fn serialize_opt_ranges_as_tuples<S>(
353 ranges: &Option<Vec<Range<usize>>>,
354 serializer: S,
355) -> Result<S::Ok, S::Error>
356where
357 S: serde::Serializer,
358{
359 match ranges {
360 Some(ranges) => {
361 use serde::ser::SerializeSeq;
362 let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
363 for range in ranges {
364 seq.serialize_element(&(range.start, range.end))?;
365 }
366 seq.end()
367 }
368 None => serializer.serialize_none(),
369 }
370}
371
372#[derive(Debug, Clone, Serialize, Deserialize, TS)]
374#[ts(export)]
375pub struct BufferSavedDiff {
376 pub equal: bool,
377 #[serde(serialize_with = "serialize_ranges_as_tuples")]
378 #[ts(type = "Array<[number, number]>")]
379 pub byte_ranges: Vec<Range<usize>>,
380 #[serde(serialize_with = "serialize_opt_ranges_as_tuples")]
381 #[ts(type = "Array<[number, number]> | null")]
382 pub line_ranges: Option<Vec<Range<usize>>>,
383}
384
385#[derive(Debug, Clone, Serialize, Deserialize, TS)]
387#[serde(rename_all = "camelCase")]
388#[ts(export, rename_all = "camelCase")]
389pub struct ViewportInfo {
390 pub top_byte: 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 pub compose_width: Option<u16>,
407 pub column_guides: Option<Vec<u16>>,
409}
410
411#[derive(Debug, Clone, Serialize, Deserialize, TS)]
426#[serde(untagged)]
427#[ts(export)]
428pub enum OverlayColorSpec {
429 #[ts(type = "[number, number, number]")]
431 Rgb(u8, u8, u8),
432 ThemeKey(String),
434}
435
436impl OverlayColorSpec {
437 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
439 Self::Rgb(r, g, b)
440 }
441
442 pub fn theme_key(key: impl Into<String>) -> Self {
444 Self::ThemeKey(key.into())
445 }
446
447 pub fn as_rgb(&self) -> Option<(u8, u8, u8)> {
449 match self {
450 Self::Rgb(r, g, b) => Some((*r, *g, *b)),
451 Self::ThemeKey(_) => None,
452 }
453 }
454
455 pub fn as_theme_key(&self) -> Option<&str> {
457 match self {
458 Self::ThemeKey(key) => Some(key),
459 Self::Rgb(_, _, _) => None,
460 }
461 }
462}
463
464#[derive(Debug, Clone, Serialize, Deserialize, TS)]
469#[serde(deny_unknown_fields, rename_all = "camelCase")]
470#[ts(export, rename_all = "camelCase")]
471pub struct OverlayOptions {
472 #[serde(default, skip_serializing_if = "Option::is_none")]
474 pub fg: Option<OverlayColorSpec>,
475
476 #[serde(default, skip_serializing_if = "Option::is_none")]
478 pub bg: Option<OverlayColorSpec>,
479
480 #[serde(default)]
482 pub underline: bool,
483
484 #[serde(default)]
486 pub bold: bool,
487
488 #[serde(default)]
490 pub italic: bool,
491
492 #[serde(default)]
494 pub strikethrough: bool,
495
496 #[serde(default)]
498 pub extend_to_line_end: bool,
499
500 #[serde(default, skip_serializing_if = "Option::is_none")]
504 pub url: Option<String>,
505}
506
507impl Default for OverlayOptions {
508 fn default() -> Self {
509 Self {
510 fg: None,
511 bg: None,
512 underline: false,
513 bold: false,
514 italic: false,
515 strikethrough: false,
516 extend_to_line_end: false,
517 url: None,
518 }
519 }
520}
521
522#[derive(Debug, Clone, Serialize, Deserialize, TS)]
528#[serde(deny_unknown_fields)]
529#[ts(export, rename = "TsCompositeLayoutConfig")]
530pub struct CompositeLayoutConfig {
531 #[serde(rename = "type")]
533 #[ts(rename = "type")]
534 pub layout_type: String,
535 #[serde(default)]
537 pub ratios: Option<Vec<f32>>,
538 #[serde(default = "default_true", rename = "showSeparator")]
540 #[ts(rename = "showSeparator")]
541 pub show_separator: bool,
542 #[serde(default)]
544 pub spacing: Option<u16>,
545}
546
547fn default_true() -> bool {
548 true
549}
550
551#[derive(Debug, Clone, Serialize, Deserialize, TS)]
553#[serde(deny_unknown_fields)]
554#[ts(export, rename = "TsCompositeSourceConfig")]
555pub struct CompositeSourceConfig {
556 #[serde(rename = "bufferId")]
558 #[ts(rename = "bufferId")]
559 pub buffer_id: usize,
560 pub label: String,
562 #[serde(default)]
564 pub editable: bool,
565 #[serde(default)]
567 pub style: Option<CompositePaneStyle>,
568}
569
570#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
572#[serde(deny_unknown_fields)]
573#[ts(export, rename = "TsCompositePaneStyle")]
574pub struct CompositePaneStyle {
575 #[serde(default, rename = "addBg")]
578 #[ts(rename = "addBg", type = "[number, number, number] | null")]
579 pub add_bg: Option<[u8; 3]>,
580 #[serde(default, rename = "removeBg")]
582 #[ts(rename = "removeBg", type = "[number, number, number] | null")]
583 pub remove_bg: Option<[u8; 3]>,
584 #[serde(default, rename = "modifyBg")]
586 #[ts(rename = "modifyBg", type = "[number, number, number] | null")]
587 pub modify_bg: Option<[u8; 3]>,
588 #[serde(default, rename = "gutterStyle")]
590 #[ts(rename = "gutterStyle")]
591 pub gutter_style: Option<String>,
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize, TS)]
596#[serde(deny_unknown_fields)]
597#[ts(export, rename = "TsCompositeHunk")]
598pub struct CompositeHunk {
599 #[serde(rename = "oldStart")]
601 #[ts(rename = "oldStart")]
602 pub old_start: usize,
603 #[serde(rename = "oldCount")]
605 #[ts(rename = "oldCount")]
606 pub old_count: usize,
607 #[serde(rename = "newStart")]
609 #[ts(rename = "newStart")]
610 pub new_start: usize,
611 #[serde(rename = "newCount")]
613 #[ts(rename = "newCount")]
614 pub new_count: usize,
615}
616
617#[derive(Debug, Clone, Serialize, Deserialize, TS)]
619#[serde(deny_unknown_fields)]
620#[ts(export, rename = "TsCreateCompositeBufferOptions")]
621pub struct CreateCompositeBufferOptions {
622 #[serde(default)]
624 pub name: String,
625 #[serde(default)]
627 pub mode: String,
628 pub layout: CompositeLayoutConfig,
630 pub sources: Vec<CompositeSourceConfig>,
632 #[serde(default)]
634 pub hunks: Option<Vec<CompositeHunk>>,
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize, TS)]
639#[ts(export)]
640pub enum ViewTokenWireKind {
641 Text(String),
642 Newline,
643 Space,
644 Break,
647 BinaryByte(u8),
651}
652
653#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
659#[serde(deny_unknown_fields)]
660#[ts(export)]
661pub struct ViewTokenStyle {
662 #[serde(default)]
664 #[ts(type = "[number, number, number] | null")]
665 pub fg: Option<(u8, u8, u8)>,
666 #[serde(default)]
668 #[ts(type = "[number, number, number] | null")]
669 pub bg: Option<(u8, u8, u8)>,
670 #[serde(default)]
672 pub bold: bool,
673 #[serde(default)]
675 pub italic: bool,
676}
677
678#[derive(Debug, Clone, Serialize, Deserialize, TS)]
680#[serde(deny_unknown_fields)]
681#[ts(export)]
682pub struct ViewTokenWire {
683 #[ts(type = "number | null")]
685 pub source_offset: Option<usize>,
686 pub kind: ViewTokenWireKind,
688 #[serde(default)]
690 #[ts(optional)]
691 pub style: Option<ViewTokenStyle>,
692}
693
694#[derive(Debug, Clone, Serialize, Deserialize, TS)]
696#[ts(export)]
697pub struct ViewTransformPayload {
698 pub range: Range<usize>,
700 pub tokens: Vec<ViewTokenWire>,
702 pub layout_hints: Option<LayoutHints>,
704}
705
706#[derive(Debug, Clone, Serialize, Deserialize, TS)]
709#[ts(export)]
710pub struct EditorStateSnapshot {
711 pub active_buffer_id: BufferId,
713 pub active_split_id: usize,
715 pub buffers: HashMap<BufferId, BufferInfo>,
717 pub buffer_saved_diffs: HashMap<BufferId, BufferSavedDiff>,
719 pub primary_cursor: Option<CursorInfo>,
721 pub all_cursors: Vec<CursorInfo>,
723 pub viewport: Option<ViewportInfo>,
725 pub buffer_cursor_positions: HashMap<BufferId, usize>,
727 pub buffer_text_properties: HashMap<BufferId, Vec<TextProperty>>,
729 pub selected_text: Option<String>,
732 pub clipboard: String,
734 pub working_dir: PathBuf,
736 #[ts(type = "any")]
739 pub diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
740 #[ts(type = "any")]
743 pub config: serde_json::Value,
744 #[ts(type = "any")]
747 pub user_config: serde_json::Value,
748 pub editor_mode: Option<String>,
751
752 #[ts(type = "any")]
756 pub plugin_view_states: HashMap<BufferId, HashMap<String, serde_json::Value>>,
757
758 #[serde(skip)]
761 #[ts(skip)]
762 pub plugin_view_states_split: usize,
763}
764
765impl EditorStateSnapshot {
766 pub fn new() -> Self {
767 Self {
768 active_buffer_id: BufferId(0),
769 active_split_id: 0,
770 buffers: HashMap::new(),
771 buffer_saved_diffs: HashMap::new(),
772 primary_cursor: None,
773 all_cursors: Vec::new(),
774 viewport: None,
775 buffer_cursor_positions: HashMap::new(),
776 buffer_text_properties: HashMap::new(),
777 selected_text: None,
778 clipboard: String::new(),
779 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
780 diagnostics: HashMap::new(),
781 config: serde_json::Value::Null,
782 user_config: serde_json::Value::Null,
783 editor_mode: None,
784 plugin_view_states: HashMap::new(),
785 plugin_view_states_split: 0,
786 }
787 }
788}
789
790impl Default for EditorStateSnapshot {
791 fn default() -> Self {
792 Self::new()
793 }
794}
795
796#[derive(Debug, Clone, Serialize, Deserialize, TS)]
798#[ts(export)]
799pub enum MenuPosition {
800 Top,
802 Bottom,
804 Before(String),
806 After(String),
808}
809
810#[derive(Debug, Clone, Serialize, Deserialize, TS)]
812#[ts(export)]
813pub enum PluginCommand {
814 InsertText {
816 buffer_id: BufferId,
817 position: usize,
818 text: String,
819 },
820
821 DeleteRange {
823 buffer_id: BufferId,
824 range: Range<usize>,
825 },
826
827 AddOverlay {
832 buffer_id: BufferId,
833 namespace: Option<OverlayNamespace>,
834 range: Range<usize>,
835 options: OverlayOptions,
837 },
838
839 RemoveOverlay {
841 buffer_id: BufferId,
842 handle: OverlayHandle,
843 },
844
845 SetStatus { message: String },
847
848 ApplyTheme { theme_name: String },
850
851 ReloadConfig,
854
855 RegisterCommand { command: Command },
857
858 UnregisterCommand { name: String },
860
861 OpenFileInBackground { path: PathBuf },
863
864 InsertAtCursor { text: String },
866
867 SpawnProcess {
869 command: String,
870 args: Vec<String>,
871 cwd: Option<String>,
872 callback_id: JsCallbackId,
873 },
874
875 Delay {
877 callback_id: JsCallbackId,
878 duration_ms: u64,
879 },
880
881 SpawnBackgroundProcess {
885 process_id: u64,
887 command: String,
889 args: Vec<String>,
891 cwd: Option<String>,
893 callback_id: JsCallbackId,
895 },
896
897 KillBackgroundProcess { process_id: u64 },
899
900 SpawnProcessWait {
903 process_id: u64,
905 callback_id: JsCallbackId,
907 },
908
909 SetLayoutHints {
911 buffer_id: BufferId,
912 split_id: Option<SplitId>,
913 range: Range<usize>,
914 hints: LayoutHints,
915 },
916
917 SetLineNumbers { buffer_id: BufferId, enabled: bool },
919
920 SetViewMode { buffer_id: BufferId, mode: String },
922
923 SetLineWrap {
925 buffer_id: BufferId,
926 split_id: Option<SplitId>,
927 enabled: bool,
928 },
929
930 SubmitViewTransform {
932 buffer_id: BufferId,
933 split_id: Option<SplitId>,
934 payload: ViewTransformPayload,
935 },
936
937 ClearViewTransform {
939 buffer_id: BufferId,
940 split_id: Option<SplitId>,
941 },
942
943 SetViewState {
946 buffer_id: BufferId,
947 key: String,
948 #[ts(type = "any")]
949 value: Option<serde_json::Value>,
950 },
951
952 ClearAllOverlays { buffer_id: BufferId },
954
955 ClearNamespace {
957 buffer_id: BufferId,
958 namespace: OverlayNamespace,
959 },
960
961 ClearOverlaysInRange {
964 buffer_id: BufferId,
965 start: usize,
966 end: usize,
967 },
968
969 AddVirtualText {
972 buffer_id: BufferId,
973 virtual_text_id: String,
974 position: usize,
975 text: String,
976 color: (u8, u8, u8),
977 use_bg: bool, before: bool, },
980
981 RemoveVirtualText {
983 buffer_id: BufferId,
984 virtual_text_id: String,
985 },
986
987 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
989
990 ClearVirtualTexts { buffer_id: BufferId },
992
993 AddVirtualLine {
997 buffer_id: BufferId,
998 position: usize,
1000 text: String,
1002 fg_color: (u8, u8, u8),
1004 bg_color: Option<(u8, u8, u8)>,
1006 above: bool,
1008 namespace: String,
1010 priority: i32,
1012 },
1013
1014 ClearVirtualTextNamespace {
1017 buffer_id: BufferId,
1018 namespace: String,
1019 },
1020
1021 AddConceal {
1024 buffer_id: BufferId,
1025 namespace: OverlayNamespace,
1027 start: usize,
1029 end: usize,
1030 replacement: Option<String>,
1032 },
1033
1034 ClearConcealNamespace {
1036 buffer_id: BufferId,
1037 namespace: OverlayNamespace,
1038 },
1039
1040 ClearConcealsInRange {
1043 buffer_id: BufferId,
1044 start: usize,
1045 end: usize,
1046 },
1047
1048 AddSoftBreak {
1052 buffer_id: BufferId,
1053 namespace: OverlayNamespace,
1055 position: usize,
1057 indent: u16,
1059 },
1060
1061 ClearSoftBreakNamespace {
1063 buffer_id: BufferId,
1064 namespace: OverlayNamespace,
1065 },
1066
1067 ClearSoftBreaksInRange {
1069 buffer_id: BufferId,
1070 start: usize,
1071 end: usize,
1072 },
1073
1074 RefreshLines { buffer_id: BufferId },
1076
1077 RefreshAllLines,
1081
1082 HookCompleted { hook_name: String },
1086
1087 SetLineIndicator {
1090 buffer_id: BufferId,
1091 line: usize,
1093 namespace: String,
1095 symbol: String,
1097 color: (u8, u8, u8),
1099 priority: i32,
1101 },
1102
1103 ClearLineIndicators {
1105 buffer_id: BufferId,
1106 namespace: String,
1108 },
1109
1110 SetFileExplorerDecorations {
1112 namespace: String,
1114 decorations: Vec<FileExplorerDecoration>,
1116 },
1117
1118 ClearFileExplorerDecorations {
1120 namespace: String,
1122 },
1123
1124 OpenFileAtLocation {
1127 path: PathBuf,
1128 line: Option<usize>, column: Option<usize>, },
1131
1132 OpenFileInSplit {
1135 split_id: usize,
1136 path: PathBuf,
1137 line: Option<usize>, column: Option<usize>, },
1140
1141 StartPrompt {
1144 label: String,
1145 prompt_type: String, },
1147
1148 StartPromptWithInitial {
1150 label: String,
1151 prompt_type: String,
1152 initial_value: String,
1153 },
1154
1155 StartPromptAsync {
1158 label: String,
1159 initial_value: String,
1160 callback_id: JsCallbackId,
1161 },
1162
1163 SetPromptSuggestions { suggestions: Vec<Suggestion> },
1166
1167 SetPromptInputSync { sync: bool },
1169
1170 AddMenuItem {
1173 menu_label: String,
1174 item: MenuItem,
1175 position: MenuPosition,
1176 },
1177
1178 AddMenu { menu: Menu, position: MenuPosition },
1180
1181 RemoveMenuItem {
1183 menu_label: String,
1184 item_label: String,
1185 },
1186
1187 RemoveMenu { menu_label: String },
1189
1190 CreateVirtualBuffer {
1192 name: String,
1194 mode: String,
1196 read_only: bool,
1198 },
1199
1200 CreateVirtualBufferWithContent {
1204 name: String,
1206 mode: String,
1208 read_only: bool,
1210 entries: Vec<TextPropertyEntry>,
1212 show_line_numbers: bool,
1214 show_cursors: bool,
1216 editing_disabled: bool,
1218 hidden_from_tabs: bool,
1220 request_id: Option<u64>,
1222 },
1223
1224 CreateVirtualBufferInSplit {
1227 name: String,
1229 mode: String,
1231 read_only: bool,
1233 entries: Vec<TextPropertyEntry>,
1235 ratio: f32,
1237 direction: Option<String>,
1239 panel_id: Option<String>,
1241 show_line_numbers: bool,
1243 show_cursors: bool,
1245 editing_disabled: bool,
1247 line_wrap: Option<bool>,
1249 before: bool,
1251 request_id: Option<u64>,
1253 },
1254
1255 SetVirtualBufferContent {
1257 buffer_id: BufferId,
1258 entries: Vec<TextPropertyEntry>,
1260 },
1261
1262 GetTextPropertiesAtCursor { buffer_id: BufferId },
1264
1265 DefineMode {
1267 name: String,
1268 parent: Option<String>,
1269 bindings: Vec<(String, String)>, read_only: bool,
1271 },
1272
1273 ShowBuffer { buffer_id: BufferId },
1275
1276 CreateVirtualBufferInExistingSplit {
1278 name: String,
1280 mode: String,
1282 read_only: bool,
1284 entries: Vec<TextPropertyEntry>,
1286 split_id: SplitId,
1288 show_line_numbers: bool,
1290 show_cursors: bool,
1292 editing_disabled: bool,
1294 line_wrap: Option<bool>,
1296 request_id: Option<u64>,
1298 },
1299
1300 CloseBuffer { buffer_id: BufferId },
1302
1303 CreateCompositeBuffer {
1306 name: String,
1308 mode: String,
1310 layout: CompositeLayoutConfig,
1312 sources: Vec<CompositeSourceConfig>,
1314 hunks: Option<Vec<CompositeHunk>>,
1316 request_id: Option<u64>,
1318 },
1319
1320 UpdateCompositeAlignment {
1322 buffer_id: BufferId,
1323 hunks: Vec<CompositeHunk>,
1324 },
1325
1326 CloseCompositeBuffer { buffer_id: BufferId },
1328
1329 FocusSplit { split_id: SplitId },
1331
1332 SetSplitBuffer {
1334 split_id: SplitId,
1335 buffer_id: BufferId,
1336 },
1337
1338 SetSplitScroll { split_id: SplitId, top_byte: usize },
1340
1341 RequestHighlights {
1343 buffer_id: BufferId,
1344 range: Range<usize>,
1345 request_id: u64,
1346 },
1347
1348 CloseSplit { split_id: SplitId },
1350
1351 SetSplitRatio {
1353 split_id: SplitId,
1354 ratio: f32,
1356 },
1357
1358 SetSplitLabel { split_id: SplitId, label: String },
1360
1361 ClearSplitLabel { split_id: SplitId },
1363
1364 GetSplitByLabel { label: String, request_id: u64 },
1366
1367 DistributeSplitsEvenly {
1369 split_ids: Vec<SplitId>,
1371 },
1372
1373 SetBufferCursor {
1375 buffer_id: BufferId,
1376 position: usize,
1378 },
1379
1380 SendLspRequest {
1382 language: String,
1383 method: String,
1384 #[ts(type = "any")]
1385 params: Option<JsonValue>,
1386 request_id: u64,
1387 },
1388
1389 SetClipboard { text: String },
1391
1392 DeleteSelection,
1395
1396 SetContext {
1400 name: String,
1402 active: bool,
1404 },
1405
1406 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1408
1409 ExecuteAction {
1412 action_name: String,
1414 },
1415
1416 ExecuteActions {
1420 actions: Vec<ActionSpec>,
1422 },
1423
1424 GetBufferText {
1426 buffer_id: BufferId,
1428 start: usize,
1430 end: usize,
1432 request_id: u64,
1434 },
1435
1436 GetLineStartPosition {
1439 buffer_id: BufferId,
1441 line: u32,
1443 request_id: u64,
1445 },
1446
1447 GetLineEndPosition {
1451 buffer_id: BufferId,
1453 line: u32,
1455 request_id: u64,
1457 },
1458
1459 GetBufferLineCount {
1461 buffer_id: BufferId,
1463 request_id: u64,
1465 },
1466
1467 ScrollToLineCenter {
1470 split_id: SplitId,
1472 buffer_id: BufferId,
1474 line: usize,
1476 },
1477
1478 SetEditorMode {
1481 mode: Option<String>,
1483 },
1484
1485 ShowActionPopup {
1488 popup_id: String,
1490 title: String,
1492 message: String,
1494 actions: Vec<ActionPopupAction>,
1496 },
1497
1498 DisableLspForLanguage {
1500 language: String,
1502 },
1503
1504 SetLspRootUri {
1508 language: String,
1510 uri: String,
1512 },
1513
1514 CreateScrollSyncGroup {
1518 group_id: u32,
1520 left_split: SplitId,
1522 right_split: SplitId,
1524 },
1525
1526 SetScrollSyncAnchors {
1529 group_id: u32,
1531 anchors: Vec<(usize, usize)>,
1533 },
1534
1535 RemoveScrollSyncGroup {
1537 group_id: u32,
1539 },
1540
1541 SaveBufferToPath {
1544 buffer_id: BufferId,
1546 path: PathBuf,
1548 },
1549
1550 LoadPlugin {
1553 path: PathBuf,
1555 callback_id: JsCallbackId,
1557 },
1558
1559 UnloadPlugin {
1562 name: String,
1564 callback_id: JsCallbackId,
1566 },
1567
1568 ReloadPlugin {
1571 name: String,
1573 callback_id: JsCallbackId,
1575 },
1576
1577 ListPlugins {
1580 callback_id: JsCallbackId,
1582 },
1583
1584 ReloadThemes,
1587
1588 RegisterGrammar {
1591 language: String,
1593 grammar_path: String,
1595 extensions: Vec<String>,
1597 },
1598
1599 RegisterLanguageConfig {
1602 language: String,
1604 config: LanguagePackConfig,
1606 },
1607
1608 RegisterLspServer {
1611 language: String,
1613 config: LspServerPackConfig,
1615 },
1616
1617 ReloadGrammars,
1620
1621 CreateTerminal {
1625 cwd: Option<String>,
1627 direction: Option<String>,
1629 ratio: Option<f32>,
1631 focus: Option<bool>,
1633 request_id: u64,
1635 },
1636
1637 SendTerminalInput {
1639 terminal_id: TerminalId,
1641 data: String,
1643 },
1644
1645 CloseTerminal {
1647 terminal_id: TerminalId,
1649 },
1650}
1651
1652impl PluginCommand {
1653 pub fn debug_variant_name(&self) -> String {
1655 let dbg = format!("{:?}", self);
1656 dbg.split(|ch: char| ch == ' ' || ch == '{' || ch == '(')
1657 .next()
1658 .unwrap_or("?")
1659 .to_string()
1660 }
1661}
1662
1663#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
1672#[serde(rename_all = "camelCase")]
1673#[ts(export)]
1674pub struct LanguagePackConfig {
1675 #[serde(default)]
1677 pub comment_prefix: Option<String>,
1678
1679 #[serde(default)]
1681 pub block_comment_start: Option<String>,
1682
1683 #[serde(default)]
1685 pub block_comment_end: Option<String>,
1686
1687 #[serde(default)]
1689 pub use_tabs: Option<bool>,
1690
1691 #[serde(default)]
1693 pub tab_size: Option<usize>,
1694
1695 #[serde(default)]
1697 pub auto_indent: Option<bool>,
1698
1699 #[serde(default)]
1702 pub show_whitespace_tabs: Option<bool>,
1703
1704 #[serde(default)]
1706 pub formatter: Option<FormatterPackConfig>,
1707}
1708
1709#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1711#[serde(rename_all = "camelCase")]
1712#[ts(export)]
1713pub struct FormatterPackConfig {
1714 pub command: String,
1716
1717 #[serde(default)]
1719 pub args: Vec<String>,
1720}
1721
1722#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1724#[serde(rename_all = "camelCase")]
1725#[ts(export)]
1726pub struct LspServerPackConfig {
1727 pub command: String,
1729
1730 #[serde(default)]
1732 pub args: Vec<String>,
1733
1734 #[serde(default)]
1736 pub auto_start: Option<bool>,
1737
1738 #[serde(default)]
1740 #[ts(type = "Record<string, unknown> | null")]
1741 pub initialization_options: Option<JsonValue>,
1742}
1743
1744#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1746#[ts(export)]
1747pub enum HunkStatus {
1748 Pending,
1749 Staged,
1750 Discarded,
1751}
1752
1753#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1755#[ts(export)]
1756pub struct ReviewHunk {
1757 pub id: String,
1758 pub file: String,
1759 pub context_header: String,
1760 pub status: HunkStatus,
1761 pub base_range: Option<(usize, usize)>,
1763 pub modified_range: Option<(usize, usize)>,
1765}
1766
1767#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1769#[serde(deny_unknown_fields)]
1770#[ts(export, rename = "TsActionPopupAction")]
1771pub struct ActionPopupAction {
1772 pub id: String,
1774 pub label: String,
1776}
1777
1778#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1780#[serde(deny_unknown_fields)]
1781#[ts(export)]
1782pub struct ActionPopupOptions {
1783 pub id: String,
1785 pub title: String,
1787 pub message: String,
1789 pub actions: Vec<ActionPopupAction>,
1791}
1792
1793#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1795#[ts(export)]
1796pub struct TsHighlightSpan {
1797 pub start: u32,
1798 pub end: u32,
1799 #[ts(type = "[number, number, number]")]
1800 pub color: (u8, u8, u8),
1801 pub bold: bool,
1802 pub italic: bool,
1803}
1804
1805#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1807#[ts(export)]
1808pub struct SpawnResult {
1809 pub stdout: String,
1811 pub stderr: String,
1813 pub exit_code: i32,
1815}
1816
1817#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1819#[ts(export)]
1820pub struct BackgroundProcessResult {
1821 #[ts(type = "number")]
1823 pub process_id: u64,
1824 pub exit_code: i32,
1827}
1828
1829#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1831#[serde(deny_unknown_fields)]
1832#[ts(export, rename = "TextPropertyEntry")]
1833pub struct JsTextPropertyEntry {
1834 pub text: String,
1836 #[serde(default)]
1838 #[ts(optional, type = "Record<string, unknown>")]
1839 pub properties: Option<HashMap<String, JsonValue>>,
1840}
1841
1842#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1844#[ts(export)]
1845pub struct DirEntry {
1846 pub name: String,
1848 pub is_file: bool,
1850 pub is_dir: bool,
1852}
1853
1854#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1856#[ts(export)]
1857pub struct JsPosition {
1858 pub line: u32,
1860 pub character: u32,
1862}
1863
1864#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1866#[ts(export)]
1867pub struct JsRange {
1868 pub start: JsPosition,
1870 pub end: JsPosition,
1872}
1873
1874#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1876#[ts(export)]
1877pub struct JsDiagnostic {
1878 pub uri: String,
1880 pub message: String,
1882 pub severity: Option<u8>,
1884 pub range: JsRange,
1886 #[ts(optional)]
1888 pub source: Option<String>,
1889}
1890
1891#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1893#[serde(deny_unknown_fields)]
1894#[ts(export)]
1895pub struct CreateVirtualBufferOptions {
1896 pub name: String,
1898 #[serde(default)]
1900 #[ts(optional)]
1901 pub mode: Option<String>,
1902 #[serde(default, rename = "readOnly")]
1904 #[ts(optional, rename = "readOnly")]
1905 pub read_only: Option<bool>,
1906 #[serde(default, rename = "showLineNumbers")]
1908 #[ts(optional, rename = "showLineNumbers")]
1909 pub show_line_numbers: Option<bool>,
1910 #[serde(default, rename = "showCursors")]
1912 #[ts(optional, rename = "showCursors")]
1913 pub show_cursors: Option<bool>,
1914 #[serde(default, rename = "editingDisabled")]
1916 #[ts(optional, rename = "editingDisabled")]
1917 pub editing_disabled: Option<bool>,
1918 #[serde(default, rename = "hiddenFromTabs")]
1920 #[ts(optional, rename = "hiddenFromTabs")]
1921 pub hidden_from_tabs: Option<bool>,
1922 #[serde(default)]
1924 #[ts(optional)]
1925 pub entries: Option<Vec<JsTextPropertyEntry>>,
1926}
1927
1928#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1930#[serde(deny_unknown_fields)]
1931#[ts(export)]
1932pub struct CreateVirtualBufferInSplitOptions {
1933 pub name: String,
1935 #[serde(default)]
1937 #[ts(optional)]
1938 pub mode: Option<String>,
1939 #[serde(default, rename = "readOnly")]
1941 #[ts(optional, rename = "readOnly")]
1942 pub read_only: Option<bool>,
1943 #[serde(default)]
1945 #[ts(optional)]
1946 pub ratio: Option<f32>,
1947 #[serde(default)]
1949 #[ts(optional)]
1950 pub direction: Option<String>,
1951 #[serde(default, rename = "panelId")]
1953 #[ts(optional, rename = "panelId")]
1954 pub panel_id: Option<String>,
1955 #[serde(default, rename = "showLineNumbers")]
1957 #[ts(optional, rename = "showLineNumbers")]
1958 pub show_line_numbers: Option<bool>,
1959 #[serde(default, rename = "showCursors")]
1961 #[ts(optional, rename = "showCursors")]
1962 pub show_cursors: Option<bool>,
1963 #[serde(default, rename = "editingDisabled")]
1965 #[ts(optional, rename = "editingDisabled")]
1966 pub editing_disabled: Option<bool>,
1967 #[serde(default, rename = "lineWrap")]
1969 #[ts(optional, rename = "lineWrap")]
1970 pub line_wrap: Option<bool>,
1971 #[serde(default)]
1973 #[ts(optional)]
1974 pub before: Option<bool>,
1975 #[serde(default)]
1977 #[ts(optional)]
1978 pub entries: Option<Vec<JsTextPropertyEntry>>,
1979}
1980
1981#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1983#[serde(deny_unknown_fields)]
1984#[ts(export)]
1985pub struct CreateVirtualBufferInExistingSplitOptions {
1986 pub name: String,
1988 #[serde(rename = "splitId")]
1990 #[ts(rename = "splitId")]
1991 pub split_id: usize,
1992 #[serde(default)]
1994 #[ts(optional)]
1995 pub mode: Option<String>,
1996 #[serde(default, rename = "readOnly")]
1998 #[ts(optional, rename = "readOnly")]
1999 pub read_only: Option<bool>,
2000 #[serde(default, rename = "showLineNumbers")]
2002 #[ts(optional, rename = "showLineNumbers")]
2003 pub show_line_numbers: Option<bool>,
2004 #[serde(default, rename = "showCursors")]
2006 #[ts(optional, rename = "showCursors")]
2007 pub show_cursors: Option<bool>,
2008 #[serde(default, rename = "editingDisabled")]
2010 #[ts(optional, rename = "editingDisabled")]
2011 pub editing_disabled: Option<bool>,
2012 #[serde(default, rename = "lineWrap")]
2014 #[ts(optional, rename = "lineWrap")]
2015 pub line_wrap: Option<bool>,
2016 #[serde(default)]
2018 #[ts(optional)]
2019 pub entries: Option<Vec<JsTextPropertyEntry>>,
2020}
2021
2022#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2024#[serde(deny_unknown_fields)]
2025#[ts(export)]
2026pub struct CreateTerminalOptions {
2027 #[serde(default)]
2029 #[ts(optional)]
2030 pub cwd: Option<String>,
2031 #[serde(default)]
2033 #[ts(optional)]
2034 pub direction: Option<String>,
2035 #[serde(default)]
2037 #[ts(optional)]
2038 pub ratio: Option<f32>,
2039 #[serde(default)]
2041 #[ts(optional)]
2042 pub focus: Option<bool>,
2043}
2044
2045#[derive(Debug, Clone, Serialize, TS)]
2050#[ts(export, type = "Array<Record<string, unknown>>")]
2051pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
2052
2053#[cfg(feature = "plugins")]
2055mod fromjs_impls {
2056 use super::*;
2057 use rquickjs::{Ctx, FromJs, Value};
2058
2059 impl<'js> FromJs<'js> for JsTextPropertyEntry {
2060 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2061 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2062 from: "object",
2063 to: "JsTextPropertyEntry",
2064 message: Some(e.to_string()),
2065 })
2066 }
2067 }
2068
2069 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
2070 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2071 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2072 from: "object",
2073 to: "CreateVirtualBufferOptions",
2074 message: Some(e.to_string()),
2075 })
2076 }
2077 }
2078
2079 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
2080 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2081 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2082 from: "object",
2083 to: "CreateVirtualBufferInSplitOptions",
2084 message: Some(e.to_string()),
2085 })
2086 }
2087 }
2088
2089 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
2090 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2091 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2092 from: "object",
2093 to: "CreateVirtualBufferInExistingSplitOptions",
2094 message: Some(e.to_string()),
2095 })
2096 }
2097 }
2098
2099 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
2100 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
2101 rquickjs_serde::to_value(ctx.clone(), &self.0)
2102 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
2103 }
2104 }
2105
2106 impl<'js> FromJs<'js> for ActionSpec {
2109 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2110 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2111 from: "object",
2112 to: "ActionSpec",
2113 message: Some(e.to_string()),
2114 })
2115 }
2116 }
2117
2118 impl<'js> FromJs<'js> for ActionPopupAction {
2119 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2120 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2121 from: "object",
2122 to: "ActionPopupAction",
2123 message: Some(e.to_string()),
2124 })
2125 }
2126 }
2127
2128 impl<'js> FromJs<'js> for ActionPopupOptions {
2129 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2130 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2131 from: "object",
2132 to: "ActionPopupOptions",
2133 message: Some(e.to_string()),
2134 })
2135 }
2136 }
2137
2138 impl<'js> FromJs<'js> for ViewTokenWire {
2139 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2140 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2141 from: "object",
2142 to: "ViewTokenWire",
2143 message: Some(e.to_string()),
2144 })
2145 }
2146 }
2147
2148 impl<'js> FromJs<'js> for ViewTokenStyle {
2149 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2150 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2151 from: "object",
2152 to: "ViewTokenStyle",
2153 message: Some(e.to_string()),
2154 })
2155 }
2156 }
2157
2158 impl<'js> FromJs<'js> for LayoutHints {
2159 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2160 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2161 from: "object",
2162 to: "LayoutHints",
2163 message: Some(e.to_string()),
2164 })
2165 }
2166 }
2167
2168 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
2169 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2170 let json: serde_json::Value =
2172 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2173 from: "object",
2174 to: "CreateCompositeBufferOptions (json)",
2175 message: Some(e.to_string()),
2176 })?;
2177 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
2178 from: "json",
2179 to: "CreateCompositeBufferOptions",
2180 message: Some(e.to_string()),
2181 })
2182 }
2183 }
2184
2185 impl<'js> FromJs<'js> for CompositeHunk {
2186 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2187 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2188 from: "object",
2189 to: "CompositeHunk",
2190 message: Some(e.to_string()),
2191 })
2192 }
2193 }
2194
2195 impl<'js> FromJs<'js> for LanguagePackConfig {
2196 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2197 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2198 from: "object",
2199 to: "LanguagePackConfig",
2200 message: Some(e.to_string()),
2201 })
2202 }
2203 }
2204
2205 impl<'js> FromJs<'js> for LspServerPackConfig {
2206 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2207 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2208 from: "object",
2209 to: "LspServerPackConfig",
2210 message: Some(e.to_string()),
2211 })
2212 }
2213 }
2214
2215 impl<'js> FromJs<'js> for CreateTerminalOptions {
2216 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2217 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2218 from: "object",
2219 to: "CreateTerminalOptions",
2220 message: Some(e.to_string()),
2221 })
2222 }
2223 }
2224}
2225
2226pub struct PluginApi {
2228 hooks: Arc<RwLock<HookRegistry>>,
2230
2231 commands: Arc<RwLock<CommandRegistry>>,
2233
2234 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2236
2237 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2239}
2240
2241impl PluginApi {
2242 pub fn new(
2244 hooks: Arc<RwLock<HookRegistry>>,
2245 commands: Arc<RwLock<CommandRegistry>>,
2246 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2247 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2248 ) -> Self {
2249 Self {
2250 hooks,
2251 commands,
2252 command_sender,
2253 state_snapshot,
2254 }
2255 }
2256
2257 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
2259 let mut hooks = self.hooks.write().unwrap();
2260 hooks.add_hook(hook_name, callback);
2261 }
2262
2263 pub fn unregister_hooks(&self, hook_name: &str) {
2265 let mut hooks = self.hooks.write().unwrap();
2266 hooks.remove_hooks(hook_name);
2267 }
2268
2269 pub fn register_command(&self, command: Command) {
2271 let commands = self.commands.read().unwrap();
2272 commands.register(command);
2273 }
2274
2275 pub fn unregister_command(&self, name: &str) {
2277 let commands = self.commands.read().unwrap();
2278 commands.unregister(name);
2279 }
2280
2281 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
2283 self.command_sender
2284 .send(command)
2285 .map_err(|e| format!("Failed to send command: {}", e))
2286 }
2287
2288 pub fn insert_text(
2290 &self,
2291 buffer_id: BufferId,
2292 position: usize,
2293 text: String,
2294 ) -> Result<(), String> {
2295 self.send_command(PluginCommand::InsertText {
2296 buffer_id,
2297 position,
2298 text,
2299 })
2300 }
2301
2302 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
2304 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
2305 }
2306
2307 pub fn add_overlay(
2315 &self,
2316 buffer_id: BufferId,
2317 namespace: Option<String>,
2318 range: Range<usize>,
2319 options: OverlayOptions,
2320 ) -> Result<(), String> {
2321 self.send_command(PluginCommand::AddOverlay {
2322 buffer_id,
2323 namespace: namespace.map(OverlayNamespace::from_string),
2324 range,
2325 options,
2326 })
2327 }
2328
2329 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
2331 self.send_command(PluginCommand::RemoveOverlay {
2332 buffer_id,
2333 handle: OverlayHandle::from_string(handle),
2334 })
2335 }
2336
2337 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
2339 self.send_command(PluginCommand::ClearNamespace {
2340 buffer_id,
2341 namespace: OverlayNamespace::from_string(namespace),
2342 })
2343 }
2344
2345 pub fn clear_overlays_in_range(
2348 &self,
2349 buffer_id: BufferId,
2350 start: usize,
2351 end: usize,
2352 ) -> Result<(), String> {
2353 self.send_command(PluginCommand::ClearOverlaysInRange {
2354 buffer_id,
2355 start,
2356 end,
2357 })
2358 }
2359
2360 pub fn set_status(&self, message: String) -> Result<(), String> {
2362 self.send_command(PluginCommand::SetStatus { message })
2363 }
2364
2365 pub fn open_file_at_location(
2368 &self,
2369 path: PathBuf,
2370 line: Option<usize>,
2371 column: Option<usize>,
2372 ) -> Result<(), String> {
2373 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
2374 }
2375
2376 pub fn open_file_in_split(
2381 &self,
2382 split_id: usize,
2383 path: PathBuf,
2384 line: Option<usize>,
2385 column: Option<usize>,
2386 ) -> Result<(), String> {
2387 self.send_command(PluginCommand::OpenFileInSplit {
2388 split_id,
2389 path,
2390 line,
2391 column,
2392 })
2393 }
2394
2395 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
2398 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
2399 }
2400
2401 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
2404 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
2405 }
2406
2407 pub fn set_prompt_input_sync(&self, sync: bool) -> Result<(), String> {
2409 self.send_command(PluginCommand::SetPromptInputSync { sync })
2410 }
2411
2412 pub fn add_menu_item(
2414 &self,
2415 menu_label: String,
2416 item: MenuItem,
2417 position: MenuPosition,
2418 ) -> Result<(), String> {
2419 self.send_command(PluginCommand::AddMenuItem {
2420 menu_label,
2421 item,
2422 position,
2423 })
2424 }
2425
2426 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
2428 self.send_command(PluginCommand::AddMenu { menu, position })
2429 }
2430
2431 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
2433 self.send_command(PluginCommand::RemoveMenuItem {
2434 menu_label,
2435 item_label,
2436 })
2437 }
2438
2439 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
2441 self.send_command(PluginCommand::RemoveMenu { menu_label })
2442 }
2443
2444 pub fn create_virtual_buffer(
2451 &self,
2452 name: String,
2453 mode: String,
2454 read_only: bool,
2455 ) -> Result<(), String> {
2456 self.send_command(PluginCommand::CreateVirtualBuffer {
2457 name,
2458 mode,
2459 read_only,
2460 })
2461 }
2462
2463 pub fn create_virtual_buffer_with_content(
2469 &self,
2470 name: String,
2471 mode: String,
2472 read_only: bool,
2473 entries: Vec<TextPropertyEntry>,
2474 ) -> Result<(), String> {
2475 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
2476 name,
2477 mode,
2478 read_only,
2479 entries,
2480 show_line_numbers: true,
2481 show_cursors: true,
2482 editing_disabled: false,
2483 hidden_from_tabs: false,
2484 request_id: None,
2485 })
2486 }
2487
2488 pub fn set_virtual_buffer_content(
2492 &self,
2493 buffer_id: BufferId,
2494 entries: Vec<TextPropertyEntry>,
2495 ) -> Result<(), String> {
2496 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
2497 }
2498
2499 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
2503 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
2504 }
2505
2506 pub fn define_mode(
2511 &self,
2512 name: String,
2513 parent: Option<String>,
2514 bindings: Vec<(String, String)>,
2515 read_only: bool,
2516 ) -> Result<(), String> {
2517 self.send_command(PluginCommand::DefineMode {
2518 name,
2519 parent,
2520 bindings,
2521 read_only,
2522 })
2523 }
2524
2525 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
2527 self.send_command(PluginCommand::ShowBuffer { buffer_id })
2528 }
2529
2530 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
2532 self.send_command(PluginCommand::SetSplitScroll {
2533 split_id: SplitId(split_id),
2534 top_byte,
2535 })
2536 }
2537
2538 pub fn get_highlights(
2540 &self,
2541 buffer_id: BufferId,
2542 range: Range<usize>,
2543 request_id: u64,
2544 ) -> Result<(), String> {
2545 self.send_command(PluginCommand::RequestHighlights {
2546 buffer_id,
2547 range,
2548 request_id,
2549 })
2550 }
2551
2552 pub fn get_active_buffer_id(&self) -> BufferId {
2556 let snapshot = self.state_snapshot.read().unwrap();
2557 snapshot.active_buffer_id
2558 }
2559
2560 pub fn get_active_split_id(&self) -> usize {
2562 let snapshot = self.state_snapshot.read().unwrap();
2563 snapshot.active_split_id
2564 }
2565
2566 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
2568 let snapshot = self.state_snapshot.read().unwrap();
2569 snapshot.buffers.get(&buffer_id).cloned()
2570 }
2571
2572 pub fn list_buffers(&self) -> Vec<BufferInfo> {
2574 let snapshot = self.state_snapshot.read().unwrap();
2575 snapshot.buffers.values().cloned().collect()
2576 }
2577
2578 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
2580 let snapshot = self.state_snapshot.read().unwrap();
2581 snapshot.primary_cursor.clone()
2582 }
2583
2584 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
2586 let snapshot = self.state_snapshot.read().unwrap();
2587 snapshot.all_cursors.clone()
2588 }
2589
2590 pub fn get_viewport(&self) -> Option<ViewportInfo> {
2592 let snapshot = self.state_snapshot.read().unwrap();
2593 snapshot.viewport.clone()
2594 }
2595
2596 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
2598 Arc::clone(&self.state_snapshot)
2599 }
2600}
2601
2602impl Clone for PluginApi {
2603 fn clone(&self) -> Self {
2604 Self {
2605 hooks: Arc::clone(&self.hooks),
2606 commands: Arc::clone(&self.commands),
2607 command_sender: self.command_sender.clone(),
2608 state_snapshot: Arc::clone(&self.state_snapshot),
2609 }
2610 }
2611}
2612
2613#[cfg(test)]
2614mod tests {
2615 use super::*;
2616
2617 #[test]
2618 fn test_plugin_api_creation() {
2619 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2620 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2621 let (tx, _rx) = std::sync::mpsc::channel();
2622 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2623
2624 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2625
2626 let _clone = api.clone();
2628 }
2629
2630 #[test]
2631 fn test_register_hook() {
2632 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2633 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2634 let (tx, _rx) = std::sync::mpsc::channel();
2635 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2636
2637 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
2638
2639 api.register_hook("test-hook", Box::new(|_| true));
2640
2641 let hook_registry = hooks.read().unwrap();
2642 assert_eq!(hook_registry.hook_count("test-hook"), 1);
2643 }
2644
2645 #[test]
2646 fn test_send_command() {
2647 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2648 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2649 let (tx, rx) = std::sync::mpsc::channel();
2650 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2651
2652 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2653
2654 let result = api.insert_text(BufferId(1), 0, "test".to_string());
2655 assert!(result.is_ok());
2656
2657 let received = rx.try_recv();
2659 assert!(received.is_ok());
2660
2661 match received.unwrap() {
2662 PluginCommand::InsertText {
2663 buffer_id,
2664 position,
2665 text,
2666 } => {
2667 assert_eq!(buffer_id.0, 1);
2668 assert_eq!(position, 0);
2669 assert_eq!(text, "test");
2670 }
2671 _ => panic!("Wrong command type"),
2672 }
2673 }
2674
2675 #[test]
2676 fn test_add_overlay_command() {
2677 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2678 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2679 let (tx, rx) = std::sync::mpsc::channel();
2680 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2681
2682 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2683
2684 let result = api.add_overlay(
2685 BufferId(1),
2686 Some("test-overlay".to_string()),
2687 0..10,
2688 OverlayOptions {
2689 fg: Some(OverlayColorSpec::ThemeKey("ui.status_bar_fg".to_string())),
2690 bg: None,
2691 underline: true,
2692 bold: false,
2693 italic: false,
2694 strikethrough: false,
2695 extend_to_line_end: false,
2696 url: None,
2697 },
2698 );
2699 assert!(result.is_ok());
2700
2701 let received = rx.try_recv().unwrap();
2702 match received {
2703 PluginCommand::AddOverlay {
2704 buffer_id,
2705 namespace,
2706 range,
2707 options,
2708 } => {
2709 assert_eq!(buffer_id.0, 1);
2710 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
2711 assert_eq!(range, 0..10);
2712 assert!(matches!(
2713 options.fg,
2714 Some(OverlayColorSpec::ThemeKey(ref k)) if k == "ui.status_bar_fg"
2715 ));
2716 assert!(options.bg.is_none());
2717 assert!(options.underline);
2718 assert!(!options.bold);
2719 assert!(!options.italic);
2720 assert!(!options.extend_to_line_end);
2721 }
2722 _ => panic!("Wrong command type"),
2723 }
2724 }
2725
2726 #[test]
2727 fn test_set_status_command() {
2728 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2729 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2730 let (tx, rx) = std::sync::mpsc::channel();
2731 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2732
2733 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2734
2735 let result = api.set_status("Test status".to_string());
2736 assert!(result.is_ok());
2737
2738 let received = rx.try_recv().unwrap();
2739 match received {
2740 PluginCommand::SetStatus { message } => {
2741 assert_eq!(message, "Test status");
2742 }
2743 _ => panic!("Wrong command type"),
2744 }
2745 }
2746
2747 #[test]
2748 fn test_get_active_buffer_id() {
2749 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2750 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2751 let (tx, _rx) = std::sync::mpsc::channel();
2752 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2753
2754 {
2756 let mut snapshot = state_snapshot.write().unwrap();
2757 snapshot.active_buffer_id = BufferId(5);
2758 }
2759
2760 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2761
2762 let active_id = api.get_active_buffer_id();
2763 assert_eq!(active_id.0, 5);
2764 }
2765
2766 #[test]
2767 fn test_get_buffer_info() {
2768 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2769 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2770 let (tx, _rx) = std::sync::mpsc::channel();
2771 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2772
2773 {
2775 let mut snapshot = state_snapshot.write().unwrap();
2776 let buffer_info = BufferInfo {
2777 id: BufferId(1),
2778 path: Some(std::path::PathBuf::from("/test/file.txt")),
2779 modified: true,
2780 length: 100,
2781 is_virtual: false,
2782 view_mode: "source".to_string(),
2783 is_composing_in_any_split: false,
2784 compose_width: None,
2785 };
2786 snapshot.buffers.insert(BufferId(1), buffer_info);
2787 }
2788
2789 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2790
2791 let info = api.get_buffer_info(BufferId(1));
2792 assert!(info.is_some());
2793 let info = info.unwrap();
2794 assert_eq!(info.id.0, 1);
2795 assert_eq!(
2796 info.path.as_ref().unwrap().to_str().unwrap(),
2797 "/test/file.txt"
2798 );
2799 assert!(info.modified);
2800 assert_eq!(info.length, 100);
2801
2802 let no_info = api.get_buffer_info(BufferId(999));
2804 assert!(no_info.is_none());
2805 }
2806
2807 #[test]
2808 fn test_list_buffers() {
2809 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2810 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2811 let (tx, _rx) = std::sync::mpsc::channel();
2812 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2813
2814 {
2816 let mut snapshot = state_snapshot.write().unwrap();
2817 snapshot.buffers.insert(
2818 BufferId(1),
2819 BufferInfo {
2820 id: BufferId(1),
2821 path: Some(std::path::PathBuf::from("/file1.txt")),
2822 modified: false,
2823 length: 50,
2824 is_virtual: false,
2825 view_mode: "source".to_string(),
2826 is_composing_in_any_split: false,
2827 compose_width: None,
2828 },
2829 );
2830 snapshot.buffers.insert(
2831 BufferId(2),
2832 BufferInfo {
2833 id: BufferId(2),
2834 path: Some(std::path::PathBuf::from("/file2.txt")),
2835 modified: true,
2836 length: 100,
2837 is_virtual: false,
2838 view_mode: "source".to_string(),
2839 is_composing_in_any_split: false,
2840 compose_width: None,
2841 },
2842 );
2843 snapshot.buffers.insert(
2844 BufferId(3),
2845 BufferInfo {
2846 id: BufferId(3),
2847 path: None,
2848 modified: false,
2849 length: 0,
2850 is_virtual: true,
2851 view_mode: "source".to_string(),
2852 is_composing_in_any_split: false,
2853 compose_width: None,
2854 },
2855 );
2856 }
2857
2858 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2859
2860 let buffers = api.list_buffers();
2861 assert_eq!(buffers.len(), 3);
2862
2863 assert!(buffers.iter().any(|b| b.id.0 == 1));
2865 assert!(buffers.iter().any(|b| b.id.0 == 2));
2866 assert!(buffers.iter().any(|b| b.id.0 == 3));
2867 }
2868
2869 #[test]
2870 fn test_get_primary_cursor() {
2871 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2872 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2873 let (tx, _rx) = std::sync::mpsc::channel();
2874 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2875
2876 {
2878 let mut snapshot = state_snapshot.write().unwrap();
2879 snapshot.primary_cursor = Some(CursorInfo {
2880 position: 42,
2881 selection: Some(10..42),
2882 });
2883 }
2884
2885 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2886
2887 let cursor = api.get_primary_cursor();
2888 assert!(cursor.is_some());
2889 let cursor = cursor.unwrap();
2890 assert_eq!(cursor.position, 42);
2891 assert_eq!(cursor.selection, Some(10..42));
2892 }
2893
2894 #[test]
2895 fn test_get_all_cursors() {
2896 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2897 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2898 let (tx, _rx) = std::sync::mpsc::channel();
2899 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2900
2901 {
2903 let mut snapshot = state_snapshot.write().unwrap();
2904 snapshot.all_cursors = vec![
2905 CursorInfo {
2906 position: 10,
2907 selection: None,
2908 },
2909 CursorInfo {
2910 position: 20,
2911 selection: Some(15..20),
2912 },
2913 CursorInfo {
2914 position: 30,
2915 selection: Some(25..30),
2916 },
2917 ];
2918 }
2919
2920 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2921
2922 let cursors = api.get_all_cursors();
2923 assert_eq!(cursors.len(), 3);
2924 assert_eq!(cursors[0].position, 10);
2925 assert_eq!(cursors[0].selection, None);
2926 assert_eq!(cursors[1].position, 20);
2927 assert_eq!(cursors[1].selection, Some(15..20));
2928 assert_eq!(cursors[2].position, 30);
2929 assert_eq!(cursors[2].selection, Some(25..30));
2930 }
2931
2932 #[test]
2933 fn test_get_viewport() {
2934 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2935 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2936 let (tx, _rx) = std::sync::mpsc::channel();
2937 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2938
2939 {
2941 let mut snapshot = state_snapshot.write().unwrap();
2942 snapshot.viewport = Some(ViewportInfo {
2943 top_byte: 100,
2944 left_column: 5,
2945 width: 80,
2946 height: 24,
2947 });
2948 }
2949
2950 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2951
2952 let viewport = api.get_viewport();
2953 assert!(viewport.is_some());
2954 let viewport = viewport.unwrap();
2955 assert_eq!(viewport.top_byte, 100);
2956 assert_eq!(viewport.left_column, 5);
2957 assert_eq!(viewport.width, 80);
2958 assert_eq!(viewport.height, 24);
2959 }
2960
2961 #[test]
2962 fn test_composite_buffer_options_rejects_unknown_fields() {
2963 let valid_json = r#"{
2965 "name": "test",
2966 "mode": "diff",
2967 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2968 "sources": [{"bufferId": 1, "label": "old"}]
2969 }"#;
2970 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
2971 assert!(
2972 result.is_ok(),
2973 "Valid JSON should parse: {:?}",
2974 result.err()
2975 );
2976
2977 let invalid_json = r#"{
2979 "name": "test",
2980 "mode": "diff",
2981 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2982 "sources": [{"buffer_id": 1, "label": "old"}]
2983 }"#;
2984 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
2985 assert!(
2986 result.is_err(),
2987 "JSON with unknown field should fail to parse"
2988 );
2989 let err = result.unwrap_err().to_string();
2990 assert!(
2991 err.contains("unknown field") || err.contains("buffer_id"),
2992 "Error should mention unknown field: {}",
2993 err
2994 );
2995 }
2996
2997 #[test]
2998 fn test_composite_hunk_rejects_unknown_fields() {
2999 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3001 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
3002 assert!(
3003 result.is_ok(),
3004 "Valid JSON should parse: {:?}",
3005 result.err()
3006 );
3007
3008 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3010 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
3011 assert!(
3012 result.is_err(),
3013 "JSON with unknown field should fail to parse"
3014 );
3015 let err = result.unwrap_err().to_string();
3016 assert!(
3017 err.contains("unknown field") || err.contains("old_start"),
3018 "Error should mention unknown field: {}",
3019 err
3020 );
3021 }
3022
3023 #[test]
3024 fn test_plugin_response_line_end_position() {
3025 let response = PluginResponse::LineEndPosition {
3026 request_id: 42,
3027 position: Some(100),
3028 };
3029 let json = serde_json::to_string(&response).unwrap();
3030 assert!(json.contains("LineEndPosition"));
3031 assert!(json.contains("42"));
3032 assert!(json.contains("100"));
3033
3034 let response_none = PluginResponse::LineEndPosition {
3036 request_id: 1,
3037 position: None,
3038 };
3039 let json_none = serde_json::to_string(&response_none).unwrap();
3040 assert!(json_none.contains("null"));
3041 }
3042
3043 #[test]
3044 fn test_plugin_response_buffer_line_count() {
3045 let response = PluginResponse::BufferLineCount {
3046 request_id: 99,
3047 count: Some(500),
3048 };
3049 let json = serde_json::to_string(&response).unwrap();
3050 assert!(json.contains("BufferLineCount"));
3051 assert!(json.contains("99"));
3052 assert!(json.contains("500"));
3053 }
3054
3055 #[test]
3056 fn test_plugin_command_get_line_end_position() {
3057 let command = PluginCommand::GetLineEndPosition {
3058 buffer_id: BufferId(1),
3059 line: 10,
3060 request_id: 123,
3061 };
3062 let json = serde_json::to_string(&command).unwrap();
3063 assert!(json.contains("GetLineEndPosition"));
3064 assert!(json.contains("10"));
3065 }
3066
3067 #[test]
3068 fn test_plugin_command_get_buffer_line_count() {
3069 let command = PluginCommand::GetBufferLineCount {
3070 buffer_id: BufferId(0),
3071 request_id: 456,
3072 };
3073 let json = serde_json::to_string(&command).unwrap();
3074 assert!(json.contains("GetBufferLineCount"));
3075 assert!(json.contains("456"));
3076 }
3077
3078 #[test]
3079 fn test_plugin_command_scroll_to_line_center() {
3080 let command = PluginCommand::ScrollToLineCenter {
3081 split_id: SplitId(1),
3082 buffer_id: BufferId(2),
3083 line: 50,
3084 };
3085 let json = serde_json::to_string(&command).unwrap();
3086 assert!(json.contains("ScrollToLineCenter"));
3087 assert!(json.contains("50"));
3088 }
3089}