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 pub language: String,
329}
330
331fn serialize_path<S: serde::Serializer>(path: &Option<PathBuf>, s: S) -> Result<S::Ok, S::Error> {
332 s.serialize_str(
333 &path
334 .as_ref()
335 .map(|p| p.to_string_lossy().to_string())
336 .unwrap_or_default(),
337 )
338}
339
340fn serialize_ranges_as_tuples<S>(ranges: &[Range<usize>], serializer: S) -> Result<S::Ok, S::Error>
342where
343 S: serde::Serializer,
344{
345 use serde::ser::SerializeSeq;
346 let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
347 for range in ranges {
348 seq.serialize_element(&(range.start, range.end))?;
349 }
350 seq.end()
351}
352
353fn serialize_opt_ranges_as_tuples<S>(
355 ranges: &Option<Vec<Range<usize>>>,
356 serializer: S,
357) -> Result<S::Ok, S::Error>
358where
359 S: serde::Serializer,
360{
361 match ranges {
362 Some(ranges) => {
363 use serde::ser::SerializeSeq;
364 let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
365 for range in ranges {
366 seq.serialize_element(&(range.start, range.end))?;
367 }
368 seq.end()
369 }
370 None => serializer.serialize_none(),
371 }
372}
373
374#[derive(Debug, Clone, Serialize, Deserialize, TS)]
376#[ts(export)]
377pub struct BufferSavedDiff {
378 pub equal: bool,
379 #[serde(serialize_with = "serialize_ranges_as_tuples")]
380 #[ts(type = "Array<[number, number]>")]
381 pub byte_ranges: Vec<Range<usize>>,
382 #[serde(serialize_with = "serialize_opt_ranges_as_tuples")]
383 #[ts(type = "Array<[number, number]> | null")]
384 pub line_ranges: Option<Vec<Range<usize>>>,
385}
386
387#[derive(Debug, Clone, Serialize, Deserialize, TS)]
389#[serde(rename_all = "camelCase")]
390#[ts(export, rename_all = "camelCase")]
391pub struct ViewportInfo {
392 pub top_byte: usize,
394 pub top_line: Option<usize>,
396 pub left_column: usize,
398 pub width: u16,
400 pub height: u16,
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize, TS)]
406#[serde(rename_all = "camelCase")]
407#[ts(export, rename_all = "camelCase")]
408pub struct LayoutHints {
409 pub compose_width: Option<u16>,
411 pub column_guides: Option<Vec<u16>>,
413}
414
415#[derive(Debug, Clone, Serialize, Deserialize, TS)]
430#[serde(untagged)]
431#[ts(export)]
432pub enum OverlayColorSpec {
433 #[ts(type = "[number, number, number]")]
435 Rgb(u8, u8, u8),
436 ThemeKey(String),
438}
439
440impl OverlayColorSpec {
441 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
443 Self::Rgb(r, g, b)
444 }
445
446 pub fn theme_key(key: impl Into<String>) -> Self {
448 Self::ThemeKey(key.into())
449 }
450
451 pub fn as_rgb(&self) -> Option<(u8, u8, u8)> {
453 match self {
454 Self::Rgb(r, g, b) => Some((*r, *g, *b)),
455 Self::ThemeKey(_) => None,
456 }
457 }
458
459 pub fn as_theme_key(&self) -> Option<&str> {
461 match self {
462 Self::ThemeKey(key) => Some(key),
463 Self::Rgb(_, _, _) => None,
464 }
465 }
466}
467
468#[derive(Debug, Clone, Serialize, Deserialize, TS)]
473#[serde(deny_unknown_fields, rename_all = "camelCase")]
474#[ts(export, rename_all = "camelCase")]
475#[derive(Default)]
476pub struct OverlayOptions {
477 #[serde(default, skip_serializing_if = "Option::is_none")]
479 pub fg: Option<OverlayColorSpec>,
480
481 #[serde(default, skip_serializing_if = "Option::is_none")]
483 pub bg: Option<OverlayColorSpec>,
484
485 #[serde(default)]
487 pub underline: bool,
488
489 #[serde(default)]
491 pub bold: bool,
492
493 #[serde(default)]
495 pub italic: bool,
496
497 #[serde(default)]
499 pub strikethrough: bool,
500
501 #[serde(default)]
503 pub extend_to_line_end: bool,
504
505 #[serde(default, skip_serializing_if = "Option::is_none")]
509 pub url: Option<String>,
510}
511
512#[derive(Debug, Clone, Serialize, Deserialize, TS)]
518#[serde(deny_unknown_fields)]
519#[ts(export, rename = "TsCompositeLayoutConfig")]
520pub struct CompositeLayoutConfig {
521 #[serde(rename = "type")]
523 #[ts(rename = "type")]
524 pub layout_type: String,
525 #[serde(default)]
527 pub ratios: Option<Vec<f32>>,
528 #[serde(default = "default_true", rename = "showSeparator")]
530 #[ts(rename = "showSeparator")]
531 pub show_separator: bool,
532 #[serde(default)]
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(rename = "addBg", type = "[number, number, number] | null")]
569 pub add_bg: Option<[u8; 3]>,
570 #[serde(default, rename = "removeBg")]
572 #[ts(rename = "removeBg", type = "[number, number, number] | null")]
573 pub remove_bg: Option<[u8; 3]>,
574 #[serde(default, rename = "modifyBg")]
576 #[ts(rename = "modifyBg", type = "[number, number, number] | null")]
577 pub modify_bg: Option<[u8; 3]>,
578 #[serde(default, rename = "gutterStyle")]
580 #[ts(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
759impl EditorStateSnapshot {
760 pub fn new() -> Self {
761 Self {
762 active_buffer_id: BufferId(0),
763 active_split_id: 0,
764 buffers: HashMap::new(),
765 buffer_saved_diffs: HashMap::new(),
766 primary_cursor: None,
767 all_cursors: Vec::new(),
768 viewport: None,
769 buffer_cursor_positions: HashMap::new(),
770 buffer_text_properties: HashMap::new(),
771 selected_text: None,
772 clipboard: String::new(),
773 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
774 diagnostics: HashMap::new(),
775 folding_ranges: HashMap::new(),
776 config: serde_json::Value::Null,
777 user_config: serde_json::Value::Null,
778 editor_mode: None,
779 plugin_view_states: HashMap::new(),
780 plugin_view_states_split: 0,
781 }
782 }
783}
784
785impl Default for EditorStateSnapshot {
786 fn default() -> Self {
787 Self::new()
788 }
789}
790
791#[derive(Debug, Clone, Serialize, Deserialize, TS)]
793#[ts(export)]
794pub enum MenuPosition {
795 Top,
797 Bottom,
799 Before(String),
801 After(String),
803}
804
805#[derive(Debug, Clone, Serialize, Deserialize, TS)]
807#[ts(export)]
808pub enum PluginCommand {
809 InsertText {
811 buffer_id: BufferId,
812 position: usize,
813 text: String,
814 },
815
816 DeleteRange {
818 buffer_id: BufferId,
819 range: Range<usize>,
820 },
821
822 AddOverlay {
827 buffer_id: BufferId,
828 namespace: Option<OverlayNamespace>,
829 range: Range<usize>,
830 options: OverlayOptions,
832 },
833
834 RemoveOverlay {
836 buffer_id: BufferId,
837 handle: OverlayHandle,
838 },
839
840 SetStatus { message: String },
842
843 ApplyTheme { theme_name: String },
845
846 ReloadConfig,
849
850 RegisterCommand { command: Command },
852
853 UnregisterCommand { name: String },
855
856 OpenFileInBackground { path: PathBuf },
858
859 InsertAtCursor { text: String },
861
862 SpawnProcess {
864 command: String,
865 args: Vec<String>,
866 cwd: Option<String>,
867 callback_id: JsCallbackId,
868 },
869
870 Delay {
872 callback_id: JsCallbackId,
873 duration_ms: u64,
874 },
875
876 SpawnBackgroundProcess {
880 process_id: u64,
882 command: String,
884 args: Vec<String>,
886 cwd: Option<String>,
888 callback_id: JsCallbackId,
890 },
891
892 KillBackgroundProcess { process_id: u64 },
894
895 SpawnProcessWait {
898 process_id: u64,
900 callback_id: JsCallbackId,
902 },
903
904 SetLayoutHints {
906 buffer_id: BufferId,
907 split_id: Option<SplitId>,
908 range: Range<usize>,
909 hints: LayoutHints,
910 },
911
912 SetLineNumbers { buffer_id: BufferId, enabled: bool },
914
915 SetViewMode { buffer_id: BufferId, mode: String },
917
918 SetLineWrap {
920 buffer_id: BufferId,
921 split_id: Option<SplitId>,
922 enabled: bool,
923 },
924
925 SubmitViewTransform {
927 buffer_id: BufferId,
928 split_id: Option<SplitId>,
929 payload: ViewTransformPayload,
930 },
931
932 ClearViewTransform {
934 buffer_id: BufferId,
935 split_id: Option<SplitId>,
936 },
937
938 SetViewState {
941 buffer_id: BufferId,
942 key: String,
943 #[ts(type = "any")]
944 value: Option<serde_json::Value>,
945 },
946
947 ClearAllOverlays { buffer_id: BufferId },
949
950 ClearNamespace {
952 buffer_id: BufferId,
953 namespace: OverlayNamespace,
954 },
955
956 ClearOverlaysInRange {
959 buffer_id: BufferId,
960 start: usize,
961 end: usize,
962 },
963
964 AddVirtualText {
967 buffer_id: BufferId,
968 virtual_text_id: String,
969 position: usize,
970 text: String,
971 color: (u8, u8, u8),
972 use_bg: bool, before: bool, },
975
976 RemoveVirtualText {
978 buffer_id: BufferId,
979 virtual_text_id: String,
980 },
981
982 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
984
985 ClearVirtualTexts { buffer_id: BufferId },
987
988 AddVirtualLine {
992 buffer_id: BufferId,
993 position: usize,
995 text: String,
997 fg_color: (u8, u8, u8),
999 bg_color: Option<(u8, u8, u8)>,
1001 above: bool,
1003 namespace: String,
1005 priority: i32,
1007 },
1008
1009 ClearVirtualTextNamespace {
1012 buffer_id: BufferId,
1013 namespace: String,
1014 },
1015
1016 AddConceal {
1019 buffer_id: BufferId,
1020 namespace: OverlayNamespace,
1022 start: usize,
1024 end: usize,
1025 replacement: Option<String>,
1027 },
1028
1029 ClearConcealNamespace {
1031 buffer_id: BufferId,
1032 namespace: OverlayNamespace,
1033 },
1034
1035 ClearConcealsInRange {
1038 buffer_id: BufferId,
1039 start: usize,
1040 end: usize,
1041 },
1042
1043 AddSoftBreak {
1047 buffer_id: BufferId,
1048 namespace: OverlayNamespace,
1050 position: usize,
1052 indent: u16,
1054 },
1055
1056 ClearSoftBreakNamespace {
1058 buffer_id: BufferId,
1059 namespace: OverlayNamespace,
1060 },
1061
1062 ClearSoftBreaksInRange {
1064 buffer_id: BufferId,
1065 start: usize,
1066 end: usize,
1067 },
1068
1069 RefreshLines { buffer_id: BufferId },
1071
1072 RefreshAllLines,
1076
1077 HookCompleted { hook_name: String },
1081
1082 SetLineIndicator {
1085 buffer_id: BufferId,
1086 line: usize,
1088 namespace: String,
1090 symbol: String,
1092 color: (u8, u8, u8),
1094 priority: i32,
1096 },
1097
1098 SetLineIndicators {
1101 buffer_id: BufferId,
1102 lines: Vec<usize>,
1104 namespace: String,
1106 symbol: String,
1108 color: (u8, u8, u8),
1110 priority: i32,
1112 },
1113
1114 ClearLineIndicators {
1116 buffer_id: BufferId,
1117 namespace: String,
1119 },
1120
1121 SetFileExplorerDecorations {
1123 namespace: String,
1125 decorations: Vec<FileExplorerDecoration>,
1127 },
1128
1129 ClearFileExplorerDecorations {
1131 namespace: String,
1133 },
1134
1135 OpenFileAtLocation {
1138 path: PathBuf,
1139 line: Option<usize>, column: Option<usize>, },
1142
1143 OpenFileInSplit {
1146 split_id: usize,
1147 path: PathBuf,
1148 line: Option<usize>, column: Option<usize>, },
1151
1152 StartPrompt {
1155 label: String,
1156 prompt_type: String, },
1158
1159 StartPromptWithInitial {
1161 label: String,
1162 prompt_type: String,
1163 initial_value: String,
1164 },
1165
1166 StartPromptAsync {
1169 label: String,
1170 initial_value: String,
1171 callback_id: JsCallbackId,
1172 },
1173
1174 SetPromptSuggestions { suggestions: Vec<Suggestion> },
1177
1178 SetPromptInputSync { sync: bool },
1180
1181 AddMenuItem {
1184 menu_label: String,
1185 item: MenuItem,
1186 position: MenuPosition,
1187 },
1188
1189 AddMenu { menu: Menu, position: MenuPosition },
1191
1192 RemoveMenuItem {
1194 menu_label: String,
1195 item_label: String,
1196 },
1197
1198 RemoveMenu { menu_label: String },
1200
1201 CreateVirtualBuffer {
1203 name: String,
1205 mode: String,
1207 read_only: bool,
1209 },
1210
1211 CreateVirtualBufferWithContent {
1215 name: String,
1217 mode: String,
1219 read_only: bool,
1221 entries: Vec<TextPropertyEntry>,
1223 show_line_numbers: bool,
1225 show_cursors: bool,
1227 editing_disabled: bool,
1229 hidden_from_tabs: bool,
1231 request_id: Option<u64>,
1233 },
1234
1235 CreateVirtualBufferInSplit {
1238 name: String,
1240 mode: String,
1242 read_only: bool,
1244 entries: Vec<TextPropertyEntry>,
1246 ratio: f32,
1248 direction: Option<String>,
1250 panel_id: Option<String>,
1252 show_line_numbers: bool,
1254 show_cursors: bool,
1256 editing_disabled: bool,
1258 line_wrap: Option<bool>,
1260 before: bool,
1262 request_id: Option<u64>,
1264 },
1265
1266 SetVirtualBufferContent {
1268 buffer_id: BufferId,
1269 entries: Vec<TextPropertyEntry>,
1271 },
1272
1273 GetTextPropertiesAtCursor { buffer_id: BufferId },
1275
1276 DefineMode {
1278 name: String,
1279 parent: Option<String>,
1280 bindings: Vec<(String, String)>, read_only: bool,
1282 },
1283
1284 ShowBuffer { buffer_id: BufferId },
1286
1287 CreateVirtualBufferInExistingSplit {
1289 name: String,
1291 mode: String,
1293 read_only: bool,
1295 entries: Vec<TextPropertyEntry>,
1297 split_id: SplitId,
1299 show_line_numbers: bool,
1301 show_cursors: bool,
1303 editing_disabled: bool,
1305 line_wrap: Option<bool>,
1307 request_id: Option<u64>,
1309 },
1310
1311 CloseBuffer { buffer_id: BufferId },
1313
1314 CreateCompositeBuffer {
1317 name: String,
1319 mode: String,
1321 layout: CompositeLayoutConfig,
1323 sources: Vec<CompositeSourceConfig>,
1325 hunks: Option<Vec<CompositeHunk>>,
1327 request_id: Option<u64>,
1329 },
1330
1331 UpdateCompositeAlignment {
1333 buffer_id: BufferId,
1334 hunks: Vec<CompositeHunk>,
1335 },
1336
1337 CloseCompositeBuffer { buffer_id: BufferId },
1339
1340 FocusSplit { split_id: SplitId },
1342
1343 SetSplitBuffer {
1345 split_id: SplitId,
1346 buffer_id: BufferId,
1347 },
1348
1349 SetSplitScroll { split_id: SplitId, top_byte: usize },
1351
1352 RequestHighlights {
1354 buffer_id: BufferId,
1355 range: Range<usize>,
1356 request_id: u64,
1357 },
1358
1359 CloseSplit { split_id: SplitId },
1361
1362 SetSplitRatio {
1364 split_id: SplitId,
1365 ratio: f32,
1367 },
1368
1369 SetSplitLabel { split_id: SplitId, label: String },
1371
1372 ClearSplitLabel { split_id: SplitId },
1374
1375 GetSplitByLabel { label: String, request_id: u64 },
1377
1378 DistributeSplitsEvenly {
1380 split_ids: Vec<SplitId>,
1382 },
1383
1384 SetBufferCursor {
1386 buffer_id: BufferId,
1387 position: usize,
1389 },
1390
1391 SendLspRequest {
1393 language: String,
1394 method: String,
1395 #[ts(type = "any")]
1396 params: Option<JsonValue>,
1397 request_id: u64,
1398 },
1399
1400 SetClipboard { text: String },
1402
1403 DeleteSelection,
1406
1407 SetContext {
1411 name: String,
1413 active: bool,
1415 },
1416
1417 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1419
1420 ExecuteAction {
1423 action_name: String,
1425 },
1426
1427 ExecuteActions {
1431 actions: Vec<ActionSpec>,
1433 },
1434
1435 GetBufferText {
1437 buffer_id: BufferId,
1439 start: usize,
1441 end: usize,
1443 request_id: u64,
1445 },
1446
1447 GetLineStartPosition {
1450 buffer_id: BufferId,
1452 line: u32,
1454 request_id: u64,
1456 },
1457
1458 GetLineEndPosition {
1462 buffer_id: BufferId,
1464 line: u32,
1466 request_id: u64,
1468 },
1469
1470 GetBufferLineCount {
1472 buffer_id: BufferId,
1474 request_id: u64,
1476 },
1477
1478 ScrollToLineCenter {
1481 split_id: SplitId,
1483 buffer_id: BufferId,
1485 line: usize,
1487 },
1488
1489 SetEditorMode {
1492 mode: Option<String>,
1494 },
1495
1496 ShowActionPopup {
1499 popup_id: String,
1501 title: String,
1503 message: String,
1505 actions: Vec<ActionPopupAction>,
1507 },
1508
1509 DisableLspForLanguage {
1511 language: String,
1513 },
1514
1515 SetLspRootUri {
1519 language: String,
1521 uri: String,
1523 },
1524
1525 CreateScrollSyncGroup {
1529 group_id: u32,
1531 left_split: SplitId,
1533 right_split: SplitId,
1535 },
1536
1537 SetScrollSyncAnchors {
1540 group_id: u32,
1542 anchors: Vec<(usize, usize)>,
1544 },
1545
1546 RemoveScrollSyncGroup {
1548 group_id: u32,
1550 },
1551
1552 SaveBufferToPath {
1555 buffer_id: BufferId,
1557 path: PathBuf,
1559 },
1560
1561 LoadPlugin {
1564 path: PathBuf,
1566 callback_id: JsCallbackId,
1568 },
1569
1570 UnloadPlugin {
1573 name: String,
1575 callback_id: JsCallbackId,
1577 },
1578
1579 ReloadPlugin {
1582 name: String,
1584 callback_id: JsCallbackId,
1586 },
1587
1588 ListPlugins {
1591 callback_id: JsCallbackId,
1593 },
1594
1595 ReloadThemes,
1598
1599 RegisterGrammar {
1602 language: String,
1604 grammar_path: String,
1606 extensions: Vec<String>,
1608 },
1609
1610 RegisterLanguageConfig {
1613 language: String,
1615 config: LanguagePackConfig,
1617 },
1618
1619 RegisterLspServer {
1622 language: String,
1624 config: LspServerPackConfig,
1626 },
1627
1628 ReloadGrammars,
1631
1632 CreateTerminal {
1636 cwd: Option<String>,
1638 direction: Option<String>,
1640 ratio: Option<f32>,
1642 focus: Option<bool>,
1644 request_id: u64,
1646 },
1647
1648 SendTerminalInput {
1650 terminal_id: TerminalId,
1652 data: String,
1654 },
1655
1656 CloseTerminal {
1658 terminal_id: TerminalId,
1660 },
1661}
1662
1663impl PluginCommand {
1664 pub fn debug_variant_name(&self) -> String {
1666 let dbg = format!("{:?}", self);
1667 dbg.split([' ', '{', '(']).next().unwrap_or("?").to_string()
1668 }
1669}
1670
1671#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
1680#[serde(rename_all = "camelCase")]
1681#[ts(export)]
1682pub struct LanguagePackConfig {
1683 #[serde(default)]
1685 pub comment_prefix: Option<String>,
1686
1687 #[serde(default)]
1689 pub block_comment_start: Option<String>,
1690
1691 #[serde(default)]
1693 pub block_comment_end: Option<String>,
1694
1695 #[serde(default)]
1697 pub use_tabs: Option<bool>,
1698
1699 #[serde(default)]
1701 pub tab_size: Option<usize>,
1702
1703 #[serde(default)]
1705 pub auto_indent: Option<bool>,
1706
1707 #[serde(default)]
1710 pub show_whitespace_tabs: Option<bool>,
1711
1712 #[serde(default)]
1714 pub formatter: Option<FormatterPackConfig>,
1715}
1716
1717#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1719#[serde(rename_all = "camelCase")]
1720#[ts(export)]
1721pub struct FormatterPackConfig {
1722 pub command: String,
1724
1725 #[serde(default)]
1727 pub args: Vec<String>,
1728}
1729
1730#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1732#[serde(rename_all = "camelCase")]
1733#[ts(export)]
1734pub struct LspServerPackConfig {
1735 pub command: String,
1737
1738 #[serde(default)]
1740 pub args: Vec<String>,
1741
1742 #[serde(default)]
1744 pub auto_start: Option<bool>,
1745
1746 #[serde(default)]
1748 #[ts(type = "Record<string, unknown> | null")]
1749 pub initialization_options: Option<JsonValue>,
1750}
1751
1752#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1754#[ts(export)]
1755pub enum HunkStatus {
1756 Pending,
1757 Staged,
1758 Discarded,
1759}
1760
1761#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1763#[ts(export)]
1764pub struct ReviewHunk {
1765 pub id: String,
1766 pub file: String,
1767 pub context_header: String,
1768 pub status: HunkStatus,
1769 pub base_range: Option<(usize, usize)>,
1771 pub modified_range: Option<(usize, usize)>,
1773}
1774
1775#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1777#[serde(deny_unknown_fields)]
1778#[ts(export, rename = "TsActionPopupAction")]
1779pub struct ActionPopupAction {
1780 pub id: String,
1782 pub label: String,
1784}
1785
1786#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1788#[serde(deny_unknown_fields)]
1789#[ts(export)]
1790pub struct ActionPopupOptions {
1791 pub id: String,
1793 pub title: String,
1795 pub message: String,
1797 pub actions: Vec<ActionPopupAction>,
1799}
1800
1801#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1803#[ts(export)]
1804pub struct TsHighlightSpan {
1805 pub start: u32,
1806 pub end: u32,
1807 #[ts(type = "[number, number, number]")]
1808 pub color: (u8, u8, u8),
1809 pub bold: bool,
1810 pub italic: bool,
1811}
1812
1813#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1815#[ts(export)]
1816pub struct SpawnResult {
1817 pub stdout: String,
1819 pub stderr: String,
1821 pub exit_code: i32,
1823}
1824
1825#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1827#[ts(export)]
1828pub struct BackgroundProcessResult {
1829 #[ts(type = "number")]
1831 pub process_id: u64,
1832 pub exit_code: i32,
1835}
1836
1837#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1839#[serde(deny_unknown_fields)]
1840#[ts(export, rename = "TextPropertyEntry")]
1841pub struct JsTextPropertyEntry {
1842 pub text: String,
1844 #[serde(default)]
1846 #[ts(optional, type = "Record<string, unknown>")]
1847 pub properties: Option<HashMap<String, JsonValue>>,
1848}
1849
1850#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1852#[ts(export)]
1853pub struct DirEntry {
1854 pub name: String,
1856 pub is_file: bool,
1858 pub is_dir: bool,
1860}
1861
1862#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1864#[ts(export)]
1865pub struct JsPosition {
1866 pub line: u32,
1868 pub character: u32,
1870}
1871
1872#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1874#[ts(export)]
1875pub struct JsRange {
1876 pub start: JsPosition,
1878 pub end: JsPosition,
1880}
1881
1882#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1884#[ts(export)]
1885pub struct JsDiagnostic {
1886 pub uri: String,
1888 pub message: String,
1890 pub severity: Option<u8>,
1892 pub range: JsRange,
1894 #[ts(optional)]
1896 pub source: Option<String>,
1897}
1898
1899#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1901#[serde(deny_unknown_fields)]
1902#[ts(export)]
1903pub struct CreateVirtualBufferOptions {
1904 pub name: String,
1906 #[serde(default)]
1908 #[ts(optional)]
1909 pub mode: Option<String>,
1910 #[serde(default, rename = "readOnly")]
1912 #[ts(optional, rename = "readOnly")]
1913 pub read_only: Option<bool>,
1914 #[serde(default, rename = "showLineNumbers")]
1916 #[ts(optional, rename = "showLineNumbers")]
1917 pub show_line_numbers: Option<bool>,
1918 #[serde(default, rename = "showCursors")]
1920 #[ts(optional, rename = "showCursors")]
1921 pub show_cursors: Option<bool>,
1922 #[serde(default, rename = "editingDisabled")]
1924 #[ts(optional, rename = "editingDisabled")]
1925 pub editing_disabled: Option<bool>,
1926 #[serde(default, rename = "hiddenFromTabs")]
1928 #[ts(optional, rename = "hiddenFromTabs")]
1929 pub hidden_from_tabs: Option<bool>,
1930 #[serde(default)]
1932 #[ts(optional)]
1933 pub entries: Option<Vec<JsTextPropertyEntry>>,
1934}
1935
1936#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1938#[serde(deny_unknown_fields)]
1939#[ts(export)]
1940pub struct CreateVirtualBufferInSplitOptions {
1941 pub name: String,
1943 #[serde(default)]
1945 #[ts(optional)]
1946 pub mode: Option<String>,
1947 #[serde(default, rename = "readOnly")]
1949 #[ts(optional, rename = "readOnly")]
1950 pub read_only: Option<bool>,
1951 #[serde(default)]
1953 #[ts(optional)]
1954 pub ratio: Option<f32>,
1955 #[serde(default)]
1957 #[ts(optional)]
1958 pub direction: Option<String>,
1959 #[serde(default, rename = "panelId")]
1961 #[ts(optional, rename = "panelId")]
1962 pub panel_id: Option<String>,
1963 #[serde(default, rename = "showLineNumbers")]
1965 #[ts(optional, rename = "showLineNumbers")]
1966 pub show_line_numbers: Option<bool>,
1967 #[serde(default, rename = "showCursors")]
1969 #[ts(optional, rename = "showCursors")]
1970 pub show_cursors: Option<bool>,
1971 #[serde(default, rename = "editingDisabled")]
1973 #[ts(optional, rename = "editingDisabled")]
1974 pub editing_disabled: Option<bool>,
1975 #[serde(default, rename = "lineWrap")]
1977 #[ts(optional, rename = "lineWrap")]
1978 pub line_wrap: Option<bool>,
1979 #[serde(default)]
1981 #[ts(optional)]
1982 pub before: Option<bool>,
1983 #[serde(default)]
1985 #[ts(optional)]
1986 pub entries: Option<Vec<JsTextPropertyEntry>>,
1987}
1988
1989#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1991#[serde(deny_unknown_fields)]
1992#[ts(export)]
1993pub struct CreateVirtualBufferInExistingSplitOptions {
1994 pub name: String,
1996 #[serde(rename = "splitId")]
1998 #[ts(rename = "splitId")]
1999 pub split_id: usize,
2000 #[serde(default)]
2002 #[ts(optional)]
2003 pub mode: Option<String>,
2004 #[serde(default, rename = "readOnly")]
2006 #[ts(optional, rename = "readOnly")]
2007 pub read_only: Option<bool>,
2008 #[serde(default, rename = "showLineNumbers")]
2010 #[ts(optional, rename = "showLineNumbers")]
2011 pub show_line_numbers: Option<bool>,
2012 #[serde(default, rename = "showCursors")]
2014 #[ts(optional, rename = "showCursors")]
2015 pub show_cursors: Option<bool>,
2016 #[serde(default, rename = "editingDisabled")]
2018 #[ts(optional, rename = "editingDisabled")]
2019 pub editing_disabled: Option<bool>,
2020 #[serde(default, rename = "lineWrap")]
2022 #[ts(optional, rename = "lineWrap")]
2023 pub line_wrap: Option<bool>,
2024 #[serde(default)]
2026 #[ts(optional)]
2027 pub entries: Option<Vec<JsTextPropertyEntry>>,
2028}
2029
2030#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2032#[serde(deny_unknown_fields)]
2033#[ts(export)]
2034pub struct CreateTerminalOptions {
2035 #[serde(default)]
2037 #[ts(optional)]
2038 pub cwd: Option<String>,
2039 #[serde(default)]
2041 #[ts(optional)]
2042 pub direction: Option<String>,
2043 #[serde(default)]
2045 #[ts(optional)]
2046 pub ratio: Option<f32>,
2047 #[serde(default)]
2049 #[ts(optional)]
2050 pub focus: Option<bool>,
2051}
2052
2053#[derive(Debug, Clone, Serialize, TS)]
2058#[ts(export, type = "Array<Record<string, unknown>>")]
2059pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
2060
2061#[cfg(feature = "plugins")]
2063mod fromjs_impls {
2064 use super::*;
2065 use rquickjs::{Ctx, FromJs, Value};
2066
2067 impl<'js> FromJs<'js> for JsTextPropertyEntry {
2068 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2069 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2070 from: "object",
2071 to: "JsTextPropertyEntry",
2072 message: Some(e.to_string()),
2073 })
2074 }
2075 }
2076
2077 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
2078 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2079 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2080 from: "object",
2081 to: "CreateVirtualBufferOptions",
2082 message: Some(e.to_string()),
2083 })
2084 }
2085 }
2086
2087 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
2088 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2089 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2090 from: "object",
2091 to: "CreateVirtualBufferInSplitOptions",
2092 message: Some(e.to_string()),
2093 })
2094 }
2095 }
2096
2097 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
2098 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2099 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2100 from: "object",
2101 to: "CreateVirtualBufferInExistingSplitOptions",
2102 message: Some(e.to_string()),
2103 })
2104 }
2105 }
2106
2107 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
2108 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
2109 rquickjs_serde::to_value(ctx.clone(), &self.0)
2110 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
2111 }
2112 }
2113
2114 impl<'js> FromJs<'js> for ActionSpec {
2117 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2118 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2119 from: "object",
2120 to: "ActionSpec",
2121 message: Some(e.to_string()),
2122 })
2123 }
2124 }
2125
2126 impl<'js> FromJs<'js> for ActionPopupAction {
2127 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2128 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2129 from: "object",
2130 to: "ActionPopupAction",
2131 message: Some(e.to_string()),
2132 })
2133 }
2134 }
2135
2136 impl<'js> FromJs<'js> for ActionPopupOptions {
2137 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2138 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2139 from: "object",
2140 to: "ActionPopupOptions",
2141 message: Some(e.to_string()),
2142 })
2143 }
2144 }
2145
2146 impl<'js> FromJs<'js> for ViewTokenWire {
2147 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2148 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2149 from: "object",
2150 to: "ViewTokenWire",
2151 message: Some(e.to_string()),
2152 })
2153 }
2154 }
2155
2156 impl<'js> FromJs<'js> for ViewTokenStyle {
2157 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2158 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2159 from: "object",
2160 to: "ViewTokenStyle",
2161 message: Some(e.to_string()),
2162 })
2163 }
2164 }
2165
2166 impl<'js> FromJs<'js> for LayoutHints {
2167 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2168 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2169 from: "object",
2170 to: "LayoutHints",
2171 message: Some(e.to_string()),
2172 })
2173 }
2174 }
2175
2176 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
2177 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2178 let json: serde_json::Value =
2180 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2181 from: "object",
2182 to: "CreateCompositeBufferOptions (json)",
2183 message: Some(e.to_string()),
2184 })?;
2185 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
2186 from: "json",
2187 to: "CreateCompositeBufferOptions",
2188 message: Some(e.to_string()),
2189 })
2190 }
2191 }
2192
2193 impl<'js> FromJs<'js> for CompositeHunk {
2194 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2195 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2196 from: "object",
2197 to: "CompositeHunk",
2198 message: Some(e.to_string()),
2199 })
2200 }
2201 }
2202
2203 impl<'js> FromJs<'js> for LanguagePackConfig {
2204 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2205 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2206 from: "object",
2207 to: "LanguagePackConfig",
2208 message: Some(e.to_string()),
2209 })
2210 }
2211 }
2212
2213 impl<'js> FromJs<'js> for LspServerPackConfig {
2214 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2215 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2216 from: "object",
2217 to: "LspServerPackConfig",
2218 message: Some(e.to_string()),
2219 })
2220 }
2221 }
2222
2223 impl<'js> FromJs<'js> for CreateTerminalOptions {
2224 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2225 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2226 from: "object",
2227 to: "CreateTerminalOptions",
2228 message: Some(e.to_string()),
2229 })
2230 }
2231 }
2232}
2233
2234pub struct PluginApi {
2236 hooks: Arc<RwLock<HookRegistry>>,
2238
2239 commands: Arc<RwLock<CommandRegistry>>,
2241
2242 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2244
2245 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2247}
2248
2249impl PluginApi {
2250 pub fn new(
2252 hooks: Arc<RwLock<HookRegistry>>,
2253 commands: Arc<RwLock<CommandRegistry>>,
2254 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2255 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2256 ) -> Self {
2257 Self {
2258 hooks,
2259 commands,
2260 command_sender,
2261 state_snapshot,
2262 }
2263 }
2264
2265 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
2267 let mut hooks = self.hooks.write().unwrap();
2268 hooks.add_hook(hook_name, callback);
2269 }
2270
2271 pub fn unregister_hooks(&self, hook_name: &str) {
2273 let mut hooks = self.hooks.write().unwrap();
2274 hooks.remove_hooks(hook_name);
2275 }
2276
2277 pub fn register_command(&self, command: Command) {
2279 let commands = self.commands.read().unwrap();
2280 commands.register(command);
2281 }
2282
2283 pub fn unregister_command(&self, name: &str) {
2285 let commands = self.commands.read().unwrap();
2286 commands.unregister(name);
2287 }
2288
2289 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
2291 self.command_sender
2292 .send(command)
2293 .map_err(|e| format!("Failed to send command: {}", e))
2294 }
2295
2296 pub fn insert_text(
2298 &self,
2299 buffer_id: BufferId,
2300 position: usize,
2301 text: String,
2302 ) -> Result<(), String> {
2303 self.send_command(PluginCommand::InsertText {
2304 buffer_id,
2305 position,
2306 text,
2307 })
2308 }
2309
2310 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
2312 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
2313 }
2314
2315 pub fn add_overlay(
2323 &self,
2324 buffer_id: BufferId,
2325 namespace: Option<String>,
2326 range: Range<usize>,
2327 options: OverlayOptions,
2328 ) -> Result<(), String> {
2329 self.send_command(PluginCommand::AddOverlay {
2330 buffer_id,
2331 namespace: namespace.map(OverlayNamespace::from_string),
2332 range,
2333 options,
2334 })
2335 }
2336
2337 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
2339 self.send_command(PluginCommand::RemoveOverlay {
2340 buffer_id,
2341 handle: OverlayHandle::from_string(handle),
2342 })
2343 }
2344
2345 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
2347 self.send_command(PluginCommand::ClearNamespace {
2348 buffer_id,
2349 namespace: OverlayNamespace::from_string(namespace),
2350 })
2351 }
2352
2353 pub fn clear_overlays_in_range(
2356 &self,
2357 buffer_id: BufferId,
2358 start: usize,
2359 end: usize,
2360 ) -> Result<(), String> {
2361 self.send_command(PluginCommand::ClearOverlaysInRange {
2362 buffer_id,
2363 start,
2364 end,
2365 })
2366 }
2367
2368 pub fn set_status(&self, message: String) -> Result<(), String> {
2370 self.send_command(PluginCommand::SetStatus { message })
2371 }
2372
2373 pub fn open_file_at_location(
2376 &self,
2377 path: PathBuf,
2378 line: Option<usize>,
2379 column: Option<usize>,
2380 ) -> Result<(), String> {
2381 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
2382 }
2383
2384 pub fn open_file_in_split(
2389 &self,
2390 split_id: usize,
2391 path: PathBuf,
2392 line: Option<usize>,
2393 column: Option<usize>,
2394 ) -> Result<(), String> {
2395 self.send_command(PluginCommand::OpenFileInSplit {
2396 split_id,
2397 path,
2398 line,
2399 column,
2400 })
2401 }
2402
2403 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
2406 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
2407 }
2408
2409 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
2412 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
2413 }
2414
2415 pub fn set_prompt_input_sync(&self, sync: bool) -> Result<(), String> {
2417 self.send_command(PluginCommand::SetPromptInputSync { sync })
2418 }
2419
2420 pub fn add_menu_item(
2422 &self,
2423 menu_label: String,
2424 item: MenuItem,
2425 position: MenuPosition,
2426 ) -> Result<(), String> {
2427 self.send_command(PluginCommand::AddMenuItem {
2428 menu_label,
2429 item,
2430 position,
2431 })
2432 }
2433
2434 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
2436 self.send_command(PluginCommand::AddMenu { menu, position })
2437 }
2438
2439 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
2441 self.send_command(PluginCommand::RemoveMenuItem {
2442 menu_label,
2443 item_label,
2444 })
2445 }
2446
2447 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
2449 self.send_command(PluginCommand::RemoveMenu { menu_label })
2450 }
2451
2452 pub fn create_virtual_buffer(
2459 &self,
2460 name: String,
2461 mode: String,
2462 read_only: bool,
2463 ) -> Result<(), String> {
2464 self.send_command(PluginCommand::CreateVirtualBuffer {
2465 name,
2466 mode,
2467 read_only,
2468 })
2469 }
2470
2471 pub fn create_virtual_buffer_with_content(
2477 &self,
2478 name: String,
2479 mode: String,
2480 read_only: bool,
2481 entries: Vec<TextPropertyEntry>,
2482 ) -> Result<(), String> {
2483 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
2484 name,
2485 mode,
2486 read_only,
2487 entries,
2488 show_line_numbers: true,
2489 show_cursors: true,
2490 editing_disabled: false,
2491 hidden_from_tabs: false,
2492 request_id: None,
2493 })
2494 }
2495
2496 pub fn set_virtual_buffer_content(
2500 &self,
2501 buffer_id: BufferId,
2502 entries: Vec<TextPropertyEntry>,
2503 ) -> Result<(), String> {
2504 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
2505 }
2506
2507 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
2511 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
2512 }
2513
2514 pub fn define_mode(
2519 &self,
2520 name: String,
2521 parent: Option<String>,
2522 bindings: Vec<(String, String)>,
2523 read_only: bool,
2524 ) -> Result<(), String> {
2525 self.send_command(PluginCommand::DefineMode {
2526 name,
2527 parent,
2528 bindings,
2529 read_only,
2530 })
2531 }
2532
2533 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
2535 self.send_command(PluginCommand::ShowBuffer { buffer_id })
2536 }
2537
2538 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
2540 self.send_command(PluginCommand::SetSplitScroll {
2541 split_id: SplitId(split_id),
2542 top_byte,
2543 })
2544 }
2545
2546 pub fn get_highlights(
2548 &self,
2549 buffer_id: BufferId,
2550 range: Range<usize>,
2551 request_id: u64,
2552 ) -> Result<(), String> {
2553 self.send_command(PluginCommand::RequestHighlights {
2554 buffer_id,
2555 range,
2556 request_id,
2557 })
2558 }
2559
2560 pub fn get_active_buffer_id(&self) -> BufferId {
2564 let snapshot = self.state_snapshot.read().unwrap();
2565 snapshot.active_buffer_id
2566 }
2567
2568 pub fn get_active_split_id(&self) -> usize {
2570 let snapshot = self.state_snapshot.read().unwrap();
2571 snapshot.active_split_id
2572 }
2573
2574 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
2576 let snapshot = self.state_snapshot.read().unwrap();
2577 snapshot.buffers.get(&buffer_id).cloned()
2578 }
2579
2580 pub fn list_buffers(&self) -> Vec<BufferInfo> {
2582 let snapshot = self.state_snapshot.read().unwrap();
2583 snapshot.buffers.values().cloned().collect()
2584 }
2585
2586 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
2588 let snapshot = self.state_snapshot.read().unwrap();
2589 snapshot.primary_cursor.clone()
2590 }
2591
2592 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
2594 let snapshot = self.state_snapshot.read().unwrap();
2595 snapshot.all_cursors.clone()
2596 }
2597
2598 pub fn get_viewport(&self) -> Option<ViewportInfo> {
2600 let snapshot = self.state_snapshot.read().unwrap();
2601 snapshot.viewport.clone()
2602 }
2603
2604 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
2606 Arc::clone(&self.state_snapshot)
2607 }
2608}
2609
2610impl Clone for PluginApi {
2611 fn clone(&self) -> Self {
2612 Self {
2613 hooks: Arc::clone(&self.hooks),
2614 commands: Arc::clone(&self.commands),
2615 command_sender: self.command_sender.clone(),
2616 state_snapshot: Arc::clone(&self.state_snapshot),
2617 }
2618 }
2619}
2620
2621#[cfg(test)]
2622mod tests {
2623 use super::*;
2624
2625 #[test]
2626 fn test_plugin_api_creation() {
2627 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2628 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2629 let (tx, _rx) = std::sync::mpsc::channel();
2630 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2631
2632 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2633
2634 let _clone = api.clone();
2636 }
2637
2638 #[test]
2639 fn test_register_hook() {
2640 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2641 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2642 let (tx, _rx) = std::sync::mpsc::channel();
2643 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2644
2645 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
2646
2647 api.register_hook("test-hook", Box::new(|_| true));
2648
2649 let hook_registry = hooks.read().unwrap();
2650 assert_eq!(hook_registry.hook_count("test-hook"), 1);
2651 }
2652
2653 #[test]
2654 fn test_send_command() {
2655 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2656 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2657 let (tx, rx) = std::sync::mpsc::channel();
2658 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2659
2660 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2661
2662 let result = api.insert_text(BufferId(1), 0, "test".to_string());
2663 assert!(result.is_ok());
2664
2665 let received = rx.try_recv();
2667 assert!(received.is_ok());
2668
2669 match received.unwrap() {
2670 PluginCommand::InsertText {
2671 buffer_id,
2672 position,
2673 text,
2674 } => {
2675 assert_eq!(buffer_id.0, 1);
2676 assert_eq!(position, 0);
2677 assert_eq!(text, "test");
2678 }
2679 _ => panic!("Wrong command type"),
2680 }
2681 }
2682
2683 #[test]
2684 fn test_add_overlay_command() {
2685 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2686 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2687 let (tx, rx) = std::sync::mpsc::channel();
2688 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2689
2690 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2691
2692 let result = api.add_overlay(
2693 BufferId(1),
2694 Some("test-overlay".to_string()),
2695 0..10,
2696 OverlayOptions {
2697 fg: Some(OverlayColorSpec::ThemeKey("ui.status_bar_fg".to_string())),
2698 bg: None,
2699 underline: true,
2700 bold: false,
2701 italic: false,
2702 strikethrough: false,
2703 extend_to_line_end: false,
2704 url: None,
2705 },
2706 );
2707 assert!(result.is_ok());
2708
2709 let received = rx.try_recv().unwrap();
2710 match received {
2711 PluginCommand::AddOverlay {
2712 buffer_id,
2713 namespace,
2714 range,
2715 options,
2716 } => {
2717 assert_eq!(buffer_id.0, 1);
2718 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
2719 assert_eq!(range, 0..10);
2720 assert!(matches!(
2721 options.fg,
2722 Some(OverlayColorSpec::ThemeKey(ref k)) if k == "ui.status_bar_fg"
2723 ));
2724 assert!(options.bg.is_none());
2725 assert!(options.underline);
2726 assert!(!options.bold);
2727 assert!(!options.italic);
2728 assert!(!options.extend_to_line_end);
2729 }
2730 _ => panic!("Wrong command type"),
2731 }
2732 }
2733
2734 #[test]
2735 fn test_set_status_command() {
2736 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2737 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2738 let (tx, rx) = std::sync::mpsc::channel();
2739 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2740
2741 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2742
2743 let result = api.set_status("Test status".to_string());
2744 assert!(result.is_ok());
2745
2746 let received = rx.try_recv().unwrap();
2747 match received {
2748 PluginCommand::SetStatus { message } => {
2749 assert_eq!(message, "Test status");
2750 }
2751 _ => panic!("Wrong command type"),
2752 }
2753 }
2754
2755 #[test]
2756 fn test_get_active_buffer_id() {
2757 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2758 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2759 let (tx, _rx) = std::sync::mpsc::channel();
2760 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2761
2762 {
2764 let mut snapshot = state_snapshot.write().unwrap();
2765 snapshot.active_buffer_id = BufferId(5);
2766 }
2767
2768 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2769
2770 let active_id = api.get_active_buffer_id();
2771 assert_eq!(active_id.0, 5);
2772 }
2773
2774 #[test]
2775 fn test_get_buffer_info() {
2776 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2777 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2778 let (tx, _rx) = std::sync::mpsc::channel();
2779 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2780
2781 {
2783 let mut snapshot = state_snapshot.write().unwrap();
2784 let buffer_info = BufferInfo {
2785 id: BufferId(1),
2786 path: Some(std::path::PathBuf::from("/test/file.txt")),
2787 modified: true,
2788 length: 100,
2789 is_virtual: false,
2790 view_mode: "source".to_string(),
2791 is_composing_in_any_split: false,
2792 compose_width: None,
2793 language: "text".to_string(),
2794 };
2795 snapshot.buffers.insert(BufferId(1), buffer_info);
2796 }
2797
2798 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2799
2800 let info = api.get_buffer_info(BufferId(1));
2801 assert!(info.is_some());
2802 let info = info.unwrap();
2803 assert_eq!(info.id.0, 1);
2804 assert_eq!(
2805 info.path.as_ref().unwrap().to_str().unwrap(),
2806 "/test/file.txt"
2807 );
2808 assert!(info.modified);
2809 assert_eq!(info.length, 100);
2810
2811 let no_info = api.get_buffer_info(BufferId(999));
2813 assert!(no_info.is_none());
2814 }
2815
2816 #[test]
2817 fn test_list_buffers() {
2818 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2819 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2820 let (tx, _rx) = std::sync::mpsc::channel();
2821 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2822
2823 {
2825 let mut snapshot = state_snapshot.write().unwrap();
2826 snapshot.buffers.insert(
2827 BufferId(1),
2828 BufferInfo {
2829 id: BufferId(1),
2830 path: Some(std::path::PathBuf::from("/file1.txt")),
2831 modified: false,
2832 length: 50,
2833 is_virtual: false,
2834 view_mode: "source".to_string(),
2835 is_composing_in_any_split: false,
2836 compose_width: None,
2837 language: "text".to_string(),
2838 },
2839 );
2840 snapshot.buffers.insert(
2841 BufferId(2),
2842 BufferInfo {
2843 id: BufferId(2),
2844 path: Some(std::path::PathBuf::from("/file2.txt")),
2845 modified: true,
2846 length: 100,
2847 is_virtual: false,
2848 view_mode: "source".to_string(),
2849 is_composing_in_any_split: false,
2850 compose_width: None,
2851 language: "text".to_string(),
2852 },
2853 );
2854 snapshot.buffers.insert(
2855 BufferId(3),
2856 BufferInfo {
2857 id: BufferId(3),
2858 path: None,
2859 modified: false,
2860 length: 0,
2861 is_virtual: true,
2862 view_mode: "source".to_string(),
2863 is_composing_in_any_split: false,
2864 compose_width: None,
2865 language: "text".to_string(),
2866 },
2867 );
2868 }
2869
2870 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2871
2872 let buffers = api.list_buffers();
2873 assert_eq!(buffers.len(), 3);
2874
2875 assert!(buffers.iter().any(|b| b.id.0 == 1));
2877 assert!(buffers.iter().any(|b| b.id.0 == 2));
2878 assert!(buffers.iter().any(|b| b.id.0 == 3));
2879 }
2880
2881 #[test]
2882 fn test_get_primary_cursor() {
2883 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2884 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2885 let (tx, _rx) = std::sync::mpsc::channel();
2886 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2887
2888 {
2890 let mut snapshot = state_snapshot.write().unwrap();
2891 snapshot.primary_cursor = Some(CursorInfo {
2892 position: 42,
2893 selection: Some(10..42),
2894 });
2895 }
2896
2897 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2898
2899 let cursor = api.get_primary_cursor();
2900 assert!(cursor.is_some());
2901 let cursor = cursor.unwrap();
2902 assert_eq!(cursor.position, 42);
2903 assert_eq!(cursor.selection, Some(10..42));
2904 }
2905
2906 #[test]
2907 fn test_get_all_cursors() {
2908 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2909 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2910 let (tx, _rx) = std::sync::mpsc::channel();
2911 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2912
2913 {
2915 let mut snapshot = state_snapshot.write().unwrap();
2916 snapshot.all_cursors = vec![
2917 CursorInfo {
2918 position: 10,
2919 selection: None,
2920 },
2921 CursorInfo {
2922 position: 20,
2923 selection: Some(15..20),
2924 },
2925 CursorInfo {
2926 position: 30,
2927 selection: Some(25..30),
2928 },
2929 ];
2930 }
2931
2932 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2933
2934 let cursors = api.get_all_cursors();
2935 assert_eq!(cursors.len(), 3);
2936 assert_eq!(cursors[0].position, 10);
2937 assert_eq!(cursors[0].selection, None);
2938 assert_eq!(cursors[1].position, 20);
2939 assert_eq!(cursors[1].selection, Some(15..20));
2940 assert_eq!(cursors[2].position, 30);
2941 assert_eq!(cursors[2].selection, Some(25..30));
2942 }
2943
2944 #[test]
2945 fn test_get_viewport() {
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 snapshot.viewport = Some(ViewportInfo {
2955 top_byte: 100,
2956 top_line: Some(5),
2957 left_column: 5,
2958 width: 80,
2959 height: 24,
2960 });
2961 }
2962
2963 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2964
2965 let viewport = api.get_viewport();
2966 assert!(viewport.is_some());
2967 let viewport = viewport.unwrap();
2968 assert_eq!(viewport.top_byte, 100);
2969 assert_eq!(viewport.left_column, 5);
2970 assert_eq!(viewport.width, 80);
2971 assert_eq!(viewport.height, 24);
2972 }
2973
2974 #[test]
2975 fn test_composite_buffer_options_rejects_unknown_fields() {
2976 let valid_json = r#"{
2978 "name": "test",
2979 "mode": "diff",
2980 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2981 "sources": [{"bufferId": 1, "label": "old"}]
2982 }"#;
2983 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
2984 assert!(
2985 result.is_ok(),
2986 "Valid JSON should parse: {:?}",
2987 result.err()
2988 );
2989
2990 let invalid_json = r#"{
2992 "name": "test",
2993 "mode": "diff",
2994 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2995 "sources": [{"buffer_id": 1, "label": "old"}]
2996 }"#;
2997 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
2998 assert!(
2999 result.is_err(),
3000 "JSON with unknown field should fail to parse"
3001 );
3002 let err = result.unwrap_err().to_string();
3003 assert!(
3004 err.contains("unknown field") || err.contains("buffer_id"),
3005 "Error should mention unknown field: {}",
3006 err
3007 );
3008 }
3009
3010 #[test]
3011 fn test_composite_hunk_rejects_unknown_fields() {
3012 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3014 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
3015 assert!(
3016 result.is_ok(),
3017 "Valid JSON should parse: {:?}",
3018 result.err()
3019 );
3020
3021 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3023 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
3024 assert!(
3025 result.is_err(),
3026 "JSON with unknown field should fail to parse"
3027 );
3028 let err = result.unwrap_err().to_string();
3029 assert!(
3030 err.contains("unknown field") || err.contains("old_start"),
3031 "Error should mention unknown field: {}",
3032 err
3033 );
3034 }
3035
3036 #[test]
3037 fn test_plugin_response_line_end_position() {
3038 let response = PluginResponse::LineEndPosition {
3039 request_id: 42,
3040 position: Some(100),
3041 };
3042 let json = serde_json::to_string(&response).unwrap();
3043 assert!(json.contains("LineEndPosition"));
3044 assert!(json.contains("42"));
3045 assert!(json.contains("100"));
3046
3047 let response_none = PluginResponse::LineEndPosition {
3049 request_id: 1,
3050 position: None,
3051 };
3052 let json_none = serde_json::to_string(&response_none).unwrap();
3053 assert!(json_none.contains("null"));
3054 }
3055
3056 #[test]
3057 fn test_plugin_response_buffer_line_count() {
3058 let response = PluginResponse::BufferLineCount {
3059 request_id: 99,
3060 count: Some(500),
3061 };
3062 let json = serde_json::to_string(&response).unwrap();
3063 assert!(json.contains("BufferLineCount"));
3064 assert!(json.contains("99"));
3065 assert!(json.contains("500"));
3066 }
3067
3068 #[test]
3069 fn test_plugin_command_get_line_end_position() {
3070 let command = PluginCommand::GetLineEndPosition {
3071 buffer_id: BufferId(1),
3072 line: 10,
3073 request_id: 123,
3074 };
3075 let json = serde_json::to_string(&command).unwrap();
3076 assert!(json.contains("GetLineEndPosition"));
3077 assert!(json.contains("10"));
3078 }
3079
3080 #[test]
3081 fn test_plugin_command_get_buffer_line_count() {
3082 let command = PluginCommand::GetBufferLineCount {
3083 buffer_id: BufferId(0),
3084 request_id: 456,
3085 };
3086 let json = serde_json::to_string(&command).unwrap();
3087 assert!(json.contains("GetBufferLineCount"));
3088 assert!(json.contains("456"));
3089 }
3090
3091 #[test]
3092 fn test_plugin_command_scroll_to_line_center() {
3093 let command = PluginCommand::ScrollToLineCenter {
3094 split_id: SplitId(1),
3095 buffer_id: BufferId(2),
3096 line: 50,
3097 };
3098 let json = serde_json::to_string(&command).unwrap();
3099 assert!(json.contains("ScrollToLineCenter"));
3100 assert!(json.contains("50"));
3101 }
3102}