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 #[ts(optional)]
411 pub compose_width: Option<u16>,
412 #[ts(optional)]
414 pub column_guides: Option<Vec<u16>>,
415}
416
417#[derive(Debug, Clone, Serialize, Deserialize, TS)]
432#[serde(untagged)]
433#[ts(export)]
434pub enum OverlayColorSpec {
435 #[ts(type = "[number, number, number]")]
437 Rgb(u8, u8, u8),
438 ThemeKey(String),
440}
441
442impl OverlayColorSpec {
443 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
445 Self::Rgb(r, g, b)
446 }
447
448 pub fn theme_key(key: impl Into<String>) -> Self {
450 Self::ThemeKey(key.into())
451 }
452
453 pub fn as_rgb(&self) -> Option<(u8, u8, u8)> {
455 match self {
456 Self::Rgb(r, g, b) => Some((*r, *g, *b)),
457 Self::ThemeKey(_) => None,
458 }
459 }
460
461 pub fn as_theme_key(&self) -> Option<&str> {
463 match self {
464 Self::ThemeKey(key) => Some(key),
465 Self::Rgb(_, _, _) => None,
466 }
467 }
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize, TS)]
475#[serde(deny_unknown_fields, rename_all = "camelCase")]
476#[ts(export, rename_all = "camelCase")]
477#[derive(Default)]
478pub struct OverlayOptions {
479 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub fg: Option<OverlayColorSpec>,
482
483 #[serde(default, skip_serializing_if = "Option::is_none")]
485 pub bg: Option<OverlayColorSpec>,
486
487 #[serde(default)]
489 pub underline: bool,
490
491 #[serde(default)]
493 pub bold: bool,
494
495 #[serde(default)]
497 pub italic: bool,
498
499 #[serde(default)]
501 pub strikethrough: bool,
502
503 #[serde(default)]
505 pub extend_to_line_end: bool,
506
507 #[serde(default, skip_serializing_if = "Option::is_none")]
511 pub url: Option<String>,
512}
513
514#[derive(Debug, Clone, Serialize, Deserialize, TS)]
520#[serde(deny_unknown_fields)]
521#[ts(export, rename = "TsCompositeLayoutConfig")]
522pub struct CompositeLayoutConfig {
523 #[serde(rename = "type")]
525 #[ts(rename = "type")]
526 pub layout_type: String,
527 #[serde(default)]
529 #[ts(optional)]
530 pub ratios: Option<Vec<f32>>,
531 #[serde(default = "default_true", rename = "showSeparator")]
533 #[ts(rename = "showSeparator")]
534 pub show_separator: bool,
535 #[serde(default)]
537 #[ts(optional)]
538 pub spacing: Option<u16>,
539}
540
541fn default_true() -> bool {
542 true
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize, TS)]
547#[serde(deny_unknown_fields)]
548#[ts(export, rename = "TsCompositeSourceConfig")]
549pub struct CompositeSourceConfig {
550 #[serde(rename = "bufferId")]
552 #[ts(rename = "bufferId")]
553 pub buffer_id: usize,
554 pub label: String,
556 #[serde(default)]
558 pub editable: bool,
559 #[serde(default)]
561 pub style: Option<CompositePaneStyle>,
562}
563
564#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
566#[serde(deny_unknown_fields)]
567#[ts(export, rename = "TsCompositePaneStyle")]
568pub struct CompositePaneStyle {
569 #[serde(default, rename = "addBg")]
572 #[ts(optional, rename = "addBg", type = "[number, number, number]")]
573 pub add_bg: Option<[u8; 3]>,
574 #[serde(default, rename = "removeBg")]
576 #[ts(optional, rename = "removeBg", type = "[number, number, number]")]
577 pub remove_bg: Option<[u8; 3]>,
578 #[serde(default, rename = "modifyBg")]
580 #[ts(optional, rename = "modifyBg", type = "[number, number, number]")]
581 pub modify_bg: Option<[u8; 3]>,
582 #[serde(default, rename = "gutterStyle")]
584 #[ts(optional, rename = "gutterStyle")]
585 pub gutter_style: Option<String>,
586}
587
588#[derive(Debug, Clone, Serialize, Deserialize, TS)]
590#[serde(deny_unknown_fields)]
591#[ts(export, rename = "TsCompositeHunk")]
592pub struct CompositeHunk {
593 #[serde(rename = "oldStart")]
595 #[ts(rename = "oldStart")]
596 pub old_start: usize,
597 #[serde(rename = "oldCount")]
599 #[ts(rename = "oldCount")]
600 pub old_count: usize,
601 #[serde(rename = "newStart")]
603 #[ts(rename = "newStart")]
604 pub new_start: usize,
605 #[serde(rename = "newCount")]
607 #[ts(rename = "newCount")]
608 pub new_count: usize,
609}
610
611#[derive(Debug, Clone, Serialize, Deserialize, TS)]
613#[serde(deny_unknown_fields)]
614#[ts(export, rename = "TsCreateCompositeBufferOptions")]
615pub struct CreateCompositeBufferOptions {
616 #[serde(default)]
618 pub name: String,
619 #[serde(default)]
621 pub mode: String,
622 pub layout: CompositeLayoutConfig,
624 pub sources: Vec<CompositeSourceConfig>,
626 #[serde(default)]
628 pub hunks: Option<Vec<CompositeHunk>>,
629}
630
631#[derive(Debug, Clone, Serialize, Deserialize, TS)]
633#[ts(export)]
634pub enum ViewTokenWireKind {
635 Text(String),
636 Newline,
637 Space,
638 Break,
641 BinaryByte(u8),
645}
646
647#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
653#[serde(deny_unknown_fields)]
654#[ts(export)]
655pub struct ViewTokenStyle {
656 #[serde(default)]
658 #[ts(type = "[number, number, number] | null")]
659 pub fg: Option<(u8, u8, u8)>,
660 #[serde(default)]
662 #[ts(type = "[number, number, number] | null")]
663 pub bg: Option<(u8, u8, u8)>,
664 #[serde(default)]
666 pub bold: bool,
667 #[serde(default)]
669 pub italic: bool,
670}
671
672#[derive(Debug, Clone, Serialize, Deserialize, TS)]
674#[serde(deny_unknown_fields)]
675#[ts(export)]
676pub struct ViewTokenWire {
677 #[ts(type = "number | null")]
679 pub source_offset: Option<usize>,
680 pub kind: ViewTokenWireKind,
682 #[serde(default)]
684 #[ts(optional)]
685 pub style: Option<ViewTokenStyle>,
686}
687
688#[derive(Debug, Clone, Serialize, Deserialize, TS)]
690#[ts(export)]
691pub struct ViewTransformPayload {
692 pub range: Range<usize>,
694 pub tokens: Vec<ViewTokenWire>,
696 pub layout_hints: Option<LayoutHints>,
698}
699
700#[derive(Debug, Clone, Serialize, Deserialize, TS)]
703#[ts(export)]
704pub struct EditorStateSnapshot {
705 pub active_buffer_id: BufferId,
707 pub active_split_id: usize,
709 pub buffers: HashMap<BufferId, BufferInfo>,
711 pub buffer_saved_diffs: HashMap<BufferId, BufferSavedDiff>,
713 pub primary_cursor: Option<CursorInfo>,
715 pub all_cursors: Vec<CursorInfo>,
717 pub viewport: Option<ViewportInfo>,
719 pub buffer_cursor_positions: HashMap<BufferId, usize>,
721 pub buffer_text_properties: HashMap<BufferId, Vec<TextProperty>>,
723 pub selected_text: Option<String>,
726 pub clipboard: String,
728 pub working_dir: PathBuf,
730 #[ts(type = "any")]
733 pub diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
734 #[ts(type = "any")]
737 pub folding_ranges: HashMap<String, Vec<lsp_types::FoldingRange>>,
738 #[ts(type = "any")]
741 pub config: serde_json::Value,
742 #[ts(type = "any")]
745 pub user_config: serde_json::Value,
746 pub editor_mode: Option<String>,
749
750 #[ts(type = "any")]
754 pub plugin_view_states: HashMap<BufferId, HashMap<String, serde_json::Value>>,
755
756 #[serde(skip)]
759 #[ts(skip)]
760 pub plugin_view_states_split: usize,
761}
762
763impl EditorStateSnapshot {
764 pub fn new() -> Self {
765 Self {
766 active_buffer_id: BufferId(0),
767 active_split_id: 0,
768 buffers: HashMap::new(),
769 buffer_saved_diffs: HashMap::new(),
770 primary_cursor: None,
771 all_cursors: Vec::new(),
772 viewport: None,
773 buffer_cursor_positions: HashMap::new(),
774 buffer_text_properties: HashMap::new(),
775 selected_text: None,
776 clipboard: String::new(),
777 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
778 diagnostics: HashMap::new(),
779 folding_ranges: HashMap::new(),
780 config: serde_json::Value::Null,
781 user_config: serde_json::Value::Null,
782 editor_mode: None,
783 plugin_view_states: HashMap::new(),
784 plugin_view_states_split: 0,
785 }
786 }
787}
788
789impl Default for EditorStateSnapshot {
790 fn default() -> Self {
791 Self::new()
792 }
793}
794
795#[derive(Debug, Clone, Serialize, Deserialize, TS)]
797#[ts(export)]
798pub enum MenuPosition {
799 Top,
801 Bottom,
803 Before(String),
805 After(String),
807}
808
809#[derive(Debug, Clone, Serialize, Deserialize, TS)]
811#[ts(export)]
812pub enum PluginCommand {
813 InsertText {
815 buffer_id: BufferId,
816 position: usize,
817 text: String,
818 },
819
820 DeleteRange {
822 buffer_id: BufferId,
823 range: Range<usize>,
824 },
825
826 AddOverlay {
831 buffer_id: BufferId,
832 namespace: Option<OverlayNamespace>,
833 range: Range<usize>,
834 options: OverlayOptions,
836 },
837
838 RemoveOverlay {
840 buffer_id: BufferId,
841 handle: OverlayHandle,
842 },
843
844 SetStatus { message: String },
846
847 ApplyTheme { theme_name: String },
849
850 ReloadConfig,
853
854 RegisterCommand { command: Command },
856
857 UnregisterCommand { name: String },
859
860 OpenFileInBackground { path: PathBuf },
862
863 InsertAtCursor { text: String },
865
866 SpawnProcess {
868 command: String,
869 args: Vec<String>,
870 cwd: Option<String>,
871 callback_id: JsCallbackId,
872 },
873
874 Delay {
876 callback_id: JsCallbackId,
877 duration_ms: u64,
878 },
879
880 SpawnBackgroundProcess {
884 process_id: u64,
886 command: String,
888 args: Vec<String>,
890 cwd: Option<String>,
892 callback_id: JsCallbackId,
894 },
895
896 KillBackgroundProcess { process_id: u64 },
898
899 SpawnProcessWait {
902 process_id: u64,
904 callback_id: JsCallbackId,
906 },
907
908 SetLayoutHints {
910 buffer_id: BufferId,
911 split_id: Option<SplitId>,
912 range: Range<usize>,
913 hints: LayoutHints,
914 },
915
916 SetLineNumbers { buffer_id: BufferId, enabled: bool },
918
919 SetViewMode { buffer_id: BufferId, mode: String },
921
922 SetLineWrap {
924 buffer_id: BufferId,
925 split_id: Option<SplitId>,
926 enabled: bool,
927 },
928
929 SubmitViewTransform {
931 buffer_id: BufferId,
932 split_id: Option<SplitId>,
933 payload: ViewTransformPayload,
934 },
935
936 ClearViewTransform {
938 buffer_id: BufferId,
939 split_id: Option<SplitId>,
940 },
941
942 SetViewState {
945 buffer_id: BufferId,
946 key: String,
947 #[ts(type = "any")]
948 value: Option<serde_json::Value>,
949 },
950
951 ClearAllOverlays { buffer_id: BufferId },
953
954 ClearNamespace {
956 buffer_id: BufferId,
957 namespace: OverlayNamespace,
958 },
959
960 ClearOverlaysInRange {
963 buffer_id: BufferId,
964 start: usize,
965 end: usize,
966 },
967
968 AddVirtualText {
971 buffer_id: BufferId,
972 virtual_text_id: String,
973 position: usize,
974 text: String,
975 color: (u8, u8, u8),
976 use_bg: bool, before: bool, },
979
980 RemoveVirtualText {
982 buffer_id: BufferId,
983 virtual_text_id: String,
984 },
985
986 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
988
989 ClearVirtualTexts { buffer_id: BufferId },
991
992 AddVirtualLine {
996 buffer_id: BufferId,
997 position: usize,
999 text: String,
1001 fg_color: (u8, u8, u8),
1003 bg_color: Option<(u8, u8, u8)>,
1005 above: bool,
1007 namespace: String,
1009 priority: i32,
1011 },
1012
1013 ClearVirtualTextNamespace {
1016 buffer_id: BufferId,
1017 namespace: String,
1018 },
1019
1020 AddConceal {
1023 buffer_id: BufferId,
1024 namespace: OverlayNamespace,
1026 start: usize,
1028 end: usize,
1029 replacement: Option<String>,
1031 },
1032
1033 ClearConcealNamespace {
1035 buffer_id: BufferId,
1036 namespace: OverlayNamespace,
1037 },
1038
1039 ClearConcealsInRange {
1042 buffer_id: BufferId,
1043 start: usize,
1044 end: usize,
1045 },
1046
1047 AddSoftBreak {
1051 buffer_id: BufferId,
1052 namespace: OverlayNamespace,
1054 position: usize,
1056 indent: u16,
1058 },
1059
1060 ClearSoftBreakNamespace {
1062 buffer_id: BufferId,
1063 namespace: OverlayNamespace,
1064 },
1065
1066 ClearSoftBreaksInRange {
1068 buffer_id: BufferId,
1069 start: usize,
1070 end: usize,
1071 },
1072
1073 RefreshLines { buffer_id: BufferId },
1075
1076 RefreshAllLines,
1080
1081 HookCompleted { hook_name: String },
1085
1086 SetLineIndicator {
1089 buffer_id: BufferId,
1090 line: usize,
1092 namespace: String,
1094 symbol: String,
1096 color: (u8, u8, u8),
1098 priority: i32,
1100 },
1101
1102 SetLineIndicators {
1105 buffer_id: BufferId,
1106 lines: Vec<usize>,
1108 namespace: String,
1110 symbol: String,
1112 color: (u8, u8, u8),
1114 priority: i32,
1116 },
1117
1118 ClearLineIndicators {
1120 buffer_id: BufferId,
1121 namespace: String,
1123 },
1124
1125 SetFileExplorerDecorations {
1127 namespace: String,
1129 decorations: Vec<FileExplorerDecoration>,
1131 },
1132
1133 ClearFileExplorerDecorations {
1135 namespace: String,
1137 },
1138
1139 OpenFileAtLocation {
1142 path: PathBuf,
1143 line: Option<usize>, column: Option<usize>, },
1146
1147 OpenFileInSplit {
1150 split_id: usize,
1151 path: PathBuf,
1152 line: Option<usize>, column: Option<usize>, },
1155
1156 StartPrompt {
1159 label: String,
1160 prompt_type: String, },
1162
1163 StartPromptWithInitial {
1165 label: String,
1166 prompt_type: String,
1167 initial_value: String,
1168 },
1169
1170 StartPromptAsync {
1173 label: String,
1174 initial_value: String,
1175 callback_id: JsCallbackId,
1176 },
1177
1178 SetPromptSuggestions { suggestions: Vec<Suggestion> },
1181
1182 SetPromptInputSync { sync: bool },
1184
1185 AddMenuItem {
1188 menu_label: String,
1189 item: MenuItem,
1190 position: MenuPosition,
1191 },
1192
1193 AddMenu { menu: Menu, position: MenuPosition },
1195
1196 RemoveMenuItem {
1198 menu_label: String,
1199 item_label: String,
1200 },
1201
1202 RemoveMenu { menu_label: String },
1204
1205 CreateVirtualBuffer {
1207 name: String,
1209 mode: String,
1211 read_only: bool,
1213 },
1214
1215 CreateVirtualBufferWithContent {
1219 name: String,
1221 mode: String,
1223 read_only: bool,
1225 entries: Vec<TextPropertyEntry>,
1227 show_line_numbers: bool,
1229 show_cursors: bool,
1231 editing_disabled: bool,
1233 hidden_from_tabs: bool,
1235 request_id: Option<u64>,
1237 },
1238
1239 CreateVirtualBufferInSplit {
1242 name: String,
1244 mode: String,
1246 read_only: bool,
1248 entries: Vec<TextPropertyEntry>,
1250 ratio: f32,
1252 direction: Option<String>,
1254 panel_id: Option<String>,
1256 show_line_numbers: bool,
1258 show_cursors: bool,
1260 editing_disabled: bool,
1262 line_wrap: Option<bool>,
1264 before: bool,
1266 request_id: Option<u64>,
1268 },
1269
1270 SetVirtualBufferContent {
1272 buffer_id: BufferId,
1273 entries: Vec<TextPropertyEntry>,
1275 },
1276
1277 GetTextPropertiesAtCursor { buffer_id: BufferId },
1279
1280 DefineMode {
1282 name: String,
1283 parent: Option<String>,
1284 bindings: Vec<(String, String)>, read_only: bool,
1286 },
1287
1288 ShowBuffer { buffer_id: BufferId },
1290
1291 CreateVirtualBufferInExistingSplit {
1293 name: String,
1295 mode: String,
1297 read_only: bool,
1299 entries: Vec<TextPropertyEntry>,
1301 split_id: SplitId,
1303 show_line_numbers: bool,
1305 show_cursors: bool,
1307 editing_disabled: bool,
1309 line_wrap: Option<bool>,
1311 request_id: Option<u64>,
1313 },
1314
1315 CloseBuffer { buffer_id: BufferId },
1317
1318 CreateCompositeBuffer {
1321 name: String,
1323 mode: String,
1325 layout: CompositeLayoutConfig,
1327 sources: Vec<CompositeSourceConfig>,
1329 hunks: Option<Vec<CompositeHunk>>,
1331 request_id: Option<u64>,
1333 },
1334
1335 UpdateCompositeAlignment {
1337 buffer_id: BufferId,
1338 hunks: Vec<CompositeHunk>,
1339 },
1340
1341 CloseCompositeBuffer { buffer_id: BufferId },
1343
1344 FocusSplit { split_id: SplitId },
1346
1347 SetSplitBuffer {
1349 split_id: SplitId,
1350 buffer_id: BufferId,
1351 },
1352
1353 SetSplitScroll { split_id: SplitId, top_byte: usize },
1355
1356 RequestHighlights {
1358 buffer_id: BufferId,
1359 range: Range<usize>,
1360 request_id: u64,
1361 },
1362
1363 CloseSplit { split_id: SplitId },
1365
1366 SetSplitRatio {
1368 split_id: SplitId,
1369 ratio: f32,
1371 },
1372
1373 SetSplitLabel { split_id: SplitId, label: String },
1375
1376 ClearSplitLabel { split_id: SplitId },
1378
1379 GetSplitByLabel { label: String, request_id: u64 },
1381
1382 DistributeSplitsEvenly {
1384 split_ids: Vec<SplitId>,
1386 },
1387
1388 SetBufferCursor {
1390 buffer_id: BufferId,
1391 position: usize,
1393 },
1394
1395 SendLspRequest {
1397 language: String,
1398 method: String,
1399 #[ts(type = "any")]
1400 params: Option<JsonValue>,
1401 request_id: u64,
1402 },
1403
1404 SetClipboard { text: String },
1406
1407 DeleteSelection,
1410
1411 SetContext {
1415 name: String,
1417 active: bool,
1419 },
1420
1421 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1423
1424 ExecuteAction {
1427 action_name: String,
1429 },
1430
1431 ExecuteActions {
1435 actions: Vec<ActionSpec>,
1437 },
1438
1439 GetBufferText {
1441 buffer_id: BufferId,
1443 start: usize,
1445 end: usize,
1447 request_id: u64,
1449 },
1450
1451 GetLineStartPosition {
1454 buffer_id: BufferId,
1456 line: u32,
1458 request_id: u64,
1460 },
1461
1462 GetLineEndPosition {
1466 buffer_id: BufferId,
1468 line: u32,
1470 request_id: u64,
1472 },
1473
1474 GetBufferLineCount {
1476 buffer_id: BufferId,
1478 request_id: u64,
1480 },
1481
1482 ScrollToLineCenter {
1485 split_id: SplitId,
1487 buffer_id: BufferId,
1489 line: usize,
1491 },
1492
1493 SetEditorMode {
1496 mode: Option<String>,
1498 },
1499
1500 ShowActionPopup {
1503 popup_id: String,
1505 title: String,
1507 message: String,
1509 actions: Vec<ActionPopupAction>,
1511 },
1512
1513 DisableLspForLanguage {
1515 language: String,
1517 },
1518
1519 RestartLspForLanguage {
1521 language: String,
1523 },
1524
1525 SetLspRootUri {
1529 language: String,
1531 uri: String,
1533 },
1534
1535 CreateScrollSyncGroup {
1539 group_id: u32,
1541 left_split: SplitId,
1543 right_split: SplitId,
1545 },
1546
1547 SetScrollSyncAnchors {
1550 group_id: u32,
1552 anchors: Vec<(usize, usize)>,
1554 },
1555
1556 RemoveScrollSyncGroup {
1558 group_id: u32,
1560 },
1561
1562 SaveBufferToPath {
1565 buffer_id: BufferId,
1567 path: PathBuf,
1569 },
1570
1571 LoadPlugin {
1574 path: PathBuf,
1576 callback_id: JsCallbackId,
1578 },
1579
1580 UnloadPlugin {
1583 name: String,
1585 callback_id: JsCallbackId,
1587 },
1588
1589 ReloadPlugin {
1592 name: String,
1594 callback_id: JsCallbackId,
1596 },
1597
1598 ListPlugins {
1601 callback_id: JsCallbackId,
1603 },
1604
1605 ReloadThemes { apply_theme: Option<String> },
1609
1610 RegisterGrammar {
1613 language: String,
1615 grammar_path: String,
1617 extensions: Vec<String>,
1619 },
1620
1621 RegisterLanguageConfig {
1624 language: String,
1626 config: LanguagePackConfig,
1628 },
1629
1630 RegisterLspServer {
1633 language: String,
1635 config: LspServerPackConfig,
1637 },
1638
1639 ReloadGrammars { callback_id: JsCallbackId },
1643
1644 CreateTerminal {
1648 cwd: Option<String>,
1650 direction: Option<String>,
1652 ratio: Option<f32>,
1654 focus: Option<bool>,
1656 request_id: u64,
1658 },
1659
1660 SendTerminalInput {
1662 terminal_id: TerminalId,
1664 data: String,
1666 },
1667
1668 CloseTerminal {
1670 terminal_id: TerminalId,
1672 },
1673}
1674
1675impl PluginCommand {
1676 pub fn debug_variant_name(&self) -> String {
1678 let dbg = format!("{:?}", self);
1679 dbg.split([' ', '{', '(']).next().unwrap_or("?").to_string()
1680 }
1681}
1682
1683#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
1692#[serde(rename_all = "camelCase")]
1693#[ts(export)]
1694pub struct LanguagePackConfig {
1695 #[serde(default)]
1697 pub comment_prefix: Option<String>,
1698
1699 #[serde(default)]
1701 pub block_comment_start: Option<String>,
1702
1703 #[serde(default)]
1705 pub block_comment_end: Option<String>,
1706
1707 #[serde(default)]
1709 pub use_tabs: Option<bool>,
1710
1711 #[serde(default)]
1713 pub tab_size: Option<usize>,
1714
1715 #[serde(default)]
1717 pub auto_indent: Option<bool>,
1718
1719 #[serde(default)]
1722 pub show_whitespace_tabs: Option<bool>,
1723
1724 #[serde(default)]
1726 pub formatter: Option<FormatterPackConfig>,
1727}
1728
1729#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1731#[serde(rename_all = "camelCase")]
1732#[ts(export)]
1733pub struct FormatterPackConfig {
1734 pub command: String,
1736
1737 #[serde(default)]
1739 pub args: Vec<String>,
1740}
1741
1742#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1744#[serde(rename_all = "camelCase")]
1745#[ts(export)]
1746pub struct ProcessLimitsPackConfig {
1747 #[serde(default)]
1749 pub max_memory_percent: Option<u32>,
1750
1751 #[serde(default)]
1753 pub max_cpu_percent: Option<u32>,
1754
1755 #[serde(default)]
1757 pub enabled: Option<bool>,
1758}
1759
1760#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1762#[serde(rename_all = "camelCase")]
1763#[ts(export)]
1764pub struct LspServerPackConfig {
1765 pub command: String,
1767
1768 #[serde(default)]
1770 pub args: Vec<String>,
1771
1772 #[serde(default)]
1774 pub auto_start: Option<bool>,
1775
1776 #[serde(default)]
1778 #[ts(type = "Record<string, unknown> | null")]
1779 pub initialization_options: Option<JsonValue>,
1780
1781 #[serde(default)]
1783 pub process_limits: Option<ProcessLimitsPackConfig>,
1784}
1785
1786#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1788#[ts(export)]
1789pub enum HunkStatus {
1790 Pending,
1791 Staged,
1792 Discarded,
1793}
1794
1795#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1797#[ts(export)]
1798pub struct ReviewHunk {
1799 pub id: String,
1800 pub file: String,
1801 pub context_header: String,
1802 pub status: HunkStatus,
1803 pub base_range: Option<(usize, usize)>,
1805 pub modified_range: Option<(usize, usize)>,
1807}
1808
1809#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1811#[serde(deny_unknown_fields)]
1812#[ts(export, rename = "TsActionPopupAction")]
1813pub struct ActionPopupAction {
1814 pub id: String,
1816 pub label: String,
1818}
1819
1820#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1822#[serde(deny_unknown_fields)]
1823#[ts(export)]
1824pub struct ActionPopupOptions {
1825 pub id: String,
1827 pub title: String,
1829 pub message: String,
1831 pub actions: Vec<ActionPopupAction>,
1833}
1834
1835#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1837#[ts(export)]
1838pub struct TsHighlightSpan {
1839 pub start: u32,
1840 pub end: u32,
1841 #[ts(type = "[number, number, number]")]
1842 pub color: (u8, u8, u8),
1843 pub bold: bool,
1844 pub italic: bool,
1845}
1846
1847#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1849#[ts(export)]
1850pub struct SpawnResult {
1851 pub stdout: String,
1853 pub stderr: String,
1855 pub exit_code: i32,
1857}
1858
1859#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1861#[ts(export)]
1862pub struct BackgroundProcessResult {
1863 #[ts(type = "number")]
1865 pub process_id: u64,
1866 pub exit_code: i32,
1869}
1870
1871#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1873#[serde(deny_unknown_fields, rename_all = "camelCase")]
1874#[ts(export, rename = "TextPropertyEntry", rename_all = "camelCase")]
1875pub struct JsTextPropertyEntry {
1876 pub text: String,
1878 #[serde(default)]
1880 #[ts(optional, type = "Record<string, unknown>")]
1881 pub properties: Option<HashMap<String, JsonValue>>,
1882 #[serde(default)]
1884 #[ts(optional, type = "Partial<OverlayOptions>")]
1885 pub style: Option<OverlayOptions>,
1886 #[serde(default)]
1888 #[ts(optional)]
1889 pub inline_overlays: Option<Vec<crate::text_property::InlineOverlay>>,
1890}
1891
1892#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1894#[ts(export)]
1895pub struct DirEntry {
1896 pub name: String,
1898 pub is_file: bool,
1900 pub is_dir: bool,
1902}
1903
1904#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1906#[ts(export)]
1907pub struct JsPosition {
1908 pub line: u32,
1910 pub character: u32,
1912}
1913
1914#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1916#[ts(export)]
1917pub struct JsRange {
1918 pub start: JsPosition,
1920 pub end: JsPosition,
1922}
1923
1924#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1926#[ts(export)]
1927pub struct JsDiagnostic {
1928 pub uri: String,
1930 pub message: String,
1932 pub severity: Option<u8>,
1934 pub range: JsRange,
1936 #[ts(optional)]
1938 pub source: Option<String>,
1939}
1940
1941#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1943#[serde(deny_unknown_fields)]
1944#[ts(export)]
1945pub struct CreateVirtualBufferOptions {
1946 pub name: String,
1948 #[serde(default)]
1950 #[ts(optional)]
1951 pub mode: Option<String>,
1952 #[serde(default, rename = "readOnly")]
1954 #[ts(optional, rename = "readOnly")]
1955 pub read_only: Option<bool>,
1956 #[serde(default, rename = "showLineNumbers")]
1958 #[ts(optional, rename = "showLineNumbers")]
1959 pub show_line_numbers: Option<bool>,
1960 #[serde(default, rename = "showCursors")]
1962 #[ts(optional, rename = "showCursors")]
1963 pub show_cursors: Option<bool>,
1964 #[serde(default, rename = "editingDisabled")]
1966 #[ts(optional, rename = "editingDisabled")]
1967 pub editing_disabled: Option<bool>,
1968 #[serde(default, rename = "hiddenFromTabs")]
1970 #[ts(optional, rename = "hiddenFromTabs")]
1971 pub hidden_from_tabs: Option<bool>,
1972 #[serde(default)]
1974 #[ts(optional)]
1975 pub entries: Option<Vec<JsTextPropertyEntry>>,
1976}
1977
1978#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1980#[serde(deny_unknown_fields)]
1981#[ts(export)]
1982pub struct CreateVirtualBufferInSplitOptions {
1983 pub name: String,
1985 #[serde(default)]
1987 #[ts(optional)]
1988 pub mode: Option<String>,
1989 #[serde(default, rename = "readOnly")]
1991 #[ts(optional, rename = "readOnly")]
1992 pub read_only: Option<bool>,
1993 #[serde(default)]
1995 #[ts(optional)]
1996 pub ratio: Option<f32>,
1997 #[serde(default)]
1999 #[ts(optional)]
2000 pub direction: Option<String>,
2001 #[serde(default, rename = "panelId")]
2003 #[ts(optional, rename = "panelId")]
2004 pub panel_id: Option<String>,
2005 #[serde(default, rename = "showLineNumbers")]
2007 #[ts(optional, rename = "showLineNumbers")]
2008 pub show_line_numbers: Option<bool>,
2009 #[serde(default, rename = "showCursors")]
2011 #[ts(optional, rename = "showCursors")]
2012 pub show_cursors: Option<bool>,
2013 #[serde(default, rename = "editingDisabled")]
2015 #[ts(optional, rename = "editingDisabled")]
2016 pub editing_disabled: Option<bool>,
2017 #[serde(default, rename = "lineWrap")]
2019 #[ts(optional, rename = "lineWrap")]
2020 pub line_wrap: Option<bool>,
2021 #[serde(default)]
2023 #[ts(optional)]
2024 pub before: Option<bool>,
2025 #[serde(default)]
2027 #[ts(optional)]
2028 pub entries: Option<Vec<JsTextPropertyEntry>>,
2029}
2030
2031#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2033#[serde(deny_unknown_fields)]
2034#[ts(export)]
2035pub struct CreateVirtualBufferInExistingSplitOptions {
2036 pub name: String,
2038 #[serde(rename = "splitId")]
2040 #[ts(rename = "splitId")]
2041 pub split_id: usize,
2042 #[serde(default)]
2044 #[ts(optional)]
2045 pub mode: Option<String>,
2046 #[serde(default, rename = "readOnly")]
2048 #[ts(optional, rename = "readOnly")]
2049 pub read_only: Option<bool>,
2050 #[serde(default, rename = "showLineNumbers")]
2052 #[ts(optional, rename = "showLineNumbers")]
2053 pub show_line_numbers: Option<bool>,
2054 #[serde(default, rename = "showCursors")]
2056 #[ts(optional, rename = "showCursors")]
2057 pub show_cursors: Option<bool>,
2058 #[serde(default, rename = "editingDisabled")]
2060 #[ts(optional, rename = "editingDisabled")]
2061 pub editing_disabled: Option<bool>,
2062 #[serde(default, rename = "lineWrap")]
2064 #[ts(optional, rename = "lineWrap")]
2065 pub line_wrap: Option<bool>,
2066 #[serde(default)]
2068 #[ts(optional)]
2069 pub entries: Option<Vec<JsTextPropertyEntry>>,
2070}
2071
2072#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2074#[serde(deny_unknown_fields)]
2075#[ts(export)]
2076pub struct CreateTerminalOptions {
2077 #[serde(default)]
2079 #[ts(optional)]
2080 pub cwd: Option<String>,
2081 #[serde(default)]
2083 #[ts(optional)]
2084 pub direction: Option<String>,
2085 #[serde(default)]
2087 #[ts(optional)]
2088 pub ratio: Option<f32>,
2089 #[serde(default)]
2091 #[ts(optional)]
2092 pub focus: Option<bool>,
2093}
2094
2095#[derive(Debug, Clone, Serialize, TS)]
2100#[ts(export, type = "Array<Record<string, unknown>>")]
2101pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
2102
2103#[cfg(feature = "plugins")]
2105mod fromjs_impls {
2106 use super::*;
2107 use rquickjs::{Ctx, FromJs, Value};
2108
2109 impl<'js> FromJs<'js> for JsTextPropertyEntry {
2110 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2111 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2112 from: "object",
2113 to: "JsTextPropertyEntry",
2114 message: Some(e.to_string()),
2115 })
2116 }
2117 }
2118
2119 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
2120 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2121 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2122 from: "object",
2123 to: "CreateVirtualBufferOptions",
2124 message: Some(e.to_string()),
2125 })
2126 }
2127 }
2128
2129 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
2130 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2131 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2132 from: "object",
2133 to: "CreateVirtualBufferInSplitOptions",
2134 message: Some(e.to_string()),
2135 })
2136 }
2137 }
2138
2139 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
2140 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2141 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2142 from: "object",
2143 to: "CreateVirtualBufferInExistingSplitOptions",
2144 message: Some(e.to_string()),
2145 })
2146 }
2147 }
2148
2149 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
2150 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
2151 rquickjs_serde::to_value(ctx.clone(), &self.0)
2152 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
2153 }
2154 }
2155
2156 impl<'js> FromJs<'js> for ActionSpec {
2159 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2160 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2161 from: "object",
2162 to: "ActionSpec",
2163 message: Some(e.to_string()),
2164 })
2165 }
2166 }
2167
2168 impl<'js> FromJs<'js> for ActionPopupAction {
2169 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2170 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2171 from: "object",
2172 to: "ActionPopupAction",
2173 message: Some(e.to_string()),
2174 })
2175 }
2176 }
2177
2178 impl<'js> FromJs<'js> for ActionPopupOptions {
2179 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2180 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2181 from: "object",
2182 to: "ActionPopupOptions",
2183 message: Some(e.to_string()),
2184 })
2185 }
2186 }
2187
2188 impl<'js> FromJs<'js> for ViewTokenWire {
2189 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2190 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2191 from: "object",
2192 to: "ViewTokenWire",
2193 message: Some(e.to_string()),
2194 })
2195 }
2196 }
2197
2198 impl<'js> FromJs<'js> for ViewTokenStyle {
2199 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2200 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2201 from: "object",
2202 to: "ViewTokenStyle",
2203 message: Some(e.to_string()),
2204 })
2205 }
2206 }
2207
2208 impl<'js> FromJs<'js> for LayoutHints {
2209 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2210 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2211 from: "object",
2212 to: "LayoutHints",
2213 message: Some(e.to_string()),
2214 })
2215 }
2216 }
2217
2218 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
2219 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2220 let json: serde_json::Value =
2222 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2223 from: "object",
2224 to: "CreateCompositeBufferOptions (json)",
2225 message: Some(e.to_string()),
2226 })?;
2227 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
2228 from: "json",
2229 to: "CreateCompositeBufferOptions",
2230 message: Some(e.to_string()),
2231 })
2232 }
2233 }
2234
2235 impl<'js> FromJs<'js> for CompositeHunk {
2236 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2237 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2238 from: "object",
2239 to: "CompositeHunk",
2240 message: Some(e.to_string()),
2241 })
2242 }
2243 }
2244
2245 impl<'js> FromJs<'js> for LanguagePackConfig {
2246 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2247 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2248 from: "object",
2249 to: "LanguagePackConfig",
2250 message: Some(e.to_string()),
2251 })
2252 }
2253 }
2254
2255 impl<'js> FromJs<'js> for LspServerPackConfig {
2256 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2257 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2258 from: "object",
2259 to: "LspServerPackConfig",
2260 message: Some(e.to_string()),
2261 })
2262 }
2263 }
2264
2265 impl<'js> FromJs<'js> for ProcessLimitsPackConfig {
2266 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2267 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2268 from: "object",
2269 to: "ProcessLimitsPackConfig",
2270 message: Some(e.to_string()),
2271 })
2272 }
2273 }
2274
2275 impl<'js> FromJs<'js> for CreateTerminalOptions {
2276 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2277 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2278 from: "object",
2279 to: "CreateTerminalOptions",
2280 message: Some(e.to_string()),
2281 })
2282 }
2283 }
2284}
2285
2286pub struct PluginApi {
2288 hooks: Arc<RwLock<HookRegistry>>,
2290
2291 commands: Arc<RwLock<CommandRegistry>>,
2293
2294 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2296
2297 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2299}
2300
2301impl PluginApi {
2302 pub fn new(
2304 hooks: Arc<RwLock<HookRegistry>>,
2305 commands: Arc<RwLock<CommandRegistry>>,
2306 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2307 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2308 ) -> Self {
2309 Self {
2310 hooks,
2311 commands,
2312 command_sender,
2313 state_snapshot,
2314 }
2315 }
2316
2317 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
2319 let mut hooks = self.hooks.write().unwrap();
2320 hooks.add_hook(hook_name, callback);
2321 }
2322
2323 pub fn unregister_hooks(&self, hook_name: &str) {
2325 let mut hooks = self.hooks.write().unwrap();
2326 hooks.remove_hooks(hook_name);
2327 }
2328
2329 pub fn register_command(&self, command: Command) {
2331 let commands = self.commands.read().unwrap();
2332 commands.register(command);
2333 }
2334
2335 pub fn unregister_command(&self, name: &str) {
2337 let commands = self.commands.read().unwrap();
2338 commands.unregister(name);
2339 }
2340
2341 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
2343 self.command_sender
2344 .send(command)
2345 .map_err(|e| format!("Failed to send command: {}", e))
2346 }
2347
2348 pub fn insert_text(
2350 &self,
2351 buffer_id: BufferId,
2352 position: usize,
2353 text: String,
2354 ) -> Result<(), String> {
2355 self.send_command(PluginCommand::InsertText {
2356 buffer_id,
2357 position,
2358 text,
2359 })
2360 }
2361
2362 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
2364 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
2365 }
2366
2367 pub fn add_overlay(
2375 &self,
2376 buffer_id: BufferId,
2377 namespace: Option<String>,
2378 range: Range<usize>,
2379 options: OverlayOptions,
2380 ) -> Result<(), String> {
2381 self.send_command(PluginCommand::AddOverlay {
2382 buffer_id,
2383 namespace: namespace.map(OverlayNamespace::from_string),
2384 range,
2385 options,
2386 })
2387 }
2388
2389 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
2391 self.send_command(PluginCommand::RemoveOverlay {
2392 buffer_id,
2393 handle: OverlayHandle::from_string(handle),
2394 })
2395 }
2396
2397 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
2399 self.send_command(PluginCommand::ClearNamespace {
2400 buffer_id,
2401 namespace: OverlayNamespace::from_string(namespace),
2402 })
2403 }
2404
2405 pub fn clear_overlays_in_range(
2408 &self,
2409 buffer_id: BufferId,
2410 start: usize,
2411 end: usize,
2412 ) -> Result<(), String> {
2413 self.send_command(PluginCommand::ClearOverlaysInRange {
2414 buffer_id,
2415 start,
2416 end,
2417 })
2418 }
2419
2420 pub fn set_status(&self, message: String) -> Result<(), String> {
2422 self.send_command(PluginCommand::SetStatus { message })
2423 }
2424
2425 pub fn open_file_at_location(
2428 &self,
2429 path: PathBuf,
2430 line: Option<usize>,
2431 column: Option<usize>,
2432 ) -> Result<(), String> {
2433 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
2434 }
2435
2436 pub fn open_file_in_split(
2441 &self,
2442 split_id: usize,
2443 path: PathBuf,
2444 line: Option<usize>,
2445 column: Option<usize>,
2446 ) -> Result<(), String> {
2447 self.send_command(PluginCommand::OpenFileInSplit {
2448 split_id,
2449 path,
2450 line,
2451 column,
2452 })
2453 }
2454
2455 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
2458 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
2459 }
2460
2461 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
2464 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
2465 }
2466
2467 pub fn set_prompt_input_sync(&self, sync: bool) -> Result<(), String> {
2469 self.send_command(PluginCommand::SetPromptInputSync { sync })
2470 }
2471
2472 pub fn add_menu_item(
2474 &self,
2475 menu_label: String,
2476 item: MenuItem,
2477 position: MenuPosition,
2478 ) -> Result<(), String> {
2479 self.send_command(PluginCommand::AddMenuItem {
2480 menu_label,
2481 item,
2482 position,
2483 })
2484 }
2485
2486 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
2488 self.send_command(PluginCommand::AddMenu { menu, position })
2489 }
2490
2491 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
2493 self.send_command(PluginCommand::RemoveMenuItem {
2494 menu_label,
2495 item_label,
2496 })
2497 }
2498
2499 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
2501 self.send_command(PluginCommand::RemoveMenu { menu_label })
2502 }
2503
2504 pub fn create_virtual_buffer(
2511 &self,
2512 name: String,
2513 mode: String,
2514 read_only: bool,
2515 ) -> Result<(), String> {
2516 self.send_command(PluginCommand::CreateVirtualBuffer {
2517 name,
2518 mode,
2519 read_only,
2520 })
2521 }
2522
2523 pub fn create_virtual_buffer_with_content(
2529 &self,
2530 name: String,
2531 mode: String,
2532 read_only: bool,
2533 entries: Vec<TextPropertyEntry>,
2534 ) -> Result<(), String> {
2535 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
2536 name,
2537 mode,
2538 read_only,
2539 entries,
2540 show_line_numbers: true,
2541 show_cursors: true,
2542 editing_disabled: false,
2543 hidden_from_tabs: false,
2544 request_id: None,
2545 })
2546 }
2547
2548 pub fn set_virtual_buffer_content(
2552 &self,
2553 buffer_id: BufferId,
2554 entries: Vec<TextPropertyEntry>,
2555 ) -> Result<(), String> {
2556 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
2557 }
2558
2559 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
2563 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
2564 }
2565
2566 pub fn define_mode(
2571 &self,
2572 name: String,
2573 parent: Option<String>,
2574 bindings: Vec<(String, String)>,
2575 read_only: bool,
2576 ) -> Result<(), String> {
2577 self.send_command(PluginCommand::DefineMode {
2578 name,
2579 parent,
2580 bindings,
2581 read_only,
2582 })
2583 }
2584
2585 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
2587 self.send_command(PluginCommand::ShowBuffer { buffer_id })
2588 }
2589
2590 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
2592 self.send_command(PluginCommand::SetSplitScroll {
2593 split_id: SplitId(split_id),
2594 top_byte,
2595 })
2596 }
2597
2598 pub fn get_highlights(
2600 &self,
2601 buffer_id: BufferId,
2602 range: Range<usize>,
2603 request_id: u64,
2604 ) -> Result<(), String> {
2605 self.send_command(PluginCommand::RequestHighlights {
2606 buffer_id,
2607 range,
2608 request_id,
2609 })
2610 }
2611
2612 pub fn get_active_buffer_id(&self) -> BufferId {
2616 let snapshot = self.state_snapshot.read().unwrap();
2617 snapshot.active_buffer_id
2618 }
2619
2620 pub fn get_active_split_id(&self) -> usize {
2622 let snapshot = self.state_snapshot.read().unwrap();
2623 snapshot.active_split_id
2624 }
2625
2626 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
2628 let snapshot = self.state_snapshot.read().unwrap();
2629 snapshot.buffers.get(&buffer_id).cloned()
2630 }
2631
2632 pub fn list_buffers(&self) -> Vec<BufferInfo> {
2634 let snapshot = self.state_snapshot.read().unwrap();
2635 snapshot.buffers.values().cloned().collect()
2636 }
2637
2638 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
2640 let snapshot = self.state_snapshot.read().unwrap();
2641 snapshot.primary_cursor.clone()
2642 }
2643
2644 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
2646 let snapshot = self.state_snapshot.read().unwrap();
2647 snapshot.all_cursors.clone()
2648 }
2649
2650 pub fn get_viewport(&self) -> Option<ViewportInfo> {
2652 let snapshot = self.state_snapshot.read().unwrap();
2653 snapshot.viewport.clone()
2654 }
2655
2656 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
2658 Arc::clone(&self.state_snapshot)
2659 }
2660}
2661
2662impl Clone for PluginApi {
2663 fn clone(&self) -> Self {
2664 Self {
2665 hooks: Arc::clone(&self.hooks),
2666 commands: Arc::clone(&self.commands),
2667 command_sender: self.command_sender.clone(),
2668 state_snapshot: Arc::clone(&self.state_snapshot),
2669 }
2670 }
2671}
2672
2673#[cfg(test)]
2674mod tests {
2675 use super::*;
2676
2677 #[test]
2678 fn test_plugin_api_creation() {
2679 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2680 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2681 let (tx, _rx) = std::sync::mpsc::channel();
2682 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2683
2684 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2685
2686 let _clone = api.clone();
2688 }
2689
2690 #[test]
2691 fn test_register_hook() {
2692 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2693 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2694 let (tx, _rx) = std::sync::mpsc::channel();
2695 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2696
2697 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
2698
2699 api.register_hook("test-hook", Box::new(|_| true));
2700
2701 let hook_registry = hooks.read().unwrap();
2702 assert_eq!(hook_registry.hook_count("test-hook"), 1);
2703 }
2704
2705 #[test]
2706 fn test_send_command() {
2707 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2708 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2709 let (tx, rx) = std::sync::mpsc::channel();
2710 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2711
2712 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2713
2714 let result = api.insert_text(BufferId(1), 0, "test".to_string());
2715 assert!(result.is_ok());
2716
2717 let received = rx.try_recv();
2719 assert!(received.is_ok());
2720
2721 match received.unwrap() {
2722 PluginCommand::InsertText {
2723 buffer_id,
2724 position,
2725 text,
2726 } => {
2727 assert_eq!(buffer_id.0, 1);
2728 assert_eq!(position, 0);
2729 assert_eq!(text, "test");
2730 }
2731 _ => panic!("Wrong command type"),
2732 }
2733 }
2734
2735 #[test]
2736 fn test_add_overlay_command() {
2737 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2738 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2739 let (tx, rx) = std::sync::mpsc::channel();
2740 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2741
2742 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2743
2744 let result = api.add_overlay(
2745 BufferId(1),
2746 Some("test-overlay".to_string()),
2747 0..10,
2748 OverlayOptions {
2749 fg: Some(OverlayColorSpec::ThemeKey("ui.status_bar_fg".to_string())),
2750 bg: None,
2751 underline: true,
2752 bold: false,
2753 italic: false,
2754 strikethrough: false,
2755 extend_to_line_end: false,
2756 url: None,
2757 },
2758 );
2759 assert!(result.is_ok());
2760
2761 let received = rx.try_recv().unwrap();
2762 match received {
2763 PluginCommand::AddOverlay {
2764 buffer_id,
2765 namespace,
2766 range,
2767 options,
2768 } => {
2769 assert_eq!(buffer_id.0, 1);
2770 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
2771 assert_eq!(range, 0..10);
2772 assert!(matches!(
2773 options.fg,
2774 Some(OverlayColorSpec::ThemeKey(ref k)) if k == "ui.status_bar_fg"
2775 ));
2776 assert!(options.bg.is_none());
2777 assert!(options.underline);
2778 assert!(!options.bold);
2779 assert!(!options.italic);
2780 assert!(!options.extend_to_line_end);
2781 }
2782 _ => panic!("Wrong command type"),
2783 }
2784 }
2785
2786 #[test]
2787 fn test_set_status_command() {
2788 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2789 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2790 let (tx, rx) = std::sync::mpsc::channel();
2791 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2792
2793 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2794
2795 let result = api.set_status("Test status".to_string());
2796 assert!(result.is_ok());
2797
2798 let received = rx.try_recv().unwrap();
2799 match received {
2800 PluginCommand::SetStatus { message } => {
2801 assert_eq!(message, "Test status");
2802 }
2803 _ => panic!("Wrong command type"),
2804 }
2805 }
2806
2807 #[test]
2808 fn test_get_active_buffer_id() {
2809 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2810 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2811 let (tx, _rx) = std::sync::mpsc::channel();
2812 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2813
2814 {
2816 let mut snapshot = state_snapshot.write().unwrap();
2817 snapshot.active_buffer_id = BufferId(5);
2818 }
2819
2820 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2821
2822 let active_id = api.get_active_buffer_id();
2823 assert_eq!(active_id.0, 5);
2824 }
2825
2826 #[test]
2827 fn test_get_buffer_info() {
2828 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2829 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2830 let (tx, _rx) = std::sync::mpsc::channel();
2831 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2832
2833 {
2835 let mut snapshot = state_snapshot.write().unwrap();
2836 let buffer_info = BufferInfo {
2837 id: BufferId(1),
2838 path: Some(std::path::PathBuf::from("/test/file.txt")),
2839 modified: true,
2840 length: 100,
2841 is_virtual: false,
2842 view_mode: "source".to_string(),
2843 is_composing_in_any_split: false,
2844 compose_width: None,
2845 language: "text".to_string(),
2846 };
2847 snapshot.buffers.insert(BufferId(1), buffer_info);
2848 }
2849
2850 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2851
2852 let info = api.get_buffer_info(BufferId(1));
2853 assert!(info.is_some());
2854 let info = info.unwrap();
2855 assert_eq!(info.id.0, 1);
2856 assert_eq!(
2857 info.path.as_ref().unwrap().to_str().unwrap(),
2858 "/test/file.txt"
2859 );
2860 assert!(info.modified);
2861 assert_eq!(info.length, 100);
2862
2863 let no_info = api.get_buffer_info(BufferId(999));
2865 assert!(no_info.is_none());
2866 }
2867
2868 #[test]
2869 fn test_list_buffers() {
2870 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2871 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2872 let (tx, _rx) = std::sync::mpsc::channel();
2873 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2874
2875 {
2877 let mut snapshot = state_snapshot.write().unwrap();
2878 snapshot.buffers.insert(
2879 BufferId(1),
2880 BufferInfo {
2881 id: BufferId(1),
2882 path: Some(std::path::PathBuf::from("/file1.txt")),
2883 modified: false,
2884 length: 50,
2885 is_virtual: false,
2886 view_mode: "source".to_string(),
2887 is_composing_in_any_split: false,
2888 compose_width: None,
2889 language: "text".to_string(),
2890 },
2891 );
2892 snapshot.buffers.insert(
2893 BufferId(2),
2894 BufferInfo {
2895 id: BufferId(2),
2896 path: Some(std::path::PathBuf::from("/file2.txt")),
2897 modified: true,
2898 length: 100,
2899 is_virtual: false,
2900 view_mode: "source".to_string(),
2901 is_composing_in_any_split: false,
2902 compose_width: None,
2903 language: "text".to_string(),
2904 },
2905 );
2906 snapshot.buffers.insert(
2907 BufferId(3),
2908 BufferInfo {
2909 id: BufferId(3),
2910 path: None,
2911 modified: false,
2912 length: 0,
2913 is_virtual: true,
2914 view_mode: "source".to_string(),
2915 is_composing_in_any_split: false,
2916 compose_width: None,
2917 language: "text".to_string(),
2918 },
2919 );
2920 }
2921
2922 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2923
2924 let buffers = api.list_buffers();
2925 assert_eq!(buffers.len(), 3);
2926
2927 assert!(buffers.iter().any(|b| b.id.0 == 1));
2929 assert!(buffers.iter().any(|b| b.id.0 == 2));
2930 assert!(buffers.iter().any(|b| b.id.0 == 3));
2931 }
2932
2933 #[test]
2934 fn test_get_primary_cursor() {
2935 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2936 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2937 let (tx, _rx) = std::sync::mpsc::channel();
2938 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2939
2940 {
2942 let mut snapshot = state_snapshot.write().unwrap();
2943 snapshot.primary_cursor = Some(CursorInfo {
2944 position: 42,
2945 selection: Some(10..42),
2946 });
2947 }
2948
2949 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2950
2951 let cursor = api.get_primary_cursor();
2952 assert!(cursor.is_some());
2953 let cursor = cursor.unwrap();
2954 assert_eq!(cursor.position, 42);
2955 assert_eq!(cursor.selection, Some(10..42));
2956 }
2957
2958 #[test]
2959 fn test_get_all_cursors() {
2960 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2961 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2962 let (tx, _rx) = std::sync::mpsc::channel();
2963 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2964
2965 {
2967 let mut snapshot = state_snapshot.write().unwrap();
2968 snapshot.all_cursors = vec![
2969 CursorInfo {
2970 position: 10,
2971 selection: None,
2972 },
2973 CursorInfo {
2974 position: 20,
2975 selection: Some(15..20),
2976 },
2977 CursorInfo {
2978 position: 30,
2979 selection: Some(25..30),
2980 },
2981 ];
2982 }
2983
2984 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2985
2986 let cursors = api.get_all_cursors();
2987 assert_eq!(cursors.len(), 3);
2988 assert_eq!(cursors[0].position, 10);
2989 assert_eq!(cursors[0].selection, None);
2990 assert_eq!(cursors[1].position, 20);
2991 assert_eq!(cursors[1].selection, Some(15..20));
2992 assert_eq!(cursors[2].position, 30);
2993 assert_eq!(cursors[2].selection, Some(25..30));
2994 }
2995
2996 #[test]
2997 fn test_get_viewport() {
2998 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2999 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3000 let (tx, _rx) = std::sync::mpsc::channel();
3001 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3002
3003 {
3005 let mut snapshot = state_snapshot.write().unwrap();
3006 snapshot.viewport = Some(ViewportInfo {
3007 top_byte: 100,
3008 top_line: Some(5),
3009 left_column: 5,
3010 width: 80,
3011 height: 24,
3012 });
3013 }
3014
3015 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3016
3017 let viewport = api.get_viewport();
3018 assert!(viewport.is_some());
3019 let viewport = viewport.unwrap();
3020 assert_eq!(viewport.top_byte, 100);
3021 assert_eq!(viewport.left_column, 5);
3022 assert_eq!(viewport.width, 80);
3023 assert_eq!(viewport.height, 24);
3024 }
3025
3026 #[test]
3027 fn test_composite_buffer_options_rejects_unknown_fields() {
3028 let valid_json = r#"{
3030 "name": "test",
3031 "mode": "diff",
3032 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3033 "sources": [{"bufferId": 1, "label": "old"}]
3034 }"#;
3035 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
3036 assert!(
3037 result.is_ok(),
3038 "Valid JSON should parse: {:?}",
3039 result.err()
3040 );
3041
3042 let invalid_json = r#"{
3044 "name": "test",
3045 "mode": "diff",
3046 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3047 "sources": [{"buffer_id": 1, "label": "old"}]
3048 }"#;
3049 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
3050 assert!(
3051 result.is_err(),
3052 "JSON with unknown field should fail to parse"
3053 );
3054 let err = result.unwrap_err().to_string();
3055 assert!(
3056 err.contains("unknown field") || err.contains("buffer_id"),
3057 "Error should mention unknown field: {}",
3058 err
3059 );
3060 }
3061
3062 #[test]
3063 fn test_composite_hunk_rejects_unknown_fields() {
3064 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3066 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
3067 assert!(
3068 result.is_ok(),
3069 "Valid JSON should parse: {:?}",
3070 result.err()
3071 );
3072
3073 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3075 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
3076 assert!(
3077 result.is_err(),
3078 "JSON with unknown field should fail to parse"
3079 );
3080 let err = result.unwrap_err().to_string();
3081 assert!(
3082 err.contains("unknown field") || err.contains("old_start"),
3083 "Error should mention unknown field: {}",
3084 err
3085 );
3086 }
3087
3088 #[test]
3089 fn test_plugin_response_line_end_position() {
3090 let response = PluginResponse::LineEndPosition {
3091 request_id: 42,
3092 position: Some(100),
3093 };
3094 let json = serde_json::to_string(&response).unwrap();
3095 assert!(json.contains("LineEndPosition"));
3096 assert!(json.contains("42"));
3097 assert!(json.contains("100"));
3098
3099 let response_none = PluginResponse::LineEndPosition {
3101 request_id: 1,
3102 position: None,
3103 };
3104 let json_none = serde_json::to_string(&response_none).unwrap();
3105 assert!(json_none.contains("null"));
3106 }
3107
3108 #[test]
3109 fn test_plugin_response_buffer_line_count() {
3110 let response = PluginResponse::BufferLineCount {
3111 request_id: 99,
3112 count: Some(500),
3113 };
3114 let json = serde_json::to_string(&response).unwrap();
3115 assert!(json.contains("BufferLineCount"));
3116 assert!(json.contains("99"));
3117 assert!(json.contains("500"));
3118 }
3119
3120 #[test]
3121 fn test_plugin_command_get_line_end_position() {
3122 let command = PluginCommand::GetLineEndPosition {
3123 buffer_id: BufferId(1),
3124 line: 10,
3125 request_id: 123,
3126 };
3127 let json = serde_json::to_string(&command).unwrap();
3128 assert!(json.contains("GetLineEndPosition"));
3129 assert!(json.contains("10"));
3130 }
3131
3132 #[test]
3133 fn test_plugin_command_get_buffer_line_count() {
3134 let command = PluginCommand::GetBufferLineCount {
3135 buffer_id: BufferId(0),
3136 request_id: 456,
3137 };
3138 let json = serde_json::to_string(&command).unwrap();
3139 assert!(json.contains("GetBufferLineCount"));
3140 assert!(json.contains("456"));
3141 }
3142
3143 #[test]
3144 fn test_plugin_command_scroll_to_line_center() {
3145 let command = PluginCommand::ScrollToLineCenter {
3146 split_id: SplitId(1),
3147 buffer_id: BufferId(2),
3148 line: 50,
3149 };
3150 let json = serde_json::to_string(&command).unwrap();
3151 assert!(json.contains("ScrollToLineCenter"));
3152 assert!(json.contains("50"));
3153 }
3154}