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 lsp_types;
55use serde::{Deserialize, Serialize};
56use serde_json::Value as JsonValue;
57use std::collections::HashMap;
58use std::ops::Range;
59use std::path::PathBuf;
60use std::sync::{Arc, RwLock};
61use ts_rs::TS;
62
63pub struct CommandRegistry {
67 commands: std::sync::RwLock<Vec<Command>>,
68}
69
70impl CommandRegistry {
71 pub fn new() -> Self {
73 Self {
74 commands: std::sync::RwLock::new(Vec::new()),
75 }
76 }
77
78 pub fn register(&self, command: Command) {
80 let mut commands = self.commands.write().unwrap();
81 commands.retain(|c| c.name != command.name);
82 commands.push(command);
83 }
84
85 pub fn unregister(&self, name: &str) {
87 let mut commands = self.commands.write().unwrap();
88 commands.retain(|c| c.name != name);
89 }
90}
91
92impl Default for CommandRegistry {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
104#[ts(export)]
105pub struct JsCallbackId(pub u64);
106
107impl JsCallbackId {
108 pub fn new(id: u64) -> Self {
110 Self(id)
111 }
112
113 pub fn as_u64(self) -> u64 {
115 self.0
116 }
117}
118
119impl From<u64> for JsCallbackId {
120 fn from(id: u64) -> Self {
121 Self(id)
122 }
123}
124
125impl From<JsCallbackId> for u64 {
126 fn from(id: JsCallbackId) -> u64 {
127 id.0
128 }
129}
130
131impl std::fmt::Display for JsCallbackId {
132 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 write!(f, "{}", self.0)
134 }
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, TS)]
139#[serde(rename_all = "camelCase")]
140#[ts(export, rename_all = "camelCase")]
141pub struct VirtualBufferResult {
142 #[ts(type = "number")]
144 pub buffer_id: u64,
145 #[ts(type = "number | null")]
147 pub split_id: Option<u64>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize, TS)]
152#[ts(export)]
153pub enum PluginResponse {
154 VirtualBufferCreated {
156 request_id: u64,
157 buffer_id: BufferId,
158 split_id: Option<SplitId>,
159 },
160 LspRequest {
162 request_id: u64,
163 #[ts(type = "any")]
164 result: Result<JsonValue, String>,
165 },
166 HighlightsComputed {
168 request_id: u64,
169 spans: Vec<TsHighlightSpan>,
170 },
171 BufferText {
173 request_id: u64,
174 text: Result<String, String>,
175 },
176 LineStartPosition {
178 request_id: u64,
179 position: Option<usize>,
181 },
182 CompositeBufferCreated {
184 request_id: u64,
185 buffer_id: BufferId,
186 },
187}
188
189#[derive(Debug, Clone, Serialize, Deserialize, TS)]
191#[ts(export)]
192pub enum PluginAsyncMessage {
193 ProcessOutput {
195 process_id: u64,
197 stdout: String,
199 stderr: String,
201 exit_code: i32,
203 },
204 DelayComplete {
206 callback_id: u64,
208 },
209 ProcessStdout { process_id: u64, data: String },
211 ProcessStderr { process_id: u64, data: String },
213 ProcessExit {
215 process_id: u64,
216 callback_id: u64,
217 exit_code: i32,
218 },
219 LspResponse {
221 language: String,
222 request_id: u64,
223 #[ts(type = "any")]
224 result: Result<JsonValue, String>,
225 },
226 PluginResponse(crate::api::PluginResponse),
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize, TS)]
232#[ts(export)]
233pub struct CursorInfo {
234 pub position: usize,
236 #[cfg_attr(
238 feature = "plugins",
239 ts(type = "{ start: number; end: number } | null")
240 )]
241 pub selection: Option<Range<usize>>,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize, TS)]
246#[serde(deny_unknown_fields)]
247#[ts(export)]
248pub struct ActionSpec {
249 pub action: String,
251 #[serde(default = "default_action_count")]
253 pub count: u32,
254}
255
256fn default_action_count() -> u32 {
257 1
258}
259
260#[derive(Debug, Clone, Serialize, Deserialize, TS)]
262#[ts(export)]
263pub struct BufferInfo {
264 #[ts(type = "number")]
266 pub id: BufferId,
267 #[serde(serialize_with = "serialize_path")]
269 #[ts(type = "string")]
270 pub path: Option<PathBuf>,
271 pub modified: bool,
273 pub length: usize,
275}
276
277fn serialize_path<S: serde::Serializer>(path: &Option<PathBuf>, s: S) -> Result<S::Ok, S::Error> {
278 s.serialize_str(
279 &path
280 .as_ref()
281 .map(|p| p.to_string_lossy().to_string())
282 .unwrap_or_default(),
283 )
284}
285
286fn serialize_ranges_as_tuples<S>(ranges: &[Range<usize>], serializer: S) -> Result<S::Ok, S::Error>
288where
289 S: serde::Serializer,
290{
291 use serde::ser::SerializeSeq;
292 let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
293 for range in ranges {
294 seq.serialize_element(&(range.start, range.end))?;
295 }
296 seq.end()
297}
298
299fn serialize_opt_ranges_as_tuples<S>(
301 ranges: &Option<Vec<Range<usize>>>,
302 serializer: S,
303) -> Result<S::Ok, S::Error>
304where
305 S: serde::Serializer,
306{
307 match ranges {
308 Some(ranges) => {
309 use serde::ser::SerializeSeq;
310 let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
311 for range in ranges {
312 seq.serialize_element(&(range.start, range.end))?;
313 }
314 seq.end()
315 }
316 None => serializer.serialize_none(),
317 }
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize, TS)]
322#[ts(export)]
323pub struct BufferSavedDiff {
324 pub equal: bool,
325 #[serde(serialize_with = "serialize_ranges_as_tuples")]
326 #[ts(type = "Array<[number, number]>")]
327 pub byte_ranges: Vec<Range<usize>>,
328 #[serde(serialize_with = "serialize_opt_ranges_as_tuples")]
329 #[ts(type = "Array<[number, number]> | null")]
330 pub line_ranges: Option<Vec<Range<usize>>>,
331}
332
333#[derive(Debug, Clone, Serialize, Deserialize, TS)]
335#[serde(rename_all = "camelCase")]
336#[ts(export, rename_all = "camelCase")]
337pub struct ViewportInfo {
338 pub top_byte: usize,
340 pub left_column: usize,
342 pub width: u16,
344 pub height: u16,
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize, TS)]
350#[serde(rename_all = "camelCase")]
351#[ts(export, rename_all = "camelCase")]
352pub struct LayoutHints {
353 pub compose_width: Option<u16>,
355 pub column_guides: Option<Vec<u16>>,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize, TS)]
365#[serde(deny_unknown_fields)]
366#[ts(export, rename = "TsCompositeLayoutConfig")]
367pub struct CompositeLayoutConfig {
368 #[serde(rename = "type")]
370 #[ts(rename = "type")]
371 pub layout_type: String,
372 #[serde(default)]
374 pub ratios: Option<Vec<f32>>,
375 #[serde(default = "default_true", rename = "showSeparator")]
377 #[ts(rename = "showSeparator")]
378 pub show_separator: bool,
379 #[serde(default)]
381 pub spacing: Option<u16>,
382}
383
384fn default_true() -> bool {
385 true
386}
387
388#[derive(Debug, Clone, Serialize, Deserialize, TS)]
390#[serde(deny_unknown_fields)]
391#[ts(export, rename = "TsCompositeSourceConfig")]
392pub struct CompositeSourceConfig {
393 #[serde(rename = "bufferId")]
395 #[ts(rename = "bufferId")]
396 pub buffer_id: usize,
397 pub label: String,
399 #[serde(default)]
401 pub editable: bool,
402 #[serde(default)]
404 pub style: Option<CompositePaneStyle>,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
409#[serde(deny_unknown_fields)]
410#[ts(export, rename = "TsCompositePaneStyle")]
411pub struct CompositePaneStyle {
412 #[serde(default, rename = "addBg")]
415 #[ts(rename = "addBg", type = "[number, number, number] | null")]
416 pub add_bg: Option<[u8; 3]>,
417 #[serde(default, rename = "removeBg")]
419 #[ts(rename = "removeBg", type = "[number, number, number] | null")]
420 pub remove_bg: Option<[u8; 3]>,
421 #[serde(default, rename = "modifyBg")]
423 #[ts(rename = "modifyBg", type = "[number, number, number] | null")]
424 pub modify_bg: Option<[u8; 3]>,
425 #[serde(default, rename = "gutterStyle")]
427 #[ts(rename = "gutterStyle")]
428 pub gutter_style: Option<String>,
429}
430
431#[derive(Debug, Clone, Serialize, Deserialize, TS)]
433#[serde(deny_unknown_fields)]
434#[ts(export, rename = "TsCompositeHunk")]
435pub struct CompositeHunk {
436 #[serde(rename = "oldStart")]
438 #[ts(rename = "oldStart")]
439 pub old_start: usize,
440 #[serde(rename = "oldCount")]
442 #[ts(rename = "oldCount")]
443 pub old_count: usize,
444 #[serde(rename = "newStart")]
446 #[ts(rename = "newStart")]
447 pub new_start: usize,
448 #[serde(rename = "newCount")]
450 #[ts(rename = "newCount")]
451 pub new_count: usize,
452}
453
454#[derive(Debug, Clone, Serialize, Deserialize, TS)]
456#[serde(deny_unknown_fields)]
457#[ts(export, rename = "TsCreateCompositeBufferOptions")]
458pub struct CreateCompositeBufferOptions {
459 #[serde(default)]
461 pub name: String,
462 #[serde(default)]
464 pub mode: String,
465 pub layout: CompositeLayoutConfig,
467 pub sources: Vec<CompositeSourceConfig>,
469 #[serde(default)]
471 pub hunks: Option<Vec<CompositeHunk>>,
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize, TS)]
476#[ts(export)]
477pub enum ViewTokenWireKind {
478 Text(String),
479 Newline,
480 Space,
481 Break,
484 BinaryByte(u8),
488}
489
490#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
496#[serde(deny_unknown_fields)]
497#[ts(export)]
498pub struct ViewTokenStyle {
499 #[serde(default)]
501 #[ts(type = "[number, number, number] | null")]
502 pub fg: Option<(u8, u8, u8)>,
503 #[serde(default)]
505 #[ts(type = "[number, number, number] | null")]
506 pub bg: Option<(u8, u8, u8)>,
507 #[serde(default)]
509 pub bold: bool,
510 #[serde(default)]
512 pub italic: bool,
513}
514
515#[derive(Debug, Clone, Serialize, Deserialize, TS)]
517#[serde(deny_unknown_fields)]
518#[ts(export)]
519pub struct ViewTokenWire {
520 #[ts(type = "number | null")]
522 pub source_offset: Option<usize>,
523 pub kind: ViewTokenWireKind,
525 #[serde(default)]
527 #[ts(optional)]
528 pub style: Option<ViewTokenStyle>,
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize, TS)]
533#[ts(export)]
534pub struct ViewTransformPayload {
535 pub range: Range<usize>,
537 pub tokens: Vec<ViewTokenWire>,
539 pub layout_hints: Option<LayoutHints>,
541}
542
543#[derive(Debug, Clone, Serialize, Deserialize, TS)]
546#[ts(export)]
547pub struct EditorStateSnapshot {
548 pub active_buffer_id: BufferId,
550 pub active_split_id: usize,
552 pub buffers: HashMap<BufferId, BufferInfo>,
554 pub buffer_saved_diffs: HashMap<BufferId, BufferSavedDiff>,
556 pub primary_cursor: Option<CursorInfo>,
558 pub all_cursors: Vec<CursorInfo>,
560 pub viewport: Option<ViewportInfo>,
562 pub buffer_cursor_positions: HashMap<BufferId, usize>,
564 pub buffer_text_properties: HashMap<BufferId, Vec<TextProperty>>,
566 pub selected_text: Option<String>,
569 pub clipboard: String,
571 pub working_dir: PathBuf,
573 #[ts(type = "any")]
576 pub diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
577 #[ts(type = "any")]
580 pub config: serde_json::Value,
581 #[ts(type = "any")]
584 pub user_config: serde_json::Value,
585 pub editor_mode: Option<String>,
588}
589
590impl EditorStateSnapshot {
591 pub fn new() -> Self {
592 Self {
593 active_buffer_id: BufferId(0),
594 active_split_id: 0,
595 buffers: HashMap::new(),
596 buffer_saved_diffs: HashMap::new(),
597 primary_cursor: None,
598 all_cursors: Vec::new(),
599 viewport: None,
600 buffer_cursor_positions: HashMap::new(),
601 buffer_text_properties: HashMap::new(),
602 selected_text: None,
603 clipboard: String::new(),
604 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
605 diagnostics: HashMap::new(),
606 config: serde_json::Value::Null,
607 user_config: serde_json::Value::Null,
608 editor_mode: None,
609 }
610 }
611}
612
613impl Default for EditorStateSnapshot {
614 fn default() -> Self {
615 Self::new()
616 }
617}
618
619#[derive(Debug, Clone, Serialize, Deserialize, TS)]
621#[ts(export)]
622pub enum MenuPosition {
623 Top,
625 Bottom,
627 Before(String),
629 After(String),
631}
632
633#[derive(Debug, Clone, Serialize, Deserialize, TS)]
635#[ts(export)]
636pub enum PluginCommand {
637 InsertText {
639 buffer_id: BufferId,
640 position: usize,
641 text: String,
642 },
643
644 DeleteRange {
646 buffer_id: BufferId,
647 range: Range<usize>,
648 },
649
650 AddOverlay {
652 buffer_id: BufferId,
653 namespace: Option<OverlayNamespace>,
654 range: Range<usize>,
655 color: (u8, u8, u8),
656 bg_color: Option<(u8, u8, u8)>,
657 underline: bool,
658 bold: bool,
659 italic: bool,
660 extend_to_line_end: bool,
661 },
662
663 RemoveOverlay {
665 buffer_id: BufferId,
666 handle: OverlayHandle,
667 },
668
669 SetStatus { message: String },
671
672 ApplyTheme { theme_name: String },
674
675 ReloadConfig,
678
679 RegisterCommand { command: Command },
681
682 UnregisterCommand { name: String },
684
685 OpenFileInBackground { path: PathBuf },
687
688 InsertAtCursor { text: String },
690
691 SpawnProcess {
693 command: String,
694 args: Vec<String>,
695 cwd: Option<String>,
696 callback_id: JsCallbackId,
697 },
698
699 Delay {
701 callback_id: JsCallbackId,
702 duration_ms: u64,
703 },
704
705 SpawnBackgroundProcess {
709 process_id: u64,
711 command: String,
713 args: Vec<String>,
715 cwd: Option<String>,
717 callback_id: JsCallbackId,
719 },
720
721 KillBackgroundProcess { process_id: u64 },
723
724 SpawnProcessWait {
727 process_id: u64,
729 callback_id: JsCallbackId,
731 },
732
733 SetLayoutHints {
735 buffer_id: BufferId,
736 split_id: Option<SplitId>,
737 range: Range<usize>,
738 hints: LayoutHints,
739 },
740
741 SetLineNumbers { buffer_id: BufferId, enabled: bool },
743
744 SubmitViewTransform {
746 buffer_id: BufferId,
747 split_id: Option<SplitId>,
748 payload: ViewTransformPayload,
749 },
750
751 ClearViewTransform {
753 buffer_id: BufferId,
754 split_id: Option<SplitId>,
755 },
756
757 ClearAllOverlays { buffer_id: BufferId },
759
760 ClearNamespace {
762 buffer_id: BufferId,
763 namespace: OverlayNamespace,
764 },
765
766 ClearOverlaysInRange {
769 buffer_id: BufferId,
770 start: usize,
771 end: usize,
772 },
773
774 AddVirtualText {
777 buffer_id: BufferId,
778 virtual_text_id: String,
779 position: usize,
780 text: String,
781 color: (u8, u8, u8),
782 use_bg: bool, before: bool, },
785
786 RemoveVirtualText {
788 buffer_id: BufferId,
789 virtual_text_id: String,
790 },
791
792 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
794
795 ClearVirtualTexts { buffer_id: BufferId },
797
798 AddVirtualLine {
802 buffer_id: BufferId,
803 position: usize,
805 text: String,
807 fg_color: (u8, u8, u8),
809 bg_color: Option<(u8, u8, u8)>,
811 above: bool,
813 namespace: String,
815 priority: i32,
817 },
818
819 ClearVirtualTextNamespace {
822 buffer_id: BufferId,
823 namespace: String,
824 },
825
826 RefreshLines { buffer_id: BufferId },
828
829 SetLineIndicator {
832 buffer_id: BufferId,
833 line: usize,
835 namespace: String,
837 symbol: String,
839 color: (u8, u8, u8),
841 priority: i32,
843 },
844
845 ClearLineIndicators {
847 buffer_id: BufferId,
848 namespace: String,
850 },
851
852 SetFileExplorerDecorations {
854 namespace: String,
856 decorations: Vec<FileExplorerDecoration>,
858 },
859
860 ClearFileExplorerDecorations {
862 namespace: String,
864 },
865
866 OpenFileAtLocation {
869 path: PathBuf,
870 line: Option<usize>, column: Option<usize>, },
873
874 OpenFileInSplit {
877 split_id: usize,
878 path: PathBuf,
879 line: Option<usize>, column: Option<usize>, },
882
883 StartPrompt {
886 label: String,
887 prompt_type: String, },
889
890 StartPromptWithInitial {
892 label: String,
893 prompt_type: String,
894 initial_value: String,
895 },
896
897 StartPromptAsync {
900 label: String,
901 initial_value: String,
902 callback_id: JsCallbackId,
903 },
904
905 SetPromptSuggestions { suggestions: Vec<Suggestion> },
908
909 AddMenuItem {
912 menu_label: String,
913 item: MenuItem,
914 position: MenuPosition,
915 },
916
917 AddMenu { menu: Menu, position: MenuPosition },
919
920 RemoveMenuItem {
922 menu_label: String,
923 item_label: String,
924 },
925
926 RemoveMenu { menu_label: String },
928
929 CreateVirtualBuffer {
931 name: String,
933 mode: String,
935 read_only: bool,
937 },
938
939 CreateVirtualBufferWithContent {
943 name: String,
945 mode: String,
947 read_only: bool,
949 entries: Vec<TextPropertyEntry>,
951 show_line_numbers: bool,
953 show_cursors: bool,
955 editing_disabled: bool,
957 hidden_from_tabs: bool,
959 request_id: Option<u64>,
961 },
962
963 CreateVirtualBufferInSplit {
966 name: String,
968 mode: String,
970 read_only: bool,
972 entries: Vec<TextPropertyEntry>,
974 ratio: f32,
976 direction: Option<String>,
978 panel_id: Option<String>,
980 show_line_numbers: bool,
982 show_cursors: bool,
984 editing_disabled: bool,
986 line_wrap: Option<bool>,
988 request_id: Option<u64>,
990 },
991
992 SetVirtualBufferContent {
994 buffer_id: BufferId,
995 entries: Vec<TextPropertyEntry>,
997 },
998
999 GetTextPropertiesAtCursor { buffer_id: BufferId },
1001
1002 DefineMode {
1004 name: String,
1005 parent: Option<String>,
1006 bindings: Vec<(String, String)>, read_only: bool,
1008 },
1009
1010 ShowBuffer { buffer_id: BufferId },
1012
1013 CreateVirtualBufferInExistingSplit {
1015 name: String,
1017 mode: String,
1019 read_only: bool,
1021 entries: Vec<TextPropertyEntry>,
1023 split_id: SplitId,
1025 show_line_numbers: bool,
1027 show_cursors: bool,
1029 editing_disabled: bool,
1031 line_wrap: Option<bool>,
1033 request_id: Option<u64>,
1035 },
1036
1037 CloseBuffer { buffer_id: BufferId },
1039
1040 CreateCompositeBuffer {
1043 name: String,
1045 mode: String,
1047 layout: CompositeLayoutConfig,
1049 sources: Vec<CompositeSourceConfig>,
1051 hunks: Option<Vec<CompositeHunk>>,
1053 request_id: Option<u64>,
1055 },
1056
1057 UpdateCompositeAlignment {
1059 buffer_id: BufferId,
1060 hunks: Vec<CompositeHunk>,
1061 },
1062
1063 CloseCompositeBuffer { buffer_id: BufferId },
1065
1066 FocusSplit { split_id: SplitId },
1068
1069 SetSplitBuffer {
1071 split_id: SplitId,
1072 buffer_id: BufferId,
1073 },
1074
1075 SetSplitScroll { split_id: SplitId, top_byte: usize },
1077
1078 RequestHighlights {
1080 buffer_id: BufferId,
1081 range: Range<usize>,
1082 request_id: u64,
1083 },
1084
1085 CloseSplit { split_id: SplitId },
1087
1088 SetSplitRatio {
1090 split_id: SplitId,
1091 ratio: f32,
1093 },
1094
1095 DistributeSplitsEvenly {
1097 split_ids: Vec<SplitId>,
1099 },
1100
1101 SetBufferCursor {
1103 buffer_id: BufferId,
1104 position: usize,
1106 },
1107
1108 SendLspRequest {
1110 language: String,
1111 method: String,
1112 #[ts(type = "any")]
1113 params: Option<JsonValue>,
1114 request_id: u64,
1115 },
1116
1117 SetClipboard { text: String },
1119
1120 DeleteSelection,
1123
1124 SetContext {
1128 name: String,
1130 active: bool,
1132 },
1133
1134 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1136
1137 ExecuteAction {
1140 action_name: String,
1142 },
1143
1144 ExecuteActions {
1148 actions: Vec<ActionSpec>,
1150 },
1151
1152 GetBufferText {
1154 buffer_id: BufferId,
1156 start: usize,
1158 end: usize,
1160 request_id: u64,
1162 },
1163
1164 GetLineStartPosition {
1167 buffer_id: BufferId,
1169 line: u32,
1171 request_id: u64,
1173 },
1174
1175 SetEditorMode {
1178 mode: Option<String>,
1180 },
1181
1182 ShowActionPopup {
1185 popup_id: String,
1187 title: String,
1189 message: String,
1191 actions: Vec<ActionPopupAction>,
1193 },
1194
1195 DisableLspForLanguage {
1197 language: String,
1199 },
1200
1201 CreateScrollSyncGroup {
1205 group_id: u32,
1207 left_split: SplitId,
1209 right_split: SplitId,
1211 },
1212
1213 SetScrollSyncAnchors {
1216 group_id: u32,
1218 anchors: Vec<(usize, usize)>,
1220 },
1221
1222 RemoveScrollSyncGroup {
1224 group_id: u32,
1226 },
1227
1228 SaveBufferToPath {
1231 buffer_id: BufferId,
1233 path: PathBuf,
1235 },
1236}
1237
1238#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1240#[ts(export)]
1241pub enum HunkStatus {
1242 Pending,
1243 Staged,
1244 Discarded,
1245}
1246
1247#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1249#[ts(export)]
1250pub struct ReviewHunk {
1251 pub id: String,
1252 pub file: String,
1253 pub context_header: String,
1254 pub status: HunkStatus,
1255 pub base_range: Option<(usize, usize)>,
1257 pub modified_range: Option<(usize, usize)>,
1259}
1260
1261#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1263#[serde(deny_unknown_fields)]
1264#[ts(export, rename = "TsActionPopupAction")]
1265pub struct ActionPopupAction {
1266 pub id: String,
1268 pub label: String,
1270}
1271
1272#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1274#[serde(deny_unknown_fields)]
1275#[ts(export)]
1276pub struct ActionPopupOptions {
1277 pub id: String,
1279 pub title: String,
1281 pub message: String,
1283 pub actions: Vec<ActionPopupAction>,
1285}
1286
1287#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1289#[ts(export)]
1290pub struct TsHighlightSpan {
1291 pub start: u32,
1292 pub end: u32,
1293 #[ts(type = "[number, number, number]")]
1294 pub color: (u8, u8, u8),
1295 pub bold: bool,
1296 pub italic: bool,
1297}
1298
1299#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1301#[ts(export)]
1302pub struct SpawnResult {
1303 pub stdout: String,
1305 pub stderr: String,
1307 pub exit_code: i32,
1309}
1310
1311#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1313#[ts(export)]
1314pub struct BackgroundProcessResult {
1315 #[ts(type = "number")]
1317 pub process_id: u64,
1318 pub exit_code: i32,
1321}
1322
1323#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1325#[serde(deny_unknown_fields)]
1326#[ts(export, rename = "TextPropertyEntry")]
1327pub struct JsTextPropertyEntry {
1328 pub text: String,
1330 #[serde(default)]
1332 #[ts(optional, type = "Record<string, unknown>")]
1333 pub properties: Option<HashMap<String, JsonValue>>,
1334}
1335
1336#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1338#[ts(export)]
1339pub struct DirEntry {
1340 pub name: String,
1342 pub is_file: bool,
1344 pub is_dir: bool,
1346}
1347
1348#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1350#[ts(export)]
1351pub struct JsPosition {
1352 pub line: u32,
1354 pub character: u32,
1356}
1357
1358#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1360#[ts(export)]
1361pub struct JsRange {
1362 pub start: JsPosition,
1364 pub end: JsPosition,
1366}
1367
1368#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1370#[ts(export)]
1371pub struct JsDiagnostic {
1372 pub uri: String,
1374 pub message: String,
1376 pub severity: Option<u8>,
1378 pub range: JsRange,
1380 #[ts(optional)]
1382 pub source: Option<String>,
1383}
1384
1385#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1387#[serde(deny_unknown_fields)]
1388#[ts(export)]
1389pub struct CreateVirtualBufferOptions {
1390 pub name: String,
1392 #[serde(default)]
1394 #[ts(optional)]
1395 pub mode: Option<String>,
1396 #[serde(default, rename = "readOnly")]
1398 #[ts(optional, rename = "readOnly")]
1399 pub read_only: Option<bool>,
1400 #[serde(default, rename = "showLineNumbers")]
1402 #[ts(optional, rename = "showLineNumbers")]
1403 pub show_line_numbers: Option<bool>,
1404 #[serde(default, rename = "showCursors")]
1406 #[ts(optional, rename = "showCursors")]
1407 pub show_cursors: Option<bool>,
1408 #[serde(default, rename = "editingDisabled")]
1410 #[ts(optional, rename = "editingDisabled")]
1411 pub editing_disabled: Option<bool>,
1412 #[serde(default, rename = "hiddenFromTabs")]
1414 #[ts(optional, rename = "hiddenFromTabs")]
1415 pub hidden_from_tabs: Option<bool>,
1416 #[serde(default)]
1418 #[ts(optional)]
1419 pub entries: Option<Vec<JsTextPropertyEntry>>,
1420}
1421
1422#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1424#[serde(deny_unknown_fields)]
1425#[ts(export)]
1426pub struct CreateVirtualBufferInSplitOptions {
1427 pub name: String,
1429 #[serde(default)]
1431 #[ts(optional)]
1432 pub mode: Option<String>,
1433 #[serde(default, rename = "readOnly")]
1435 #[ts(optional, rename = "readOnly")]
1436 pub read_only: Option<bool>,
1437 #[serde(default)]
1439 #[ts(optional)]
1440 pub ratio: Option<f32>,
1441 #[serde(default)]
1443 #[ts(optional)]
1444 pub direction: Option<String>,
1445 #[serde(default, rename = "panelId")]
1447 #[ts(optional, rename = "panelId")]
1448 pub panel_id: Option<String>,
1449 #[serde(default, rename = "showLineNumbers")]
1451 #[ts(optional, rename = "showLineNumbers")]
1452 pub show_line_numbers: Option<bool>,
1453 #[serde(default, rename = "showCursors")]
1455 #[ts(optional, rename = "showCursors")]
1456 pub show_cursors: Option<bool>,
1457 #[serde(default, rename = "editingDisabled")]
1459 #[ts(optional, rename = "editingDisabled")]
1460 pub editing_disabled: Option<bool>,
1461 #[serde(default, rename = "lineWrap")]
1463 #[ts(optional, rename = "lineWrap")]
1464 pub line_wrap: Option<bool>,
1465 #[serde(default)]
1467 #[ts(optional)]
1468 pub entries: Option<Vec<JsTextPropertyEntry>>,
1469}
1470
1471#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1473#[serde(deny_unknown_fields)]
1474#[ts(export)]
1475pub struct CreateVirtualBufferInExistingSplitOptions {
1476 pub name: String,
1478 #[serde(rename = "splitId")]
1480 #[ts(rename = "splitId")]
1481 pub split_id: usize,
1482 #[serde(default)]
1484 #[ts(optional)]
1485 pub mode: Option<String>,
1486 #[serde(default, rename = "readOnly")]
1488 #[ts(optional, rename = "readOnly")]
1489 pub read_only: Option<bool>,
1490 #[serde(default, rename = "showLineNumbers")]
1492 #[ts(optional, rename = "showLineNumbers")]
1493 pub show_line_numbers: Option<bool>,
1494 #[serde(default, rename = "showCursors")]
1496 #[ts(optional, rename = "showCursors")]
1497 pub show_cursors: Option<bool>,
1498 #[serde(default, rename = "editingDisabled")]
1500 #[ts(optional, rename = "editingDisabled")]
1501 pub editing_disabled: Option<bool>,
1502 #[serde(default, rename = "lineWrap")]
1504 #[ts(optional, rename = "lineWrap")]
1505 pub line_wrap: Option<bool>,
1506 #[serde(default)]
1508 #[ts(optional)]
1509 pub entries: Option<Vec<JsTextPropertyEntry>>,
1510}
1511
1512#[derive(Debug, Clone, Serialize, TS)]
1517#[ts(export, type = "Array<Record<string, unknown>>")]
1518pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
1519
1520#[cfg(feature = "plugins")]
1522mod fromjs_impls {
1523 use super::*;
1524 use rquickjs::{Ctx, FromJs, Value};
1525
1526 impl<'js> FromJs<'js> for JsTextPropertyEntry {
1527 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1528 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1529 from: "object",
1530 to: "JsTextPropertyEntry",
1531 message: Some(e.to_string()),
1532 })
1533 }
1534 }
1535
1536 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
1537 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1538 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1539 from: "object",
1540 to: "CreateVirtualBufferOptions",
1541 message: Some(e.to_string()),
1542 })
1543 }
1544 }
1545
1546 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
1547 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1548 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1549 from: "object",
1550 to: "CreateVirtualBufferInSplitOptions",
1551 message: Some(e.to_string()),
1552 })
1553 }
1554 }
1555
1556 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
1557 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1558 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1559 from: "object",
1560 to: "CreateVirtualBufferInExistingSplitOptions",
1561 message: Some(e.to_string()),
1562 })
1563 }
1564 }
1565
1566 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
1567 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
1568 rquickjs_serde::to_value(ctx.clone(), &self.0)
1569 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
1570 }
1571 }
1572
1573 impl<'js> FromJs<'js> for ActionSpec {
1576 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1577 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1578 from: "object",
1579 to: "ActionSpec",
1580 message: Some(e.to_string()),
1581 })
1582 }
1583 }
1584
1585 impl<'js> FromJs<'js> for ActionPopupAction {
1586 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1587 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1588 from: "object",
1589 to: "ActionPopupAction",
1590 message: Some(e.to_string()),
1591 })
1592 }
1593 }
1594
1595 impl<'js> FromJs<'js> for ActionPopupOptions {
1596 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1597 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1598 from: "object",
1599 to: "ActionPopupOptions",
1600 message: Some(e.to_string()),
1601 })
1602 }
1603 }
1604
1605 impl<'js> FromJs<'js> for ViewTokenWire {
1606 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1607 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1608 from: "object",
1609 to: "ViewTokenWire",
1610 message: Some(e.to_string()),
1611 })
1612 }
1613 }
1614
1615 impl<'js> FromJs<'js> for ViewTokenStyle {
1616 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1617 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1618 from: "object",
1619 to: "ViewTokenStyle",
1620 message: Some(e.to_string()),
1621 })
1622 }
1623 }
1624
1625 impl<'js> FromJs<'js> for LayoutHints {
1626 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1627 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1628 from: "object",
1629 to: "LayoutHints",
1630 message: Some(e.to_string()),
1631 })
1632 }
1633 }
1634
1635 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
1636 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1637 let json: serde_json::Value =
1639 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1640 from: "object",
1641 to: "CreateCompositeBufferOptions (json)",
1642 message: Some(e.to_string()),
1643 })?;
1644 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
1645 from: "json",
1646 to: "CreateCompositeBufferOptions",
1647 message: Some(e.to_string()),
1648 })
1649 }
1650 }
1651
1652 impl<'js> FromJs<'js> for CompositeHunk {
1653 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1654 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1655 from: "object",
1656 to: "CompositeHunk",
1657 message: Some(e.to_string()),
1658 })
1659 }
1660 }
1661}
1662
1663pub struct PluginApi {
1665 hooks: Arc<RwLock<HookRegistry>>,
1667
1668 commands: Arc<RwLock<CommandRegistry>>,
1670
1671 command_sender: std::sync::mpsc::Sender<PluginCommand>,
1673
1674 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
1676}
1677
1678impl PluginApi {
1679 pub fn new(
1681 hooks: Arc<RwLock<HookRegistry>>,
1682 commands: Arc<RwLock<CommandRegistry>>,
1683 command_sender: std::sync::mpsc::Sender<PluginCommand>,
1684 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
1685 ) -> Self {
1686 Self {
1687 hooks,
1688 commands,
1689 command_sender,
1690 state_snapshot,
1691 }
1692 }
1693
1694 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
1696 let mut hooks = self.hooks.write().unwrap();
1697 hooks.add_hook(hook_name, callback);
1698 }
1699
1700 pub fn unregister_hooks(&self, hook_name: &str) {
1702 let mut hooks = self.hooks.write().unwrap();
1703 hooks.remove_hooks(hook_name);
1704 }
1705
1706 pub fn register_command(&self, command: Command) {
1708 let commands = self.commands.read().unwrap();
1709 commands.register(command);
1710 }
1711
1712 pub fn unregister_command(&self, name: &str) {
1714 let commands = self.commands.read().unwrap();
1715 commands.unregister(name);
1716 }
1717
1718 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
1720 self.command_sender
1721 .send(command)
1722 .map_err(|e| format!("Failed to send command: {}", e))
1723 }
1724
1725 pub fn insert_text(
1727 &self,
1728 buffer_id: BufferId,
1729 position: usize,
1730 text: String,
1731 ) -> Result<(), String> {
1732 self.send_command(PluginCommand::InsertText {
1733 buffer_id,
1734 position,
1735 text,
1736 })
1737 }
1738
1739 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
1741 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
1742 }
1743
1744 #[allow(clippy::too_many_arguments)]
1747 pub fn add_overlay(
1748 &self,
1749 buffer_id: BufferId,
1750 namespace: Option<String>,
1751 range: Range<usize>,
1752 color: (u8, u8, u8),
1753 bg_color: Option<(u8, u8, u8)>,
1754 underline: bool,
1755 bold: bool,
1756 italic: bool,
1757 extend_to_line_end: bool,
1758 ) -> Result<(), String> {
1759 self.send_command(PluginCommand::AddOverlay {
1760 buffer_id,
1761 namespace: namespace.map(OverlayNamespace::from_string),
1762 range,
1763 color,
1764 bg_color,
1765 underline,
1766 bold,
1767 italic,
1768 extend_to_line_end,
1769 })
1770 }
1771
1772 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
1774 self.send_command(PluginCommand::RemoveOverlay {
1775 buffer_id,
1776 handle: OverlayHandle::from_string(handle),
1777 })
1778 }
1779
1780 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
1782 self.send_command(PluginCommand::ClearNamespace {
1783 buffer_id,
1784 namespace: OverlayNamespace::from_string(namespace),
1785 })
1786 }
1787
1788 pub fn clear_overlays_in_range(
1791 &self,
1792 buffer_id: BufferId,
1793 start: usize,
1794 end: usize,
1795 ) -> Result<(), String> {
1796 self.send_command(PluginCommand::ClearOverlaysInRange {
1797 buffer_id,
1798 start,
1799 end,
1800 })
1801 }
1802
1803 pub fn set_status(&self, message: String) -> Result<(), String> {
1805 self.send_command(PluginCommand::SetStatus { message })
1806 }
1807
1808 pub fn open_file_at_location(
1811 &self,
1812 path: PathBuf,
1813 line: Option<usize>,
1814 column: Option<usize>,
1815 ) -> Result<(), String> {
1816 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
1817 }
1818
1819 pub fn open_file_in_split(
1824 &self,
1825 split_id: usize,
1826 path: PathBuf,
1827 line: Option<usize>,
1828 column: Option<usize>,
1829 ) -> Result<(), String> {
1830 self.send_command(PluginCommand::OpenFileInSplit {
1831 split_id,
1832 path,
1833 line,
1834 column,
1835 })
1836 }
1837
1838 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
1841 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
1842 }
1843
1844 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
1847 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
1848 }
1849
1850 pub fn add_menu_item(
1852 &self,
1853 menu_label: String,
1854 item: MenuItem,
1855 position: MenuPosition,
1856 ) -> Result<(), String> {
1857 self.send_command(PluginCommand::AddMenuItem {
1858 menu_label,
1859 item,
1860 position,
1861 })
1862 }
1863
1864 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
1866 self.send_command(PluginCommand::AddMenu { menu, position })
1867 }
1868
1869 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
1871 self.send_command(PluginCommand::RemoveMenuItem {
1872 menu_label,
1873 item_label,
1874 })
1875 }
1876
1877 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
1879 self.send_command(PluginCommand::RemoveMenu { menu_label })
1880 }
1881
1882 pub fn create_virtual_buffer(
1889 &self,
1890 name: String,
1891 mode: String,
1892 read_only: bool,
1893 ) -> Result<(), String> {
1894 self.send_command(PluginCommand::CreateVirtualBuffer {
1895 name,
1896 mode,
1897 read_only,
1898 })
1899 }
1900
1901 pub fn create_virtual_buffer_with_content(
1907 &self,
1908 name: String,
1909 mode: String,
1910 read_only: bool,
1911 entries: Vec<TextPropertyEntry>,
1912 ) -> Result<(), String> {
1913 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
1914 name,
1915 mode,
1916 read_only,
1917 entries,
1918 show_line_numbers: true,
1919 show_cursors: true,
1920 editing_disabled: false,
1921 hidden_from_tabs: false,
1922 request_id: None,
1923 })
1924 }
1925
1926 pub fn set_virtual_buffer_content(
1930 &self,
1931 buffer_id: BufferId,
1932 entries: Vec<TextPropertyEntry>,
1933 ) -> Result<(), String> {
1934 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
1935 }
1936
1937 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
1941 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
1942 }
1943
1944 pub fn define_mode(
1949 &self,
1950 name: String,
1951 parent: Option<String>,
1952 bindings: Vec<(String, String)>,
1953 read_only: bool,
1954 ) -> Result<(), String> {
1955 self.send_command(PluginCommand::DefineMode {
1956 name,
1957 parent,
1958 bindings,
1959 read_only,
1960 })
1961 }
1962
1963 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
1965 self.send_command(PluginCommand::ShowBuffer { buffer_id })
1966 }
1967
1968 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
1970 self.send_command(PluginCommand::SetSplitScroll {
1971 split_id: SplitId(split_id),
1972 top_byte,
1973 })
1974 }
1975
1976 pub fn get_highlights(
1978 &self,
1979 buffer_id: BufferId,
1980 range: Range<usize>,
1981 request_id: u64,
1982 ) -> Result<(), String> {
1983 self.send_command(PluginCommand::RequestHighlights {
1984 buffer_id,
1985 range,
1986 request_id,
1987 })
1988 }
1989
1990 pub fn get_active_buffer_id(&self) -> BufferId {
1994 let snapshot = self.state_snapshot.read().unwrap();
1995 snapshot.active_buffer_id
1996 }
1997
1998 pub fn get_active_split_id(&self) -> usize {
2000 let snapshot = self.state_snapshot.read().unwrap();
2001 snapshot.active_split_id
2002 }
2003
2004 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
2006 let snapshot = self.state_snapshot.read().unwrap();
2007 snapshot.buffers.get(&buffer_id).cloned()
2008 }
2009
2010 pub fn list_buffers(&self) -> Vec<BufferInfo> {
2012 let snapshot = self.state_snapshot.read().unwrap();
2013 snapshot.buffers.values().cloned().collect()
2014 }
2015
2016 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
2018 let snapshot = self.state_snapshot.read().unwrap();
2019 snapshot.primary_cursor.clone()
2020 }
2021
2022 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
2024 let snapshot = self.state_snapshot.read().unwrap();
2025 snapshot.all_cursors.clone()
2026 }
2027
2028 pub fn get_viewport(&self) -> Option<ViewportInfo> {
2030 let snapshot = self.state_snapshot.read().unwrap();
2031 snapshot.viewport.clone()
2032 }
2033
2034 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
2036 Arc::clone(&self.state_snapshot)
2037 }
2038}
2039
2040impl Clone for PluginApi {
2041 fn clone(&self) -> Self {
2042 Self {
2043 hooks: Arc::clone(&self.hooks),
2044 commands: Arc::clone(&self.commands),
2045 command_sender: self.command_sender.clone(),
2046 state_snapshot: Arc::clone(&self.state_snapshot),
2047 }
2048 }
2049}
2050
2051#[cfg(test)]
2052mod tests {
2053 use super::*;
2054
2055 #[test]
2056 fn test_plugin_api_creation() {
2057 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2058 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2059 let (tx, _rx) = std::sync::mpsc::channel();
2060 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2061
2062 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2063
2064 let _clone = api.clone();
2066 }
2067
2068 #[test]
2069 fn test_register_hook() {
2070 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2071 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2072 let (tx, _rx) = std::sync::mpsc::channel();
2073 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2074
2075 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
2076
2077 api.register_hook("test-hook", Box::new(|_| true));
2078
2079 let hook_registry = hooks.read().unwrap();
2080 assert_eq!(hook_registry.hook_count("test-hook"), 1);
2081 }
2082
2083 #[test]
2084 fn test_send_command() {
2085 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2086 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2087 let (tx, rx) = std::sync::mpsc::channel();
2088 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2089
2090 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2091
2092 let result = api.insert_text(BufferId(1), 0, "test".to_string());
2093 assert!(result.is_ok());
2094
2095 let received = rx.try_recv();
2097 assert!(received.is_ok());
2098
2099 match received.unwrap() {
2100 PluginCommand::InsertText {
2101 buffer_id,
2102 position,
2103 text,
2104 } => {
2105 assert_eq!(buffer_id.0, 1);
2106 assert_eq!(position, 0);
2107 assert_eq!(text, "test");
2108 }
2109 _ => panic!("Wrong command type"),
2110 }
2111 }
2112
2113 #[test]
2114 fn test_add_overlay_command() {
2115 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2116 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2117 let (tx, rx) = std::sync::mpsc::channel();
2118 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2119
2120 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2121
2122 let result = api.add_overlay(
2123 BufferId(1),
2124 Some("test-overlay".to_string()),
2125 0..10,
2126 (255, 0, 0),
2127 None,
2128 true,
2129 false,
2130 false,
2131 false,
2132 );
2133 assert!(result.is_ok());
2134
2135 let received = rx.try_recv().unwrap();
2136 match received {
2137 PluginCommand::AddOverlay {
2138 buffer_id,
2139 namespace,
2140 range,
2141 color,
2142 bg_color,
2143 underline,
2144 bold,
2145 italic,
2146 extend_to_line_end,
2147 } => {
2148 assert_eq!(buffer_id.0, 1);
2149 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
2150 assert_eq!(range, 0..10);
2151 assert_eq!(color, (255, 0, 0));
2152 assert_eq!(bg_color, None);
2153 assert!(underline);
2154 assert!(!bold);
2155 assert!(!italic);
2156 assert!(!extend_to_line_end);
2157 }
2158 _ => panic!("Wrong command type"),
2159 }
2160 }
2161
2162 #[test]
2163 fn test_set_status_command() {
2164 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2165 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2166 let (tx, rx) = std::sync::mpsc::channel();
2167 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2168
2169 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2170
2171 let result = api.set_status("Test status".to_string());
2172 assert!(result.is_ok());
2173
2174 let received = rx.try_recv().unwrap();
2175 match received {
2176 PluginCommand::SetStatus { message } => {
2177 assert_eq!(message, "Test status");
2178 }
2179 _ => panic!("Wrong command type"),
2180 }
2181 }
2182
2183 #[test]
2184 fn test_get_active_buffer_id() {
2185 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2186 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2187 let (tx, _rx) = std::sync::mpsc::channel();
2188 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2189
2190 {
2192 let mut snapshot = state_snapshot.write().unwrap();
2193 snapshot.active_buffer_id = BufferId(5);
2194 }
2195
2196 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2197
2198 let active_id = api.get_active_buffer_id();
2199 assert_eq!(active_id.0, 5);
2200 }
2201
2202 #[test]
2203 fn test_get_buffer_info() {
2204 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2205 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2206 let (tx, _rx) = std::sync::mpsc::channel();
2207 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2208
2209 {
2211 let mut snapshot = state_snapshot.write().unwrap();
2212 let buffer_info = BufferInfo {
2213 id: BufferId(1),
2214 path: Some(std::path::PathBuf::from("/test/file.txt")),
2215 modified: true,
2216 length: 100,
2217 };
2218 snapshot.buffers.insert(BufferId(1), buffer_info);
2219 }
2220
2221 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2222
2223 let info = api.get_buffer_info(BufferId(1));
2224 assert!(info.is_some());
2225 let info = info.unwrap();
2226 assert_eq!(info.id.0, 1);
2227 assert_eq!(
2228 info.path.as_ref().unwrap().to_str().unwrap(),
2229 "/test/file.txt"
2230 );
2231 assert!(info.modified);
2232 assert_eq!(info.length, 100);
2233
2234 let no_info = api.get_buffer_info(BufferId(999));
2236 assert!(no_info.is_none());
2237 }
2238
2239 #[test]
2240 fn test_list_buffers() {
2241 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2242 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2243 let (tx, _rx) = std::sync::mpsc::channel();
2244 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2245
2246 {
2248 let mut snapshot = state_snapshot.write().unwrap();
2249 snapshot.buffers.insert(
2250 BufferId(1),
2251 BufferInfo {
2252 id: BufferId(1),
2253 path: Some(std::path::PathBuf::from("/file1.txt")),
2254 modified: false,
2255 length: 50,
2256 },
2257 );
2258 snapshot.buffers.insert(
2259 BufferId(2),
2260 BufferInfo {
2261 id: BufferId(2),
2262 path: Some(std::path::PathBuf::from("/file2.txt")),
2263 modified: true,
2264 length: 100,
2265 },
2266 );
2267 snapshot.buffers.insert(
2268 BufferId(3),
2269 BufferInfo {
2270 id: BufferId(3),
2271 path: None,
2272 modified: false,
2273 length: 0,
2274 },
2275 );
2276 }
2277
2278 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2279
2280 let buffers = api.list_buffers();
2281 assert_eq!(buffers.len(), 3);
2282
2283 assert!(buffers.iter().any(|b| b.id.0 == 1));
2285 assert!(buffers.iter().any(|b| b.id.0 == 2));
2286 assert!(buffers.iter().any(|b| b.id.0 == 3));
2287 }
2288
2289 #[test]
2290 fn test_get_primary_cursor() {
2291 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2292 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2293 let (tx, _rx) = std::sync::mpsc::channel();
2294 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2295
2296 {
2298 let mut snapshot = state_snapshot.write().unwrap();
2299 snapshot.primary_cursor = Some(CursorInfo {
2300 position: 42,
2301 selection: Some(10..42),
2302 });
2303 }
2304
2305 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2306
2307 let cursor = api.get_primary_cursor();
2308 assert!(cursor.is_some());
2309 let cursor = cursor.unwrap();
2310 assert_eq!(cursor.position, 42);
2311 assert_eq!(cursor.selection, Some(10..42));
2312 }
2313
2314 #[test]
2315 fn test_get_all_cursors() {
2316 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2317 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2318 let (tx, _rx) = std::sync::mpsc::channel();
2319 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2320
2321 {
2323 let mut snapshot = state_snapshot.write().unwrap();
2324 snapshot.all_cursors = vec![
2325 CursorInfo {
2326 position: 10,
2327 selection: None,
2328 },
2329 CursorInfo {
2330 position: 20,
2331 selection: Some(15..20),
2332 },
2333 CursorInfo {
2334 position: 30,
2335 selection: Some(25..30),
2336 },
2337 ];
2338 }
2339
2340 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2341
2342 let cursors = api.get_all_cursors();
2343 assert_eq!(cursors.len(), 3);
2344 assert_eq!(cursors[0].position, 10);
2345 assert_eq!(cursors[0].selection, None);
2346 assert_eq!(cursors[1].position, 20);
2347 assert_eq!(cursors[1].selection, Some(15..20));
2348 assert_eq!(cursors[2].position, 30);
2349 assert_eq!(cursors[2].selection, Some(25..30));
2350 }
2351
2352 #[test]
2353 fn test_get_viewport() {
2354 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2355 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2356 let (tx, _rx) = std::sync::mpsc::channel();
2357 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2358
2359 {
2361 let mut snapshot = state_snapshot.write().unwrap();
2362 snapshot.viewport = Some(ViewportInfo {
2363 top_byte: 100,
2364 left_column: 5,
2365 width: 80,
2366 height: 24,
2367 });
2368 }
2369
2370 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2371
2372 let viewport = api.get_viewport();
2373 assert!(viewport.is_some());
2374 let viewport = viewport.unwrap();
2375 assert_eq!(viewport.top_byte, 100);
2376 assert_eq!(viewport.left_column, 5);
2377 assert_eq!(viewport.width, 80);
2378 assert_eq!(viewport.height, 24);
2379 }
2380
2381 #[test]
2382 fn test_composite_buffer_options_rejects_unknown_fields() {
2383 let valid_json = r#"{
2385 "name": "test",
2386 "mode": "diff",
2387 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2388 "sources": [{"bufferId": 1, "label": "old"}]
2389 }"#;
2390 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
2391 assert!(
2392 result.is_ok(),
2393 "Valid JSON should parse: {:?}",
2394 result.err()
2395 );
2396
2397 let invalid_json = r#"{
2399 "name": "test",
2400 "mode": "diff",
2401 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2402 "sources": [{"buffer_id": 1, "label": "old"}]
2403 }"#;
2404 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
2405 assert!(
2406 result.is_err(),
2407 "JSON with unknown field should fail to parse"
2408 );
2409 let err = result.unwrap_err().to_string();
2410 assert!(
2411 err.contains("unknown field") || err.contains("buffer_id"),
2412 "Error should mention unknown field: {}",
2413 err
2414 );
2415 }
2416
2417 #[test]
2418 fn test_composite_hunk_rejects_unknown_fields() {
2419 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
2421 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
2422 assert!(
2423 result.is_ok(),
2424 "Valid JSON should parse: {:?}",
2425 result.err()
2426 );
2427
2428 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
2430 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
2431 assert!(
2432 result.is_err(),
2433 "JSON with unknown field should fail to parse"
2434 );
2435 let err = result.unwrap_err().to_string();
2436 assert!(
2437 err.contains("unknown field") || err.contains("old_start"),
2438 "Error should mention unknown field: {}",
2439 err
2440 );
2441 }
2442}