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 top_line: Option<usize>,
394 pub left_column: usize,
396 pub width: u16,
398 pub height: u16,
400}
401
402#[derive(Debug, Clone, Serialize, Deserialize, TS)]
404#[serde(rename_all = "camelCase")]
405#[ts(export, rename_all = "camelCase")]
406pub struct LayoutHints {
407 pub compose_width: Option<u16>,
409 pub column_guides: Option<Vec<u16>>,
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize, TS)]
428#[serde(untagged)]
429#[ts(export)]
430pub enum OverlayColorSpec {
431 #[ts(type = "[number, number, number]")]
433 Rgb(u8, u8, u8),
434 ThemeKey(String),
436}
437
438impl OverlayColorSpec {
439 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
441 Self::Rgb(r, g, b)
442 }
443
444 pub fn theme_key(key: impl Into<String>) -> Self {
446 Self::ThemeKey(key.into())
447 }
448
449 pub fn as_rgb(&self) -> Option<(u8, u8, u8)> {
451 match self {
452 Self::Rgb(r, g, b) => Some((*r, *g, *b)),
453 Self::ThemeKey(_) => None,
454 }
455 }
456
457 pub fn as_theme_key(&self) -> Option<&str> {
459 match self {
460 Self::ThemeKey(key) => Some(key),
461 Self::Rgb(_, _, _) => None,
462 }
463 }
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize, TS)]
471#[serde(deny_unknown_fields, rename_all = "camelCase")]
472#[ts(export, rename_all = "camelCase")]
473#[derive(Default)]
474pub struct OverlayOptions {
475 #[serde(default, skip_serializing_if = "Option::is_none")]
477 pub fg: Option<OverlayColorSpec>,
478
479 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub bg: Option<OverlayColorSpec>,
482
483 #[serde(default)]
485 pub underline: bool,
486
487 #[serde(default)]
489 pub bold: bool,
490
491 #[serde(default)]
493 pub italic: bool,
494
495 #[serde(default)]
497 pub strikethrough: bool,
498
499 #[serde(default)]
501 pub extend_to_line_end: bool,
502
503 #[serde(default, skip_serializing_if = "Option::is_none")]
507 pub url: Option<String>,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize, TS)]
516#[serde(deny_unknown_fields)]
517#[ts(export, rename = "TsCompositeLayoutConfig")]
518pub struct CompositeLayoutConfig {
519 #[serde(rename = "type")]
521 #[ts(rename = "type")]
522 pub layout_type: String,
523 #[serde(default)]
525 pub ratios: Option<Vec<f32>>,
526 #[serde(default = "default_true", rename = "showSeparator")]
528 #[ts(rename = "showSeparator")]
529 pub show_separator: bool,
530 #[serde(default)]
532 pub spacing: Option<u16>,
533}
534
535fn default_true() -> bool {
536 true
537}
538
539#[derive(Debug, Clone, Serialize, Deserialize, TS)]
541#[serde(deny_unknown_fields)]
542#[ts(export, rename = "TsCompositeSourceConfig")]
543pub struct CompositeSourceConfig {
544 #[serde(rename = "bufferId")]
546 #[ts(rename = "bufferId")]
547 pub buffer_id: usize,
548 pub label: String,
550 #[serde(default)]
552 pub editable: bool,
553 #[serde(default)]
555 pub style: Option<CompositePaneStyle>,
556}
557
558#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
560#[serde(deny_unknown_fields)]
561#[ts(export, rename = "TsCompositePaneStyle")]
562pub struct CompositePaneStyle {
563 #[serde(default, rename = "addBg")]
566 #[ts(rename = "addBg", type = "[number, number, number] | null")]
567 pub add_bg: Option<[u8; 3]>,
568 #[serde(default, rename = "removeBg")]
570 #[ts(rename = "removeBg", type = "[number, number, number] | null")]
571 pub remove_bg: Option<[u8; 3]>,
572 #[serde(default, rename = "modifyBg")]
574 #[ts(rename = "modifyBg", type = "[number, number, number] | null")]
575 pub modify_bg: Option<[u8; 3]>,
576 #[serde(default, rename = "gutterStyle")]
578 #[ts(rename = "gutterStyle")]
579 pub gutter_style: Option<String>,
580}
581
582#[derive(Debug, Clone, Serialize, Deserialize, TS)]
584#[serde(deny_unknown_fields)]
585#[ts(export, rename = "TsCompositeHunk")]
586pub struct CompositeHunk {
587 #[serde(rename = "oldStart")]
589 #[ts(rename = "oldStart")]
590 pub old_start: usize,
591 #[serde(rename = "oldCount")]
593 #[ts(rename = "oldCount")]
594 pub old_count: usize,
595 #[serde(rename = "newStart")]
597 #[ts(rename = "newStart")]
598 pub new_start: usize,
599 #[serde(rename = "newCount")]
601 #[ts(rename = "newCount")]
602 pub new_count: usize,
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize, TS)]
607#[serde(deny_unknown_fields)]
608#[ts(export, rename = "TsCreateCompositeBufferOptions")]
609pub struct CreateCompositeBufferOptions {
610 #[serde(default)]
612 pub name: String,
613 #[serde(default)]
615 pub mode: String,
616 pub layout: CompositeLayoutConfig,
618 pub sources: Vec<CompositeSourceConfig>,
620 #[serde(default)]
622 pub hunks: Option<Vec<CompositeHunk>>,
623}
624
625#[derive(Debug, Clone, Serialize, Deserialize, TS)]
627#[ts(export)]
628pub enum ViewTokenWireKind {
629 Text(String),
630 Newline,
631 Space,
632 Break,
635 BinaryByte(u8),
639}
640
641#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
647#[serde(deny_unknown_fields)]
648#[ts(export)]
649pub struct ViewTokenStyle {
650 #[serde(default)]
652 #[ts(type = "[number, number, number] | null")]
653 pub fg: Option<(u8, u8, u8)>,
654 #[serde(default)]
656 #[ts(type = "[number, number, number] | null")]
657 pub bg: Option<(u8, u8, u8)>,
658 #[serde(default)]
660 pub bold: bool,
661 #[serde(default)]
663 pub italic: bool,
664}
665
666#[derive(Debug, Clone, Serialize, Deserialize, TS)]
668#[serde(deny_unknown_fields)]
669#[ts(export)]
670pub struct ViewTokenWire {
671 #[ts(type = "number | null")]
673 pub source_offset: Option<usize>,
674 pub kind: ViewTokenWireKind,
676 #[serde(default)]
678 #[ts(optional)]
679 pub style: Option<ViewTokenStyle>,
680}
681
682#[derive(Debug, Clone, Serialize, Deserialize, TS)]
684#[ts(export)]
685pub struct ViewTransformPayload {
686 pub range: Range<usize>,
688 pub tokens: Vec<ViewTokenWire>,
690 pub layout_hints: Option<LayoutHints>,
692}
693
694#[derive(Debug, Clone, Serialize, Deserialize, TS)]
697#[ts(export)]
698pub struct EditorStateSnapshot {
699 pub active_buffer_id: BufferId,
701 pub active_split_id: usize,
703 pub buffers: HashMap<BufferId, BufferInfo>,
705 pub buffer_saved_diffs: HashMap<BufferId, BufferSavedDiff>,
707 pub primary_cursor: Option<CursorInfo>,
709 pub all_cursors: Vec<CursorInfo>,
711 pub viewport: Option<ViewportInfo>,
713 pub buffer_cursor_positions: HashMap<BufferId, usize>,
715 pub buffer_text_properties: HashMap<BufferId, Vec<TextProperty>>,
717 pub selected_text: Option<String>,
720 pub clipboard: String,
722 pub working_dir: PathBuf,
724 #[ts(type = "any")]
727 pub diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
728 #[ts(type = "any")]
731 pub folding_ranges: HashMap<String, Vec<lsp_types::FoldingRange>>,
732 #[ts(type = "any")]
735 pub config: serde_json::Value,
736 #[ts(type = "any")]
739 pub user_config: serde_json::Value,
740 pub editor_mode: Option<String>,
743
744 #[ts(type = "any")]
748 pub plugin_view_states: HashMap<BufferId, HashMap<String, serde_json::Value>>,
749
750 #[serde(skip)]
753 #[ts(skip)]
754 pub plugin_view_states_split: usize,
755}
756
757impl EditorStateSnapshot {
758 pub fn new() -> Self {
759 Self {
760 active_buffer_id: BufferId(0),
761 active_split_id: 0,
762 buffers: HashMap::new(),
763 buffer_saved_diffs: HashMap::new(),
764 primary_cursor: None,
765 all_cursors: Vec::new(),
766 viewport: None,
767 buffer_cursor_positions: HashMap::new(),
768 buffer_text_properties: HashMap::new(),
769 selected_text: None,
770 clipboard: String::new(),
771 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
772 diagnostics: HashMap::new(),
773 folding_ranges: HashMap::new(),
774 config: serde_json::Value::Null,
775 user_config: serde_json::Value::Null,
776 editor_mode: None,
777 plugin_view_states: HashMap::new(),
778 plugin_view_states_split: 0,
779 }
780 }
781}
782
783impl Default for EditorStateSnapshot {
784 fn default() -> Self {
785 Self::new()
786 }
787}
788
789#[derive(Debug, Clone, Serialize, Deserialize, TS)]
791#[ts(export)]
792pub enum MenuPosition {
793 Top,
795 Bottom,
797 Before(String),
799 After(String),
801}
802
803#[derive(Debug, Clone, Serialize, Deserialize, TS)]
805#[ts(export)]
806pub enum PluginCommand {
807 InsertText {
809 buffer_id: BufferId,
810 position: usize,
811 text: String,
812 },
813
814 DeleteRange {
816 buffer_id: BufferId,
817 range: Range<usize>,
818 },
819
820 AddOverlay {
825 buffer_id: BufferId,
826 namespace: Option<OverlayNamespace>,
827 range: Range<usize>,
828 options: OverlayOptions,
830 },
831
832 RemoveOverlay {
834 buffer_id: BufferId,
835 handle: OverlayHandle,
836 },
837
838 SetStatus { message: String },
840
841 ApplyTheme { theme_name: String },
843
844 ReloadConfig,
847
848 RegisterCommand { command: Command },
850
851 UnregisterCommand { name: String },
853
854 OpenFileInBackground { path: PathBuf },
856
857 InsertAtCursor { text: String },
859
860 SpawnProcess {
862 command: String,
863 args: Vec<String>,
864 cwd: Option<String>,
865 callback_id: JsCallbackId,
866 },
867
868 Delay {
870 callback_id: JsCallbackId,
871 duration_ms: u64,
872 },
873
874 SpawnBackgroundProcess {
878 process_id: u64,
880 command: String,
882 args: Vec<String>,
884 cwd: Option<String>,
886 callback_id: JsCallbackId,
888 },
889
890 KillBackgroundProcess { process_id: u64 },
892
893 SpawnProcessWait {
896 process_id: u64,
898 callback_id: JsCallbackId,
900 },
901
902 SetLayoutHints {
904 buffer_id: BufferId,
905 split_id: Option<SplitId>,
906 range: Range<usize>,
907 hints: LayoutHints,
908 },
909
910 SetLineNumbers { buffer_id: BufferId, enabled: bool },
912
913 SetViewMode { buffer_id: BufferId, mode: String },
915
916 SetLineWrap {
918 buffer_id: BufferId,
919 split_id: Option<SplitId>,
920 enabled: bool,
921 },
922
923 SubmitViewTransform {
925 buffer_id: BufferId,
926 split_id: Option<SplitId>,
927 payload: ViewTransformPayload,
928 },
929
930 ClearViewTransform {
932 buffer_id: BufferId,
933 split_id: Option<SplitId>,
934 },
935
936 SetViewState {
939 buffer_id: BufferId,
940 key: String,
941 #[ts(type = "any")]
942 value: Option<serde_json::Value>,
943 },
944
945 ClearAllOverlays { buffer_id: BufferId },
947
948 ClearNamespace {
950 buffer_id: BufferId,
951 namespace: OverlayNamespace,
952 },
953
954 ClearOverlaysInRange {
957 buffer_id: BufferId,
958 start: usize,
959 end: usize,
960 },
961
962 AddVirtualText {
965 buffer_id: BufferId,
966 virtual_text_id: String,
967 position: usize,
968 text: String,
969 color: (u8, u8, u8),
970 use_bg: bool, before: bool, },
973
974 RemoveVirtualText {
976 buffer_id: BufferId,
977 virtual_text_id: String,
978 },
979
980 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
982
983 ClearVirtualTexts { buffer_id: BufferId },
985
986 AddVirtualLine {
990 buffer_id: BufferId,
991 position: usize,
993 text: String,
995 fg_color: (u8, u8, u8),
997 bg_color: Option<(u8, u8, u8)>,
999 above: bool,
1001 namespace: String,
1003 priority: i32,
1005 },
1006
1007 ClearVirtualTextNamespace {
1010 buffer_id: BufferId,
1011 namespace: String,
1012 },
1013
1014 AddConceal {
1017 buffer_id: BufferId,
1018 namespace: OverlayNamespace,
1020 start: usize,
1022 end: usize,
1023 replacement: Option<String>,
1025 },
1026
1027 ClearConcealNamespace {
1029 buffer_id: BufferId,
1030 namespace: OverlayNamespace,
1031 },
1032
1033 ClearConcealsInRange {
1036 buffer_id: BufferId,
1037 start: usize,
1038 end: usize,
1039 },
1040
1041 AddSoftBreak {
1045 buffer_id: BufferId,
1046 namespace: OverlayNamespace,
1048 position: usize,
1050 indent: u16,
1052 },
1053
1054 ClearSoftBreakNamespace {
1056 buffer_id: BufferId,
1057 namespace: OverlayNamespace,
1058 },
1059
1060 ClearSoftBreaksInRange {
1062 buffer_id: BufferId,
1063 start: usize,
1064 end: usize,
1065 },
1066
1067 RefreshLines { buffer_id: BufferId },
1069
1070 RefreshAllLines,
1074
1075 HookCompleted { hook_name: String },
1079
1080 SetLineIndicator {
1083 buffer_id: BufferId,
1084 line: usize,
1086 namespace: String,
1088 symbol: String,
1090 color: (u8, u8, u8),
1092 priority: i32,
1094 },
1095
1096 SetLineIndicators {
1099 buffer_id: BufferId,
1100 lines: Vec<usize>,
1102 namespace: String,
1104 symbol: String,
1106 color: (u8, u8, u8),
1108 priority: i32,
1110 },
1111
1112 ClearLineIndicators {
1114 buffer_id: BufferId,
1115 namespace: String,
1117 },
1118
1119 SetFileExplorerDecorations {
1121 namespace: String,
1123 decorations: Vec<FileExplorerDecoration>,
1125 },
1126
1127 ClearFileExplorerDecorations {
1129 namespace: String,
1131 },
1132
1133 OpenFileAtLocation {
1136 path: PathBuf,
1137 line: Option<usize>, column: Option<usize>, },
1140
1141 OpenFileInSplit {
1144 split_id: usize,
1145 path: PathBuf,
1146 line: Option<usize>, column: Option<usize>, },
1149
1150 StartPrompt {
1153 label: String,
1154 prompt_type: String, },
1156
1157 StartPromptWithInitial {
1159 label: String,
1160 prompt_type: String,
1161 initial_value: String,
1162 },
1163
1164 StartPromptAsync {
1167 label: String,
1168 initial_value: String,
1169 callback_id: JsCallbackId,
1170 },
1171
1172 SetPromptSuggestions { suggestions: Vec<Suggestion> },
1175
1176 SetPromptInputSync { sync: bool },
1178
1179 AddMenuItem {
1182 menu_label: String,
1183 item: MenuItem,
1184 position: MenuPosition,
1185 },
1186
1187 AddMenu { menu: Menu, position: MenuPosition },
1189
1190 RemoveMenuItem {
1192 menu_label: String,
1193 item_label: String,
1194 },
1195
1196 RemoveMenu { menu_label: String },
1198
1199 CreateVirtualBuffer {
1201 name: String,
1203 mode: String,
1205 read_only: bool,
1207 },
1208
1209 CreateVirtualBufferWithContent {
1213 name: String,
1215 mode: String,
1217 read_only: bool,
1219 entries: Vec<TextPropertyEntry>,
1221 show_line_numbers: bool,
1223 show_cursors: bool,
1225 editing_disabled: bool,
1227 hidden_from_tabs: bool,
1229 request_id: Option<u64>,
1231 },
1232
1233 CreateVirtualBufferInSplit {
1236 name: String,
1238 mode: String,
1240 read_only: bool,
1242 entries: Vec<TextPropertyEntry>,
1244 ratio: f32,
1246 direction: Option<String>,
1248 panel_id: Option<String>,
1250 show_line_numbers: bool,
1252 show_cursors: bool,
1254 editing_disabled: bool,
1256 line_wrap: Option<bool>,
1258 before: bool,
1260 request_id: Option<u64>,
1262 },
1263
1264 SetVirtualBufferContent {
1266 buffer_id: BufferId,
1267 entries: Vec<TextPropertyEntry>,
1269 },
1270
1271 GetTextPropertiesAtCursor { buffer_id: BufferId },
1273
1274 DefineMode {
1276 name: String,
1277 parent: Option<String>,
1278 bindings: Vec<(String, String)>, read_only: bool,
1280 },
1281
1282 ShowBuffer { buffer_id: BufferId },
1284
1285 CreateVirtualBufferInExistingSplit {
1287 name: String,
1289 mode: String,
1291 read_only: bool,
1293 entries: Vec<TextPropertyEntry>,
1295 split_id: SplitId,
1297 show_line_numbers: bool,
1299 show_cursors: bool,
1301 editing_disabled: bool,
1303 line_wrap: Option<bool>,
1305 request_id: Option<u64>,
1307 },
1308
1309 CloseBuffer { buffer_id: BufferId },
1311
1312 CreateCompositeBuffer {
1315 name: String,
1317 mode: String,
1319 layout: CompositeLayoutConfig,
1321 sources: Vec<CompositeSourceConfig>,
1323 hunks: Option<Vec<CompositeHunk>>,
1325 request_id: Option<u64>,
1327 },
1328
1329 UpdateCompositeAlignment {
1331 buffer_id: BufferId,
1332 hunks: Vec<CompositeHunk>,
1333 },
1334
1335 CloseCompositeBuffer { buffer_id: BufferId },
1337
1338 FocusSplit { split_id: SplitId },
1340
1341 SetSplitBuffer {
1343 split_id: SplitId,
1344 buffer_id: BufferId,
1345 },
1346
1347 SetSplitScroll { split_id: SplitId, top_byte: usize },
1349
1350 RequestHighlights {
1352 buffer_id: BufferId,
1353 range: Range<usize>,
1354 request_id: u64,
1355 },
1356
1357 CloseSplit { split_id: SplitId },
1359
1360 SetSplitRatio {
1362 split_id: SplitId,
1363 ratio: f32,
1365 },
1366
1367 SetSplitLabel { split_id: SplitId, label: String },
1369
1370 ClearSplitLabel { split_id: SplitId },
1372
1373 GetSplitByLabel { label: String, request_id: u64 },
1375
1376 DistributeSplitsEvenly {
1378 split_ids: Vec<SplitId>,
1380 },
1381
1382 SetBufferCursor {
1384 buffer_id: BufferId,
1385 position: usize,
1387 },
1388
1389 SendLspRequest {
1391 language: String,
1392 method: String,
1393 #[ts(type = "any")]
1394 params: Option<JsonValue>,
1395 request_id: u64,
1396 },
1397
1398 SetClipboard { text: String },
1400
1401 DeleteSelection,
1404
1405 SetContext {
1409 name: String,
1411 active: bool,
1413 },
1414
1415 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1417
1418 ExecuteAction {
1421 action_name: String,
1423 },
1424
1425 ExecuteActions {
1429 actions: Vec<ActionSpec>,
1431 },
1432
1433 GetBufferText {
1435 buffer_id: BufferId,
1437 start: usize,
1439 end: usize,
1441 request_id: u64,
1443 },
1444
1445 GetLineStartPosition {
1448 buffer_id: BufferId,
1450 line: u32,
1452 request_id: u64,
1454 },
1455
1456 GetLineEndPosition {
1460 buffer_id: BufferId,
1462 line: u32,
1464 request_id: u64,
1466 },
1467
1468 GetBufferLineCount {
1470 buffer_id: BufferId,
1472 request_id: u64,
1474 },
1475
1476 ScrollToLineCenter {
1479 split_id: SplitId,
1481 buffer_id: BufferId,
1483 line: usize,
1485 },
1486
1487 SetEditorMode {
1490 mode: Option<String>,
1492 },
1493
1494 ShowActionPopup {
1497 popup_id: String,
1499 title: String,
1501 message: String,
1503 actions: Vec<ActionPopupAction>,
1505 },
1506
1507 DisableLspForLanguage {
1509 language: String,
1511 },
1512
1513 SetLspRootUri {
1517 language: String,
1519 uri: String,
1521 },
1522
1523 CreateScrollSyncGroup {
1527 group_id: u32,
1529 left_split: SplitId,
1531 right_split: SplitId,
1533 },
1534
1535 SetScrollSyncAnchors {
1538 group_id: u32,
1540 anchors: Vec<(usize, usize)>,
1542 },
1543
1544 RemoveScrollSyncGroup {
1546 group_id: u32,
1548 },
1549
1550 SaveBufferToPath {
1553 buffer_id: BufferId,
1555 path: PathBuf,
1557 },
1558
1559 LoadPlugin {
1562 path: PathBuf,
1564 callback_id: JsCallbackId,
1566 },
1567
1568 UnloadPlugin {
1571 name: String,
1573 callback_id: JsCallbackId,
1575 },
1576
1577 ReloadPlugin {
1580 name: String,
1582 callback_id: JsCallbackId,
1584 },
1585
1586 ListPlugins {
1589 callback_id: JsCallbackId,
1591 },
1592
1593 ReloadThemes,
1596
1597 RegisterGrammar {
1600 language: String,
1602 grammar_path: String,
1604 extensions: Vec<String>,
1606 },
1607
1608 RegisterLanguageConfig {
1611 language: String,
1613 config: LanguagePackConfig,
1615 },
1616
1617 RegisterLspServer {
1620 language: String,
1622 config: LspServerPackConfig,
1624 },
1625
1626 ReloadGrammars,
1629
1630 CreateTerminal {
1634 cwd: Option<String>,
1636 direction: Option<String>,
1638 ratio: Option<f32>,
1640 focus: Option<bool>,
1642 request_id: u64,
1644 },
1645
1646 SendTerminalInput {
1648 terminal_id: TerminalId,
1650 data: String,
1652 },
1653
1654 CloseTerminal {
1656 terminal_id: TerminalId,
1658 },
1659}
1660
1661impl PluginCommand {
1662 pub fn debug_variant_name(&self) -> String {
1664 let dbg = format!("{:?}", self);
1665 dbg.split([' ', '{', '(']).next().unwrap_or("?").to_string()
1666 }
1667}
1668
1669#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
1678#[serde(rename_all = "camelCase")]
1679#[ts(export)]
1680pub struct LanguagePackConfig {
1681 #[serde(default)]
1683 pub comment_prefix: Option<String>,
1684
1685 #[serde(default)]
1687 pub block_comment_start: Option<String>,
1688
1689 #[serde(default)]
1691 pub block_comment_end: Option<String>,
1692
1693 #[serde(default)]
1695 pub use_tabs: Option<bool>,
1696
1697 #[serde(default)]
1699 pub tab_size: Option<usize>,
1700
1701 #[serde(default)]
1703 pub auto_indent: Option<bool>,
1704
1705 #[serde(default)]
1708 pub show_whitespace_tabs: Option<bool>,
1709
1710 #[serde(default)]
1712 pub formatter: Option<FormatterPackConfig>,
1713}
1714
1715#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1717#[serde(rename_all = "camelCase")]
1718#[ts(export)]
1719pub struct FormatterPackConfig {
1720 pub command: String,
1722
1723 #[serde(default)]
1725 pub args: Vec<String>,
1726}
1727
1728#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1730#[serde(rename_all = "camelCase")]
1731#[ts(export)]
1732pub struct LspServerPackConfig {
1733 pub command: String,
1735
1736 #[serde(default)]
1738 pub args: Vec<String>,
1739
1740 #[serde(default)]
1742 pub auto_start: Option<bool>,
1743
1744 #[serde(default)]
1746 #[ts(type = "Record<string, unknown> | null")]
1747 pub initialization_options: Option<JsonValue>,
1748}
1749
1750#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1752#[ts(export)]
1753pub enum HunkStatus {
1754 Pending,
1755 Staged,
1756 Discarded,
1757}
1758
1759#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1761#[ts(export)]
1762pub struct ReviewHunk {
1763 pub id: String,
1764 pub file: String,
1765 pub context_header: String,
1766 pub status: HunkStatus,
1767 pub base_range: Option<(usize, usize)>,
1769 pub modified_range: Option<(usize, usize)>,
1771}
1772
1773#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1775#[serde(deny_unknown_fields)]
1776#[ts(export, rename = "TsActionPopupAction")]
1777pub struct ActionPopupAction {
1778 pub id: String,
1780 pub label: String,
1782}
1783
1784#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1786#[serde(deny_unknown_fields)]
1787#[ts(export)]
1788pub struct ActionPopupOptions {
1789 pub id: String,
1791 pub title: String,
1793 pub message: String,
1795 pub actions: Vec<ActionPopupAction>,
1797}
1798
1799#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1801#[ts(export)]
1802pub struct TsHighlightSpan {
1803 pub start: u32,
1804 pub end: u32,
1805 #[ts(type = "[number, number, number]")]
1806 pub color: (u8, u8, u8),
1807 pub bold: bool,
1808 pub italic: bool,
1809}
1810
1811#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1813#[ts(export)]
1814pub struct SpawnResult {
1815 pub stdout: String,
1817 pub stderr: String,
1819 pub exit_code: i32,
1821}
1822
1823#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1825#[ts(export)]
1826pub struct BackgroundProcessResult {
1827 #[ts(type = "number")]
1829 pub process_id: u64,
1830 pub exit_code: i32,
1833}
1834
1835#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1837#[serde(deny_unknown_fields)]
1838#[ts(export, rename = "TextPropertyEntry")]
1839pub struct JsTextPropertyEntry {
1840 pub text: String,
1842 #[serde(default)]
1844 #[ts(optional, type = "Record<string, unknown>")]
1845 pub properties: Option<HashMap<String, JsonValue>>,
1846}
1847
1848#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1850#[ts(export)]
1851pub struct DirEntry {
1852 pub name: String,
1854 pub is_file: bool,
1856 pub is_dir: bool,
1858}
1859
1860#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1862#[ts(export)]
1863pub struct JsPosition {
1864 pub line: u32,
1866 pub character: u32,
1868}
1869
1870#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1872#[ts(export)]
1873pub struct JsRange {
1874 pub start: JsPosition,
1876 pub end: JsPosition,
1878}
1879
1880#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1882#[ts(export)]
1883pub struct JsDiagnostic {
1884 pub uri: String,
1886 pub message: String,
1888 pub severity: Option<u8>,
1890 pub range: JsRange,
1892 #[ts(optional)]
1894 pub source: Option<String>,
1895}
1896
1897#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1899#[serde(deny_unknown_fields)]
1900#[ts(export)]
1901pub struct CreateVirtualBufferOptions {
1902 pub name: String,
1904 #[serde(default)]
1906 #[ts(optional)]
1907 pub mode: Option<String>,
1908 #[serde(default, rename = "readOnly")]
1910 #[ts(optional, rename = "readOnly")]
1911 pub read_only: Option<bool>,
1912 #[serde(default, rename = "showLineNumbers")]
1914 #[ts(optional, rename = "showLineNumbers")]
1915 pub show_line_numbers: Option<bool>,
1916 #[serde(default, rename = "showCursors")]
1918 #[ts(optional, rename = "showCursors")]
1919 pub show_cursors: Option<bool>,
1920 #[serde(default, rename = "editingDisabled")]
1922 #[ts(optional, rename = "editingDisabled")]
1923 pub editing_disabled: Option<bool>,
1924 #[serde(default, rename = "hiddenFromTabs")]
1926 #[ts(optional, rename = "hiddenFromTabs")]
1927 pub hidden_from_tabs: Option<bool>,
1928 #[serde(default)]
1930 #[ts(optional)]
1931 pub entries: Option<Vec<JsTextPropertyEntry>>,
1932}
1933
1934#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1936#[serde(deny_unknown_fields)]
1937#[ts(export)]
1938pub struct CreateVirtualBufferInSplitOptions {
1939 pub name: String,
1941 #[serde(default)]
1943 #[ts(optional)]
1944 pub mode: Option<String>,
1945 #[serde(default, rename = "readOnly")]
1947 #[ts(optional, rename = "readOnly")]
1948 pub read_only: Option<bool>,
1949 #[serde(default)]
1951 #[ts(optional)]
1952 pub ratio: Option<f32>,
1953 #[serde(default)]
1955 #[ts(optional)]
1956 pub direction: Option<String>,
1957 #[serde(default, rename = "panelId")]
1959 #[ts(optional, rename = "panelId")]
1960 pub panel_id: Option<String>,
1961 #[serde(default, rename = "showLineNumbers")]
1963 #[ts(optional, rename = "showLineNumbers")]
1964 pub show_line_numbers: Option<bool>,
1965 #[serde(default, rename = "showCursors")]
1967 #[ts(optional, rename = "showCursors")]
1968 pub show_cursors: Option<bool>,
1969 #[serde(default, rename = "editingDisabled")]
1971 #[ts(optional, rename = "editingDisabled")]
1972 pub editing_disabled: Option<bool>,
1973 #[serde(default, rename = "lineWrap")]
1975 #[ts(optional, rename = "lineWrap")]
1976 pub line_wrap: Option<bool>,
1977 #[serde(default)]
1979 #[ts(optional)]
1980 pub before: Option<bool>,
1981 #[serde(default)]
1983 #[ts(optional)]
1984 pub entries: Option<Vec<JsTextPropertyEntry>>,
1985}
1986
1987#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1989#[serde(deny_unknown_fields)]
1990#[ts(export)]
1991pub struct CreateVirtualBufferInExistingSplitOptions {
1992 pub name: String,
1994 #[serde(rename = "splitId")]
1996 #[ts(rename = "splitId")]
1997 pub split_id: usize,
1998 #[serde(default)]
2000 #[ts(optional)]
2001 pub mode: Option<String>,
2002 #[serde(default, rename = "readOnly")]
2004 #[ts(optional, rename = "readOnly")]
2005 pub read_only: Option<bool>,
2006 #[serde(default, rename = "showLineNumbers")]
2008 #[ts(optional, rename = "showLineNumbers")]
2009 pub show_line_numbers: Option<bool>,
2010 #[serde(default, rename = "showCursors")]
2012 #[ts(optional, rename = "showCursors")]
2013 pub show_cursors: Option<bool>,
2014 #[serde(default, rename = "editingDisabled")]
2016 #[ts(optional, rename = "editingDisabled")]
2017 pub editing_disabled: Option<bool>,
2018 #[serde(default, rename = "lineWrap")]
2020 #[ts(optional, rename = "lineWrap")]
2021 pub line_wrap: Option<bool>,
2022 #[serde(default)]
2024 #[ts(optional)]
2025 pub entries: Option<Vec<JsTextPropertyEntry>>,
2026}
2027
2028#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2030#[serde(deny_unknown_fields)]
2031#[ts(export)]
2032pub struct CreateTerminalOptions {
2033 #[serde(default)]
2035 #[ts(optional)]
2036 pub cwd: Option<String>,
2037 #[serde(default)]
2039 #[ts(optional)]
2040 pub direction: Option<String>,
2041 #[serde(default)]
2043 #[ts(optional)]
2044 pub ratio: Option<f32>,
2045 #[serde(default)]
2047 #[ts(optional)]
2048 pub focus: Option<bool>,
2049}
2050
2051#[derive(Debug, Clone, Serialize, TS)]
2056#[ts(export, type = "Array<Record<string, unknown>>")]
2057pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
2058
2059#[cfg(feature = "plugins")]
2061mod fromjs_impls {
2062 use super::*;
2063 use rquickjs::{Ctx, FromJs, Value};
2064
2065 impl<'js> FromJs<'js> for JsTextPropertyEntry {
2066 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2067 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2068 from: "object",
2069 to: "JsTextPropertyEntry",
2070 message: Some(e.to_string()),
2071 })
2072 }
2073 }
2074
2075 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
2076 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2077 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2078 from: "object",
2079 to: "CreateVirtualBufferOptions",
2080 message: Some(e.to_string()),
2081 })
2082 }
2083 }
2084
2085 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
2086 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2087 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2088 from: "object",
2089 to: "CreateVirtualBufferInSplitOptions",
2090 message: Some(e.to_string()),
2091 })
2092 }
2093 }
2094
2095 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
2096 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2097 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2098 from: "object",
2099 to: "CreateVirtualBufferInExistingSplitOptions",
2100 message: Some(e.to_string()),
2101 })
2102 }
2103 }
2104
2105 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
2106 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
2107 rquickjs_serde::to_value(ctx.clone(), &self.0)
2108 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
2109 }
2110 }
2111
2112 impl<'js> FromJs<'js> for ActionSpec {
2115 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2116 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2117 from: "object",
2118 to: "ActionSpec",
2119 message: Some(e.to_string()),
2120 })
2121 }
2122 }
2123
2124 impl<'js> FromJs<'js> for ActionPopupAction {
2125 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2126 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2127 from: "object",
2128 to: "ActionPopupAction",
2129 message: Some(e.to_string()),
2130 })
2131 }
2132 }
2133
2134 impl<'js> FromJs<'js> for ActionPopupOptions {
2135 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2136 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2137 from: "object",
2138 to: "ActionPopupOptions",
2139 message: Some(e.to_string()),
2140 })
2141 }
2142 }
2143
2144 impl<'js> FromJs<'js> for ViewTokenWire {
2145 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2146 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2147 from: "object",
2148 to: "ViewTokenWire",
2149 message: Some(e.to_string()),
2150 })
2151 }
2152 }
2153
2154 impl<'js> FromJs<'js> for ViewTokenStyle {
2155 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2156 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2157 from: "object",
2158 to: "ViewTokenStyle",
2159 message: Some(e.to_string()),
2160 })
2161 }
2162 }
2163
2164 impl<'js> FromJs<'js> for LayoutHints {
2165 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2166 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2167 from: "object",
2168 to: "LayoutHints",
2169 message: Some(e.to_string()),
2170 })
2171 }
2172 }
2173
2174 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
2175 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2176 let json: serde_json::Value =
2178 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2179 from: "object",
2180 to: "CreateCompositeBufferOptions (json)",
2181 message: Some(e.to_string()),
2182 })?;
2183 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
2184 from: "json",
2185 to: "CreateCompositeBufferOptions",
2186 message: Some(e.to_string()),
2187 })
2188 }
2189 }
2190
2191 impl<'js> FromJs<'js> for CompositeHunk {
2192 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2193 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2194 from: "object",
2195 to: "CompositeHunk",
2196 message: Some(e.to_string()),
2197 })
2198 }
2199 }
2200
2201 impl<'js> FromJs<'js> for LanguagePackConfig {
2202 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2203 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2204 from: "object",
2205 to: "LanguagePackConfig",
2206 message: Some(e.to_string()),
2207 })
2208 }
2209 }
2210
2211 impl<'js> FromJs<'js> for LspServerPackConfig {
2212 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2213 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2214 from: "object",
2215 to: "LspServerPackConfig",
2216 message: Some(e.to_string()),
2217 })
2218 }
2219 }
2220
2221 impl<'js> FromJs<'js> for CreateTerminalOptions {
2222 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2223 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2224 from: "object",
2225 to: "CreateTerminalOptions",
2226 message: Some(e.to_string()),
2227 })
2228 }
2229 }
2230}
2231
2232pub struct PluginApi {
2234 hooks: Arc<RwLock<HookRegistry>>,
2236
2237 commands: Arc<RwLock<CommandRegistry>>,
2239
2240 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2242
2243 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2245}
2246
2247impl PluginApi {
2248 pub fn new(
2250 hooks: Arc<RwLock<HookRegistry>>,
2251 commands: Arc<RwLock<CommandRegistry>>,
2252 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2253 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2254 ) -> Self {
2255 Self {
2256 hooks,
2257 commands,
2258 command_sender,
2259 state_snapshot,
2260 }
2261 }
2262
2263 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
2265 let mut hooks = self.hooks.write().unwrap();
2266 hooks.add_hook(hook_name, callback);
2267 }
2268
2269 pub fn unregister_hooks(&self, hook_name: &str) {
2271 let mut hooks = self.hooks.write().unwrap();
2272 hooks.remove_hooks(hook_name);
2273 }
2274
2275 pub fn register_command(&self, command: Command) {
2277 let commands = self.commands.read().unwrap();
2278 commands.register(command);
2279 }
2280
2281 pub fn unregister_command(&self, name: &str) {
2283 let commands = self.commands.read().unwrap();
2284 commands.unregister(name);
2285 }
2286
2287 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
2289 self.command_sender
2290 .send(command)
2291 .map_err(|e| format!("Failed to send command: {}", e))
2292 }
2293
2294 pub fn insert_text(
2296 &self,
2297 buffer_id: BufferId,
2298 position: usize,
2299 text: String,
2300 ) -> Result<(), String> {
2301 self.send_command(PluginCommand::InsertText {
2302 buffer_id,
2303 position,
2304 text,
2305 })
2306 }
2307
2308 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
2310 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
2311 }
2312
2313 pub fn add_overlay(
2321 &self,
2322 buffer_id: BufferId,
2323 namespace: Option<String>,
2324 range: Range<usize>,
2325 options: OverlayOptions,
2326 ) -> Result<(), String> {
2327 self.send_command(PluginCommand::AddOverlay {
2328 buffer_id,
2329 namespace: namespace.map(OverlayNamespace::from_string),
2330 range,
2331 options,
2332 })
2333 }
2334
2335 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
2337 self.send_command(PluginCommand::RemoveOverlay {
2338 buffer_id,
2339 handle: OverlayHandle::from_string(handle),
2340 })
2341 }
2342
2343 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
2345 self.send_command(PluginCommand::ClearNamespace {
2346 buffer_id,
2347 namespace: OverlayNamespace::from_string(namespace),
2348 })
2349 }
2350
2351 pub fn clear_overlays_in_range(
2354 &self,
2355 buffer_id: BufferId,
2356 start: usize,
2357 end: usize,
2358 ) -> Result<(), String> {
2359 self.send_command(PluginCommand::ClearOverlaysInRange {
2360 buffer_id,
2361 start,
2362 end,
2363 })
2364 }
2365
2366 pub fn set_status(&self, message: String) -> Result<(), String> {
2368 self.send_command(PluginCommand::SetStatus { message })
2369 }
2370
2371 pub fn open_file_at_location(
2374 &self,
2375 path: PathBuf,
2376 line: Option<usize>,
2377 column: Option<usize>,
2378 ) -> Result<(), String> {
2379 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
2380 }
2381
2382 pub fn open_file_in_split(
2387 &self,
2388 split_id: usize,
2389 path: PathBuf,
2390 line: Option<usize>,
2391 column: Option<usize>,
2392 ) -> Result<(), String> {
2393 self.send_command(PluginCommand::OpenFileInSplit {
2394 split_id,
2395 path,
2396 line,
2397 column,
2398 })
2399 }
2400
2401 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
2404 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
2405 }
2406
2407 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
2410 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
2411 }
2412
2413 pub fn set_prompt_input_sync(&self, sync: bool) -> Result<(), String> {
2415 self.send_command(PluginCommand::SetPromptInputSync { sync })
2416 }
2417
2418 pub fn add_menu_item(
2420 &self,
2421 menu_label: String,
2422 item: MenuItem,
2423 position: MenuPosition,
2424 ) -> Result<(), String> {
2425 self.send_command(PluginCommand::AddMenuItem {
2426 menu_label,
2427 item,
2428 position,
2429 })
2430 }
2431
2432 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
2434 self.send_command(PluginCommand::AddMenu { menu, position })
2435 }
2436
2437 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
2439 self.send_command(PluginCommand::RemoveMenuItem {
2440 menu_label,
2441 item_label,
2442 })
2443 }
2444
2445 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
2447 self.send_command(PluginCommand::RemoveMenu { menu_label })
2448 }
2449
2450 pub fn create_virtual_buffer(
2457 &self,
2458 name: String,
2459 mode: String,
2460 read_only: bool,
2461 ) -> Result<(), String> {
2462 self.send_command(PluginCommand::CreateVirtualBuffer {
2463 name,
2464 mode,
2465 read_only,
2466 })
2467 }
2468
2469 pub fn create_virtual_buffer_with_content(
2475 &self,
2476 name: String,
2477 mode: String,
2478 read_only: bool,
2479 entries: Vec<TextPropertyEntry>,
2480 ) -> Result<(), String> {
2481 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
2482 name,
2483 mode,
2484 read_only,
2485 entries,
2486 show_line_numbers: true,
2487 show_cursors: true,
2488 editing_disabled: false,
2489 hidden_from_tabs: false,
2490 request_id: None,
2491 })
2492 }
2493
2494 pub fn set_virtual_buffer_content(
2498 &self,
2499 buffer_id: BufferId,
2500 entries: Vec<TextPropertyEntry>,
2501 ) -> Result<(), String> {
2502 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
2503 }
2504
2505 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
2509 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
2510 }
2511
2512 pub fn define_mode(
2517 &self,
2518 name: String,
2519 parent: Option<String>,
2520 bindings: Vec<(String, String)>,
2521 read_only: bool,
2522 ) -> Result<(), String> {
2523 self.send_command(PluginCommand::DefineMode {
2524 name,
2525 parent,
2526 bindings,
2527 read_only,
2528 })
2529 }
2530
2531 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
2533 self.send_command(PluginCommand::ShowBuffer { buffer_id })
2534 }
2535
2536 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
2538 self.send_command(PluginCommand::SetSplitScroll {
2539 split_id: SplitId(split_id),
2540 top_byte,
2541 })
2542 }
2543
2544 pub fn get_highlights(
2546 &self,
2547 buffer_id: BufferId,
2548 range: Range<usize>,
2549 request_id: u64,
2550 ) -> Result<(), String> {
2551 self.send_command(PluginCommand::RequestHighlights {
2552 buffer_id,
2553 range,
2554 request_id,
2555 })
2556 }
2557
2558 pub fn get_active_buffer_id(&self) -> BufferId {
2562 let snapshot = self.state_snapshot.read().unwrap();
2563 snapshot.active_buffer_id
2564 }
2565
2566 pub fn get_active_split_id(&self) -> usize {
2568 let snapshot = self.state_snapshot.read().unwrap();
2569 snapshot.active_split_id
2570 }
2571
2572 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
2574 let snapshot = self.state_snapshot.read().unwrap();
2575 snapshot.buffers.get(&buffer_id).cloned()
2576 }
2577
2578 pub fn list_buffers(&self) -> Vec<BufferInfo> {
2580 let snapshot = self.state_snapshot.read().unwrap();
2581 snapshot.buffers.values().cloned().collect()
2582 }
2583
2584 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
2586 let snapshot = self.state_snapshot.read().unwrap();
2587 snapshot.primary_cursor.clone()
2588 }
2589
2590 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
2592 let snapshot = self.state_snapshot.read().unwrap();
2593 snapshot.all_cursors.clone()
2594 }
2595
2596 pub fn get_viewport(&self) -> Option<ViewportInfo> {
2598 let snapshot = self.state_snapshot.read().unwrap();
2599 snapshot.viewport.clone()
2600 }
2601
2602 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
2604 Arc::clone(&self.state_snapshot)
2605 }
2606}
2607
2608impl Clone for PluginApi {
2609 fn clone(&self) -> Self {
2610 Self {
2611 hooks: Arc::clone(&self.hooks),
2612 commands: Arc::clone(&self.commands),
2613 command_sender: self.command_sender.clone(),
2614 state_snapshot: Arc::clone(&self.state_snapshot),
2615 }
2616 }
2617}
2618
2619#[cfg(test)]
2620mod tests {
2621 use super::*;
2622
2623 #[test]
2624 fn test_plugin_api_creation() {
2625 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2626 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2627 let (tx, _rx) = std::sync::mpsc::channel();
2628 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2629
2630 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2631
2632 let _clone = api.clone();
2634 }
2635
2636 #[test]
2637 fn test_register_hook() {
2638 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2639 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2640 let (tx, _rx) = std::sync::mpsc::channel();
2641 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2642
2643 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
2644
2645 api.register_hook("test-hook", Box::new(|_| true));
2646
2647 let hook_registry = hooks.read().unwrap();
2648 assert_eq!(hook_registry.hook_count("test-hook"), 1);
2649 }
2650
2651 #[test]
2652 fn test_send_command() {
2653 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2654 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2655 let (tx, rx) = std::sync::mpsc::channel();
2656 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2657
2658 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2659
2660 let result = api.insert_text(BufferId(1), 0, "test".to_string());
2661 assert!(result.is_ok());
2662
2663 let received = rx.try_recv();
2665 assert!(received.is_ok());
2666
2667 match received.unwrap() {
2668 PluginCommand::InsertText {
2669 buffer_id,
2670 position,
2671 text,
2672 } => {
2673 assert_eq!(buffer_id.0, 1);
2674 assert_eq!(position, 0);
2675 assert_eq!(text, "test");
2676 }
2677 _ => panic!("Wrong command type"),
2678 }
2679 }
2680
2681 #[test]
2682 fn test_add_overlay_command() {
2683 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2684 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2685 let (tx, rx) = std::sync::mpsc::channel();
2686 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2687
2688 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2689
2690 let result = api.add_overlay(
2691 BufferId(1),
2692 Some("test-overlay".to_string()),
2693 0..10,
2694 OverlayOptions {
2695 fg: Some(OverlayColorSpec::ThemeKey("ui.status_bar_fg".to_string())),
2696 bg: None,
2697 underline: true,
2698 bold: false,
2699 italic: false,
2700 strikethrough: false,
2701 extend_to_line_end: false,
2702 url: None,
2703 },
2704 );
2705 assert!(result.is_ok());
2706
2707 let received = rx.try_recv().unwrap();
2708 match received {
2709 PluginCommand::AddOverlay {
2710 buffer_id,
2711 namespace,
2712 range,
2713 options,
2714 } => {
2715 assert_eq!(buffer_id.0, 1);
2716 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
2717 assert_eq!(range, 0..10);
2718 assert!(matches!(
2719 options.fg,
2720 Some(OverlayColorSpec::ThemeKey(ref k)) if k == "ui.status_bar_fg"
2721 ));
2722 assert!(options.bg.is_none());
2723 assert!(options.underline);
2724 assert!(!options.bold);
2725 assert!(!options.italic);
2726 assert!(!options.extend_to_line_end);
2727 }
2728 _ => panic!("Wrong command type"),
2729 }
2730 }
2731
2732 #[test]
2733 fn test_set_status_command() {
2734 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2735 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2736 let (tx, rx) = std::sync::mpsc::channel();
2737 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2738
2739 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2740
2741 let result = api.set_status("Test status".to_string());
2742 assert!(result.is_ok());
2743
2744 let received = rx.try_recv().unwrap();
2745 match received {
2746 PluginCommand::SetStatus { message } => {
2747 assert_eq!(message, "Test status");
2748 }
2749 _ => panic!("Wrong command type"),
2750 }
2751 }
2752
2753 #[test]
2754 fn test_get_active_buffer_id() {
2755 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2756 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2757 let (tx, _rx) = std::sync::mpsc::channel();
2758 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2759
2760 {
2762 let mut snapshot = state_snapshot.write().unwrap();
2763 snapshot.active_buffer_id = BufferId(5);
2764 }
2765
2766 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2767
2768 let active_id = api.get_active_buffer_id();
2769 assert_eq!(active_id.0, 5);
2770 }
2771
2772 #[test]
2773 fn test_get_buffer_info() {
2774 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2775 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2776 let (tx, _rx) = std::sync::mpsc::channel();
2777 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2778
2779 {
2781 let mut snapshot = state_snapshot.write().unwrap();
2782 let buffer_info = BufferInfo {
2783 id: BufferId(1),
2784 path: Some(std::path::PathBuf::from("/test/file.txt")),
2785 modified: true,
2786 length: 100,
2787 is_virtual: false,
2788 view_mode: "source".to_string(),
2789 is_composing_in_any_split: false,
2790 compose_width: None,
2791 };
2792 snapshot.buffers.insert(BufferId(1), buffer_info);
2793 }
2794
2795 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2796
2797 let info = api.get_buffer_info(BufferId(1));
2798 assert!(info.is_some());
2799 let info = info.unwrap();
2800 assert_eq!(info.id.0, 1);
2801 assert_eq!(
2802 info.path.as_ref().unwrap().to_str().unwrap(),
2803 "/test/file.txt"
2804 );
2805 assert!(info.modified);
2806 assert_eq!(info.length, 100);
2807
2808 let no_info = api.get_buffer_info(BufferId(999));
2810 assert!(no_info.is_none());
2811 }
2812
2813 #[test]
2814 fn test_list_buffers() {
2815 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2816 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2817 let (tx, _rx) = std::sync::mpsc::channel();
2818 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2819
2820 {
2822 let mut snapshot = state_snapshot.write().unwrap();
2823 snapshot.buffers.insert(
2824 BufferId(1),
2825 BufferInfo {
2826 id: BufferId(1),
2827 path: Some(std::path::PathBuf::from("/file1.txt")),
2828 modified: false,
2829 length: 50,
2830 is_virtual: false,
2831 view_mode: "source".to_string(),
2832 is_composing_in_any_split: false,
2833 compose_width: None,
2834 },
2835 );
2836 snapshot.buffers.insert(
2837 BufferId(2),
2838 BufferInfo {
2839 id: BufferId(2),
2840 path: Some(std::path::PathBuf::from("/file2.txt")),
2841 modified: true,
2842 length: 100,
2843 is_virtual: false,
2844 view_mode: "source".to_string(),
2845 is_composing_in_any_split: false,
2846 compose_width: None,
2847 },
2848 );
2849 snapshot.buffers.insert(
2850 BufferId(3),
2851 BufferInfo {
2852 id: BufferId(3),
2853 path: None,
2854 modified: false,
2855 length: 0,
2856 is_virtual: true,
2857 view_mode: "source".to_string(),
2858 is_composing_in_any_split: false,
2859 compose_width: None,
2860 },
2861 );
2862 }
2863
2864 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2865
2866 let buffers = api.list_buffers();
2867 assert_eq!(buffers.len(), 3);
2868
2869 assert!(buffers.iter().any(|b| b.id.0 == 1));
2871 assert!(buffers.iter().any(|b| b.id.0 == 2));
2872 assert!(buffers.iter().any(|b| b.id.0 == 3));
2873 }
2874
2875 #[test]
2876 fn test_get_primary_cursor() {
2877 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2878 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2879 let (tx, _rx) = std::sync::mpsc::channel();
2880 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2881
2882 {
2884 let mut snapshot = state_snapshot.write().unwrap();
2885 snapshot.primary_cursor = Some(CursorInfo {
2886 position: 42,
2887 selection: Some(10..42),
2888 });
2889 }
2890
2891 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2892
2893 let cursor = api.get_primary_cursor();
2894 assert!(cursor.is_some());
2895 let cursor = cursor.unwrap();
2896 assert_eq!(cursor.position, 42);
2897 assert_eq!(cursor.selection, Some(10..42));
2898 }
2899
2900 #[test]
2901 fn test_get_all_cursors() {
2902 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2903 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2904 let (tx, _rx) = std::sync::mpsc::channel();
2905 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2906
2907 {
2909 let mut snapshot = state_snapshot.write().unwrap();
2910 snapshot.all_cursors = vec![
2911 CursorInfo {
2912 position: 10,
2913 selection: None,
2914 },
2915 CursorInfo {
2916 position: 20,
2917 selection: Some(15..20),
2918 },
2919 CursorInfo {
2920 position: 30,
2921 selection: Some(25..30),
2922 },
2923 ];
2924 }
2925
2926 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2927
2928 let cursors = api.get_all_cursors();
2929 assert_eq!(cursors.len(), 3);
2930 assert_eq!(cursors[0].position, 10);
2931 assert_eq!(cursors[0].selection, None);
2932 assert_eq!(cursors[1].position, 20);
2933 assert_eq!(cursors[1].selection, Some(15..20));
2934 assert_eq!(cursors[2].position, 30);
2935 assert_eq!(cursors[2].selection, Some(25..30));
2936 }
2937
2938 #[test]
2939 fn test_get_viewport() {
2940 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2941 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2942 let (tx, _rx) = std::sync::mpsc::channel();
2943 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2944
2945 {
2947 let mut snapshot = state_snapshot.write().unwrap();
2948 snapshot.viewport = Some(ViewportInfo {
2949 top_byte: 100,
2950 top_line: Some(5),
2951 left_column: 5,
2952 width: 80,
2953 height: 24,
2954 });
2955 }
2956
2957 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2958
2959 let viewport = api.get_viewport();
2960 assert!(viewport.is_some());
2961 let viewport = viewport.unwrap();
2962 assert_eq!(viewport.top_byte, 100);
2963 assert_eq!(viewport.left_column, 5);
2964 assert_eq!(viewport.width, 80);
2965 assert_eq!(viewport.height, 24);
2966 }
2967
2968 #[test]
2969 fn test_composite_buffer_options_rejects_unknown_fields() {
2970 let valid_json = r#"{
2972 "name": "test",
2973 "mode": "diff",
2974 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2975 "sources": [{"bufferId": 1, "label": "old"}]
2976 }"#;
2977 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
2978 assert!(
2979 result.is_ok(),
2980 "Valid JSON should parse: {:?}",
2981 result.err()
2982 );
2983
2984 let invalid_json = r#"{
2986 "name": "test",
2987 "mode": "diff",
2988 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2989 "sources": [{"buffer_id": 1, "label": "old"}]
2990 }"#;
2991 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
2992 assert!(
2993 result.is_err(),
2994 "JSON with unknown field should fail to parse"
2995 );
2996 let err = result.unwrap_err().to_string();
2997 assert!(
2998 err.contains("unknown field") || err.contains("buffer_id"),
2999 "Error should mention unknown field: {}",
3000 err
3001 );
3002 }
3003
3004 #[test]
3005 fn test_composite_hunk_rejects_unknown_fields() {
3006 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3008 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
3009 assert!(
3010 result.is_ok(),
3011 "Valid JSON should parse: {:?}",
3012 result.err()
3013 );
3014
3015 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3017 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
3018 assert!(
3019 result.is_err(),
3020 "JSON with unknown field should fail to parse"
3021 );
3022 let err = result.unwrap_err().to_string();
3023 assert!(
3024 err.contains("unknown field") || err.contains("old_start"),
3025 "Error should mention unknown field: {}",
3026 err
3027 );
3028 }
3029
3030 #[test]
3031 fn test_plugin_response_line_end_position() {
3032 let response = PluginResponse::LineEndPosition {
3033 request_id: 42,
3034 position: Some(100),
3035 };
3036 let json = serde_json::to_string(&response).unwrap();
3037 assert!(json.contains("LineEndPosition"));
3038 assert!(json.contains("42"));
3039 assert!(json.contains("100"));
3040
3041 let response_none = PluginResponse::LineEndPosition {
3043 request_id: 1,
3044 position: None,
3045 };
3046 let json_none = serde_json::to_string(&response_none).unwrap();
3047 assert!(json_none.contains("null"));
3048 }
3049
3050 #[test]
3051 fn test_plugin_response_buffer_line_count() {
3052 let response = PluginResponse::BufferLineCount {
3053 request_id: 99,
3054 count: Some(500),
3055 };
3056 let json = serde_json::to_string(&response).unwrap();
3057 assert!(json.contains("BufferLineCount"));
3058 assert!(json.contains("99"));
3059 assert!(json.contains("500"));
3060 }
3061
3062 #[test]
3063 fn test_plugin_command_get_line_end_position() {
3064 let command = PluginCommand::GetLineEndPosition {
3065 buffer_id: BufferId(1),
3066 line: 10,
3067 request_id: 123,
3068 };
3069 let json = serde_json::to_string(&command).unwrap();
3070 assert!(json.contains("GetLineEndPosition"));
3071 assert!(json.contains("10"));
3072 }
3073
3074 #[test]
3075 fn test_plugin_command_get_buffer_line_count() {
3076 let command = PluginCommand::GetBufferLineCount {
3077 buffer_id: BufferId(0),
3078 request_id: 456,
3079 };
3080 let json = serde_json::to_string(&command).unwrap();
3081 assert!(json.contains("GetBufferLineCount"));
3082 assert!(json.contains("456"));
3083 }
3084
3085 #[test]
3086 fn test_plugin_command_scroll_to_line_center() {
3087 let command = PluginCommand::ScrollToLineCenter {
3088 split_id: SplitId(1),
3089 buffer_id: BufferId(2),
3090 line: 50,
3091 };
3092 let json = serde_json::to_string(&command).unwrap();
3093 assert!(json.contains("ScrollToLineCenter"));
3094 assert!(json.contains("50"));
3095 }
3096}