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 SetLspRootUri {
1205 language: String,
1207 uri: String,
1209 },
1210
1211 CreateScrollSyncGroup {
1215 group_id: u32,
1217 left_split: SplitId,
1219 right_split: SplitId,
1221 },
1222
1223 SetScrollSyncAnchors {
1226 group_id: u32,
1228 anchors: Vec<(usize, usize)>,
1230 },
1231
1232 RemoveScrollSyncGroup {
1234 group_id: u32,
1236 },
1237
1238 SaveBufferToPath {
1241 buffer_id: BufferId,
1243 path: PathBuf,
1245 },
1246}
1247
1248#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1250#[ts(export)]
1251pub enum HunkStatus {
1252 Pending,
1253 Staged,
1254 Discarded,
1255}
1256
1257#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1259#[ts(export)]
1260pub struct ReviewHunk {
1261 pub id: String,
1262 pub file: String,
1263 pub context_header: String,
1264 pub status: HunkStatus,
1265 pub base_range: Option<(usize, usize)>,
1267 pub modified_range: Option<(usize, usize)>,
1269}
1270
1271#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1273#[serde(deny_unknown_fields)]
1274#[ts(export, rename = "TsActionPopupAction")]
1275pub struct ActionPopupAction {
1276 pub id: String,
1278 pub label: String,
1280}
1281
1282#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1284#[serde(deny_unknown_fields)]
1285#[ts(export)]
1286pub struct ActionPopupOptions {
1287 pub id: String,
1289 pub title: String,
1291 pub message: String,
1293 pub actions: Vec<ActionPopupAction>,
1295}
1296
1297#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1299#[ts(export)]
1300pub struct TsHighlightSpan {
1301 pub start: u32,
1302 pub end: u32,
1303 #[ts(type = "[number, number, number]")]
1304 pub color: (u8, u8, u8),
1305 pub bold: bool,
1306 pub italic: bool,
1307}
1308
1309#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1311#[ts(export)]
1312pub struct SpawnResult {
1313 pub stdout: String,
1315 pub stderr: String,
1317 pub exit_code: i32,
1319}
1320
1321#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1323#[ts(export)]
1324pub struct BackgroundProcessResult {
1325 #[ts(type = "number")]
1327 pub process_id: u64,
1328 pub exit_code: i32,
1331}
1332
1333#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1335#[serde(deny_unknown_fields)]
1336#[ts(export, rename = "TextPropertyEntry")]
1337pub struct JsTextPropertyEntry {
1338 pub text: String,
1340 #[serde(default)]
1342 #[ts(optional, type = "Record<string, unknown>")]
1343 pub properties: Option<HashMap<String, JsonValue>>,
1344}
1345
1346#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1348#[ts(export)]
1349pub struct DirEntry {
1350 pub name: String,
1352 pub is_file: bool,
1354 pub is_dir: bool,
1356}
1357
1358#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1360#[ts(export)]
1361pub struct JsPosition {
1362 pub line: u32,
1364 pub character: u32,
1366}
1367
1368#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1370#[ts(export)]
1371pub struct JsRange {
1372 pub start: JsPosition,
1374 pub end: JsPosition,
1376}
1377
1378#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1380#[ts(export)]
1381pub struct JsDiagnostic {
1382 pub uri: String,
1384 pub message: String,
1386 pub severity: Option<u8>,
1388 pub range: JsRange,
1390 #[ts(optional)]
1392 pub source: Option<String>,
1393}
1394
1395#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1397#[serde(deny_unknown_fields)]
1398#[ts(export)]
1399pub struct CreateVirtualBufferOptions {
1400 pub name: String,
1402 #[serde(default)]
1404 #[ts(optional)]
1405 pub mode: Option<String>,
1406 #[serde(default, rename = "readOnly")]
1408 #[ts(optional, rename = "readOnly")]
1409 pub read_only: Option<bool>,
1410 #[serde(default, rename = "showLineNumbers")]
1412 #[ts(optional, rename = "showLineNumbers")]
1413 pub show_line_numbers: Option<bool>,
1414 #[serde(default, rename = "showCursors")]
1416 #[ts(optional, rename = "showCursors")]
1417 pub show_cursors: Option<bool>,
1418 #[serde(default, rename = "editingDisabled")]
1420 #[ts(optional, rename = "editingDisabled")]
1421 pub editing_disabled: Option<bool>,
1422 #[serde(default, rename = "hiddenFromTabs")]
1424 #[ts(optional, rename = "hiddenFromTabs")]
1425 pub hidden_from_tabs: Option<bool>,
1426 #[serde(default)]
1428 #[ts(optional)]
1429 pub entries: Option<Vec<JsTextPropertyEntry>>,
1430}
1431
1432#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1434#[serde(deny_unknown_fields)]
1435#[ts(export)]
1436pub struct CreateVirtualBufferInSplitOptions {
1437 pub name: String,
1439 #[serde(default)]
1441 #[ts(optional)]
1442 pub mode: Option<String>,
1443 #[serde(default, rename = "readOnly")]
1445 #[ts(optional, rename = "readOnly")]
1446 pub read_only: Option<bool>,
1447 #[serde(default)]
1449 #[ts(optional)]
1450 pub ratio: Option<f32>,
1451 #[serde(default)]
1453 #[ts(optional)]
1454 pub direction: Option<String>,
1455 #[serde(default, rename = "panelId")]
1457 #[ts(optional, rename = "panelId")]
1458 pub panel_id: Option<String>,
1459 #[serde(default, rename = "showLineNumbers")]
1461 #[ts(optional, rename = "showLineNumbers")]
1462 pub show_line_numbers: Option<bool>,
1463 #[serde(default, rename = "showCursors")]
1465 #[ts(optional, rename = "showCursors")]
1466 pub show_cursors: Option<bool>,
1467 #[serde(default, rename = "editingDisabled")]
1469 #[ts(optional, rename = "editingDisabled")]
1470 pub editing_disabled: Option<bool>,
1471 #[serde(default, rename = "lineWrap")]
1473 #[ts(optional, rename = "lineWrap")]
1474 pub line_wrap: Option<bool>,
1475 #[serde(default)]
1477 #[ts(optional)]
1478 pub entries: Option<Vec<JsTextPropertyEntry>>,
1479}
1480
1481#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1483#[serde(deny_unknown_fields)]
1484#[ts(export)]
1485pub struct CreateVirtualBufferInExistingSplitOptions {
1486 pub name: String,
1488 #[serde(rename = "splitId")]
1490 #[ts(rename = "splitId")]
1491 pub split_id: usize,
1492 #[serde(default)]
1494 #[ts(optional)]
1495 pub mode: Option<String>,
1496 #[serde(default, rename = "readOnly")]
1498 #[ts(optional, rename = "readOnly")]
1499 pub read_only: Option<bool>,
1500 #[serde(default, rename = "showLineNumbers")]
1502 #[ts(optional, rename = "showLineNumbers")]
1503 pub show_line_numbers: Option<bool>,
1504 #[serde(default, rename = "showCursors")]
1506 #[ts(optional, rename = "showCursors")]
1507 pub show_cursors: Option<bool>,
1508 #[serde(default, rename = "editingDisabled")]
1510 #[ts(optional, rename = "editingDisabled")]
1511 pub editing_disabled: Option<bool>,
1512 #[serde(default, rename = "lineWrap")]
1514 #[ts(optional, rename = "lineWrap")]
1515 pub line_wrap: Option<bool>,
1516 #[serde(default)]
1518 #[ts(optional)]
1519 pub entries: Option<Vec<JsTextPropertyEntry>>,
1520}
1521
1522#[derive(Debug, Clone, Serialize, TS)]
1527#[ts(export, type = "Array<Record<string, unknown>>")]
1528pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
1529
1530#[cfg(feature = "plugins")]
1532mod fromjs_impls {
1533 use super::*;
1534 use rquickjs::{Ctx, FromJs, Value};
1535
1536 impl<'js> FromJs<'js> for JsTextPropertyEntry {
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: "JsTextPropertyEntry",
1541 message: Some(e.to_string()),
1542 })
1543 }
1544 }
1545
1546 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
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: "CreateVirtualBufferOptions",
1551 message: Some(e.to_string()),
1552 })
1553 }
1554 }
1555
1556 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
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: "CreateVirtualBufferInSplitOptions",
1561 message: Some(e.to_string()),
1562 })
1563 }
1564 }
1565
1566 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
1567 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1568 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1569 from: "object",
1570 to: "CreateVirtualBufferInExistingSplitOptions",
1571 message: Some(e.to_string()),
1572 })
1573 }
1574 }
1575
1576 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
1577 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
1578 rquickjs_serde::to_value(ctx.clone(), &self.0)
1579 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
1580 }
1581 }
1582
1583 impl<'js> FromJs<'js> for ActionSpec {
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: "ActionSpec",
1590 message: Some(e.to_string()),
1591 })
1592 }
1593 }
1594
1595 impl<'js> FromJs<'js> for ActionPopupAction {
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: "ActionPopupAction",
1600 message: Some(e.to_string()),
1601 })
1602 }
1603 }
1604
1605 impl<'js> FromJs<'js> for ActionPopupOptions {
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: "ActionPopupOptions",
1610 message: Some(e.to_string()),
1611 })
1612 }
1613 }
1614
1615 impl<'js> FromJs<'js> for ViewTokenWire {
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: "ViewTokenWire",
1620 message: Some(e.to_string()),
1621 })
1622 }
1623 }
1624
1625 impl<'js> FromJs<'js> for ViewTokenStyle {
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: "ViewTokenStyle",
1630 message: Some(e.to_string()),
1631 })
1632 }
1633 }
1634
1635 impl<'js> FromJs<'js> for LayoutHints {
1636 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1637 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1638 from: "object",
1639 to: "LayoutHints",
1640 message: Some(e.to_string()),
1641 })
1642 }
1643 }
1644
1645 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
1646 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1647 let json: serde_json::Value =
1649 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1650 from: "object",
1651 to: "CreateCompositeBufferOptions (json)",
1652 message: Some(e.to_string()),
1653 })?;
1654 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
1655 from: "json",
1656 to: "CreateCompositeBufferOptions",
1657 message: Some(e.to_string()),
1658 })
1659 }
1660 }
1661
1662 impl<'js> FromJs<'js> for CompositeHunk {
1663 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1664 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1665 from: "object",
1666 to: "CompositeHunk",
1667 message: Some(e.to_string()),
1668 })
1669 }
1670 }
1671}
1672
1673pub struct PluginApi {
1675 hooks: Arc<RwLock<HookRegistry>>,
1677
1678 commands: Arc<RwLock<CommandRegistry>>,
1680
1681 command_sender: std::sync::mpsc::Sender<PluginCommand>,
1683
1684 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
1686}
1687
1688impl PluginApi {
1689 pub fn new(
1691 hooks: Arc<RwLock<HookRegistry>>,
1692 commands: Arc<RwLock<CommandRegistry>>,
1693 command_sender: std::sync::mpsc::Sender<PluginCommand>,
1694 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
1695 ) -> Self {
1696 Self {
1697 hooks,
1698 commands,
1699 command_sender,
1700 state_snapshot,
1701 }
1702 }
1703
1704 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
1706 let mut hooks = self.hooks.write().unwrap();
1707 hooks.add_hook(hook_name, callback);
1708 }
1709
1710 pub fn unregister_hooks(&self, hook_name: &str) {
1712 let mut hooks = self.hooks.write().unwrap();
1713 hooks.remove_hooks(hook_name);
1714 }
1715
1716 pub fn register_command(&self, command: Command) {
1718 let commands = self.commands.read().unwrap();
1719 commands.register(command);
1720 }
1721
1722 pub fn unregister_command(&self, name: &str) {
1724 let commands = self.commands.read().unwrap();
1725 commands.unregister(name);
1726 }
1727
1728 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
1730 self.command_sender
1731 .send(command)
1732 .map_err(|e| format!("Failed to send command: {}", e))
1733 }
1734
1735 pub fn insert_text(
1737 &self,
1738 buffer_id: BufferId,
1739 position: usize,
1740 text: String,
1741 ) -> Result<(), String> {
1742 self.send_command(PluginCommand::InsertText {
1743 buffer_id,
1744 position,
1745 text,
1746 })
1747 }
1748
1749 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
1751 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
1752 }
1753
1754 #[allow(clippy::too_many_arguments)]
1757 pub fn add_overlay(
1758 &self,
1759 buffer_id: BufferId,
1760 namespace: Option<String>,
1761 range: Range<usize>,
1762 color: (u8, u8, u8),
1763 bg_color: Option<(u8, u8, u8)>,
1764 underline: bool,
1765 bold: bool,
1766 italic: bool,
1767 extend_to_line_end: bool,
1768 ) -> Result<(), String> {
1769 self.send_command(PluginCommand::AddOverlay {
1770 buffer_id,
1771 namespace: namespace.map(OverlayNamespace::from_string),
1772 range,
1773 color,
1774 bg_color,
1775 underline,
1776 bold,
1777 italic,
1778 extend_to_line_end,
1779 })
1780 }
1781
1782 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
1784 self.send_command(PluginCommand::RemoveOverlay {
1785 buffer_id,
1786 handle: OverlayHandle::from_string(handle),
1787 })
1788 }
1789
1790 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
1792 self.send_command(PluginCommand::ClearNamespace {
1793 buffer_id,
1794 namespace: OverlayNamespace::from_string(namespace),
1795 })
1796 }
1797
1798 pub fn clear_overlays_in_range(
1801 &self,
1802 buffer_id: BufferId,
1803 start: usize,
1804 end: usize,
1805 ) -> Result<(), String> {
1806 self.send_command(PluginCommand::ClearOverlaysInRange {
1807 buffer_id,
1808 start,
1809 end,
1810 })
1811 }
1812
1813 pub fn set_status(&self, message: String) -> Result<(), String> {
1815 self.send_command(PluginCommand::SetStatus { message })
1816 }
1817
1818 pub fn open_file_at_location(
1821 &self,
1822 path: PathBuf,
1823 line: Option<usize>,
1824 column: Option<usize>,
1825 ) -> Result<(), String> {
1826 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
1827 }
1828
1829 pub fn open_file_in_split(
1834 &self,
1835 split_id: usize,
1836 path: PathBuf,
1837 line: Option<usize>,
1838 column: Option<usize>,
1839 ) -> Result<(), String> {
1840 self.send_command(PluginCommand::OpenFileInSplit {
1841 split_id,
1842 path,
1843 line,
1844 column,
1845 })
1846 }
1847
1848 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
1851 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
1852 }
1853
1854 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
1857 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
1858 }
1859
1860 pub fn add_menu_item(
1862 &self,
1863 menu_label: String,
1864 item: MenuItem,
1865 position: MenuPosition,
1866 ) -> Result<(), String> {
1867 self.send_command(PluginCommand::AddMenuItem {
1868 menu_label,
1869 item,
1870 position,
1871 })
1872 }
1873
1874 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
1876 self.send_command(PluginCommand::AddMenu { menu, position })
1877 }
1878
1879 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
1881 self.send_command(PluginCommand::RemoveMenuItem {
1882 menu_label,
1883 item_label,
1884 })
1885 }
1886
1887 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
1889 self.send_command(PluginCommand::RemoveMenu { menu_label })
1890 }
1891
1892 pub fn create_virtual_buffer(
1899 &self,
1900 name: String,
1901 mode: String,
1902 read_only: bool,
1903 ) -> Result<(), String> {
1904 self.send_command(PluginCommand::CreateVirtualBuffer {
1905 name,
1906 mode,
1907 read_only,
1908 })
1909 }
1910
1911 pub fn create_virtual_buffer_with_content(
1917 &self,
1918 name: String,
1919 mode: String,
1920 read_only: bool,
1921 entries: Vec<TextPropertyEntry>,
1922 ) -> Result<(), String> {
1923 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
1924 name,
1925 mode,
1926 read_only,
1927 entries,
1928 show_line_numbers: true,
1929 show_cursors: true,
1930 editing_disabled: false,
1931 hidden_from_tabs: false,
1932 request_id: None,
1933 })
1934 }
1935
1936 pub fn set_virtual_buffer_content(
1940 &self,
1941 buffer_id: BufferId,
1942 entries: Vec<TextPropertyEntry>,
1943 ) -> Result<(), String> {
1944 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
1945 }
1946
1947 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
1951 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
1952 }
1953
1954 pub fn define_mode(
1959 &self,
1960 name: String,
1961 parent: Option<String>,
1962 bindings: Vec<(String, String)>,
1963 read_only: bool,
1964 ) -> Result<(), String> {
1965 self.send_command(PluginCommand::DefineMode {
1966 name,
1967 parent,
1968 bindings,
1969 read_only,
1970 })
1971 }
1972
1973 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
1975 self.send_command(PluginCommand::ShowBuffer { buffer_id })
1976 }
1977
1978 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
1980 self.send_command(PluginCommand::SetSplitScroll {
1981 split_id: SplitId(split_id),
1982 top_byte,
1983 })
1984 }
1985
1986 pub fn get_highlights(
1988 &self,
1989 buffer_id: BufferId,
1990 range: Range<usize>,
1991 request_id: u64,
1992 ) -> Result<(), String> {
1993 self.send_command(PluginCommand::RequestHighlights {
1994 buffer_id,
1995 range,
1996 request_id,
1997 })
1998 }
1999
2000 pub fn get_active_buffer_id(&self) -> BufferId {
2004 let snapshot = self.state_snapshot.read().unwrap();
2005 snapshot.active_buffer_id
2006 }
2007
2008 pub fn get_active_split_id(&self) -> usize {
2010 let snapshot = self.state_snapshot.read().unwrap();
2011 snapshot.active_split_id
2012 }
2013
2014 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
2016 let snapshot = self.state_snapshot.read().unwrap();
2017 snapshot.buffers.get(&buffer_id).cloned()
2018 }
2019
2020 pub fn list_buffers(&self) -> Vec<BufferInfo> {
2022 let snapshot = self.state_snapshot.read().unwrap();
2023 snapshot.buffers.values().cloned().collect()
2024 }
2025
2026 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
2028 let snapshot = self.state_snapshot.read().unwrap();
2029 snapshot.primary_cursor.clone()
2030 }
2031
2032 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
2034 let snapshot = self.state_snapshot.read().unwrap();
2035 snapshot.all_cursors.clone()
2036 }
2037
2038 pub fn get_viewport(&self) -> Option<ViewportInfo> {
2040 let snapshot = self.state_snapshot.read().unwrap();
2041 snapshot.viewport.clone()
2042 }
2043
2044 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
2046 Arc::clone(&self.state_snapshot)
2047 }
2048}
2049
2050impl Clone for PluginApi {
2051 fn clone(&self) -> Self {
2052 Self {
2053 hooks: Arc::clone(&self.hooks),
2054 commands: Arc::clone(&self.commands),
2055 command_sender: self.command_sender.clone(),
2056 state_snapshot: Arc::clone(&self.state_snapshot),
2057 }
2058 }
2059}
2060
2061#[cfg(test)]
2062mod tests {
2063 use super::*;
2064
2065 #[test]
2066 fn test_plugin_api_creation() {
2067 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2068 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2069 let (tx, _rx) = std::sync::mpsc::channel();
2070 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2071
2072 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2073
2074 let _clone = api.clone();
2076 }
2077
2078 #[test]
2079 fn test_register_hook() {
2080 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2081 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2082 let (tx, _rx) = std::sync::mpsc::channel();
2083 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2084
2085 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
2086
2087 api.register_hook("test-hook", Box::new(|_| true));
2088
2089 let hook_registry = hooks.read().unwrap();
2090 assert_eq!(hook_registry.hook_count("test-hook"), 1);
2091 }
2092
2093 #[test]
2094 fn test_send_command() {
2095 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2096 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2097 let (tx, rx) = std::sync::mpsc::channel();
2098 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2099
2100 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2101
2102 let result = api.insert_text(BufferId(1), 0, "test".to_string());
2103 assert!(result.is_ok());
2104
2105 let received = rx.try_recv();
2107 assert!(received.is_ok());
2108
2109 match received.unwrap() {
2110 PluginCommand::InsertText {
2111 buffer_id,
2112 position,
2113 text,
2114 } => {
2115 assert_eq!(buffer_id.0, 1);
2116 assert_eq!(position, 0);
2117 assert_eq!(text, "test");
2118 }
2119 _ => panic!("Wrong command type"),
2120 }
2121 }
2122
2123 #[test]
2124 fn test_add_overlay_command() {
2125 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2126 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2127 let (tx, rx) = std::sync::mpsc::channel();
2128 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2129
2130 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2131
2132 let result = api.add_overlay(
2133 BufferId(1),
2134 Some("test-overlay".to_string()),
2135 0..10,
2136 (255, 0, 0),
2137 None,
2138 true,
2139 false,
2140 false,
2141 false,
2142 );
2143 assert!(result.is_ok());
2144
2145 let received = rx.try_recv().unwrap();
2146 match received {
2147 PluginCommand::AddOverlay {
2148 buffer_id,
2149 namespace,
2150 range,
2151 color,
2152 bg_color,
2153 underline,
2154 bold,
2155 italic,
2156 extend_to_line_end,
2157 } => {
2158 assert_eq!(buffer_id.0, 1);
2159 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
2160 assert_eq!(range, 0..10);
2161 assert_eq!(color, (255, 0, 0));
2162 assert_eq!(bg_color, None);
2163 assert!(underline);
2164 assert!(!bold);
2165 assert!(!italic);
2166 assert!(!extend_to_line_end);
2167 }
2168 _ => panic!("Wrong command type"),
2169 }
2170 }
2171
2172 #[test]
2173 fn test_set_status_command() {
2174 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2175 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2176 let (tx, rx) = std::sync::mpsc::channel();
2177 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2178
2179 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2180
2181 let result = api.set_status("Test status".to_string());
2182 assert!(result.is_ok());
2183
2184 let received = rx.try_recv().unwrap();
2185 match received {
2186 PluginCommand::SetStatus { message } => {
2187 assert_eq!(message, "Test status");
2188 }
2189 _ => panic!("Wrong command type"),
2190 }
2191 }
2192
2193 #[test]
2194 fn test_get_active_buffer_id() {
2195 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2196 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2197 let (tx, _rx) = std::sync::mpsc::channel();
2198 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2199
2200 {
2202 let mut snapshot = state_snapshot.write().unwrap();
2203 snapshot.active_buffer_id = BufferId(5);
2204 }
2205
2206 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2207
2208 let active_id = api.get_active_buffer_id();
2209 assert_eq!(active_id.0, 5);
2210 }
2211
2212 #[test]
2213 fn test_get_buffer_info() {
2214 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2215 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2216 let (tx, _rx) = std::sync::mpsc::channel();
2217 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2218
2219 {
2221 let mut snapshot = state_snapshot.write().unwrap();
2222 let buffer_info = BufferInfo {
2223 id: BufferId(1),
2224 path: Some(std::path::PathBuf::from("/test/file.txt")),
2225 modified: true,
2226 length: 100,
2227 };
2228 snapshot.buffers.insert(BufferId(1), buffer_info);
2229 }
2230
2231 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2232
2233 let info = api.get_buffer_info(BufferId(1));
2234 assert!(info.is_some());
2235 let info = info.unwrap();
2236 assert_eq!(info.id.0, 1);
2237 assert_eq!(
2238 info.path.as_ref().unwrap().to_str().unwrap(),
2239 "/test/file.txt"
2240 );
2241 assert!(info.modified);
2242 assert_eq!(info.length, 100);
2243
2244 let no_info = api.get_buffer_info(BufferId(999));
2246 assert!(no_info.is_none());
2247 }
2248
2249 #[test]
2250 fn test_list_buffers() {
2251 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2252 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2253 let (tx, _rx) = std::sync::mpsc::channel();
2254 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2255
2256 {
2258 let mut snapshot = state_snapshot.write().unwrap();
2259 snapshot.buffers.insert(
2260 BufferId(1),
2261 BufferInfo {
2262 id: BufferId(1),
2263 path: Some(std::path::PathBuf::from("/file1.txt")),
2264 modified: false,
2265 length: 50,
2266 },
2267 );
2268 snapshot.buffers.insert(
2269 BufferId(2),
2270 BufferInfo {
2271 id: BufferId(2),
2272 path: Some(std::path::PathBuf::from("/file2.txt")),
2273 modified: true,
2274 length: 100,
2275 },
2276 );
2277 snapshot.buffers.insert(
2278 BufferId(3),
2279 BufferInfo {
2280 id: BufferId(3),
2281 path: None,
2282 modified: false,
2283 length: 0,
2284 },
2285 );
2286 }
2287
2288 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2289
2290 let buffers = api.list_buffers();
2291 assert_eq!(buffers.len(), 3);
2292
2293 assert!(buffers.iter().any(|b| b.id.0 == 1));
2295 assert!(buffers.iter().any(|b| b.id.0 == 2));
2296 assert!(buffers.iter().any(|b| b.id.0 == 3));
2297 }
2298
2299 #[test]
2300 fn test_get_primary_cursor() {
2301 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2302 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2303 let (tx, _rx) = std::sync::mpsc::channel();
2304 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2305
2306 {
2308 let mut snapshot = state_snapshot.write().unwrap();
2309 snapshot.primary_cursor = Some(CursorInfo {
2310 position: 42,
2311 selection: Some(10..42),
2312 });
2313 }
2314
2315 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2316
2317 let cursor = api.get_primary_cursor();
2318 assert!(cursor.is_some());
2319 let cursor = cursor.unwrap();
2320 assert_eq!(cursor.position, 42);
2321 assert_eq!(cursor.selection, Some(10..42));
2322 }
2323
2324 #[test]
2325 fn test_get_all_cursors() {
2326 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2327 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2328 let (tx, _rx) = std::sync::mpsc::channel();
2329 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2330
2331 {
2333 let mut snapshot = state_snapshot.write().unwrap();
2334 snapshot.all_cursors = vec![
2335 CursorInfo {
2336 position: 10,
2337 selection: None,
2338 },
2339 CursorInfo {
2340 position: 20,
2341 selection: Some(15..20),
2342 },
2343 CursorInfo {
2344 position: 30,
2345 selection: Some(25..30),
2346 },
2347 ];
2348 }
2349
2350 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2351
2352 let cursors = api.get_all_cursors();
2353 assert_eq!(cursors.len(), 3);
2354 assert_eq!(cursors[0].position, 10);
2355 assert_eq!(cursors[0].selection, None);
2356 assert_eq!(cursors[1].position, 20);
2357 assert_eq!(cursors[1].selection, Some(15..20));
2358 assert_eq!(cursors[2].position, 30);
2359 assert_eq!(cursors[2].selection, Some(25..30));
2360 }
2361
2362 #[test]
2363 fn test_get_viewport() {
2364 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2365 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2366 let (tx, _rx) = std::sync::mpsc::channel();
2367 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2368
2369 {
2371 let mut snapshot = state_snapshot.write().unwrap();
2372 snapshot.viewport = Some(ViewportInfo {
2373 top_byte: 100,
2374 left_column: 5,
2375 width: 80,
2376 height: 24,
2377 });
2378 }
2379
2380 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2381
2382 let viewport = api.get_viewport();
2383 assert!(viewport.is_some());
2384 let viewport = viewport.unwrap();
2385 assert_eq!(viewport.top_byte, 100);
2386 assert_eq!(viewport.left_column, 5);
2387 assert_eq!(viewport.width, 80);
2388 assert_eq!(viewport.height, 24);
2389 }
2390
2391 #[test]
2392 fn test_composite_buffer_options_rejects_unknown_fields() {
2393 let valid_json = r#"{
2395 "name": "test",
2396 "mode": "diff",
2397 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2398 "sources": [{"bufferId": 1, "label": "old"}]
2399 }"#;
2400 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
2401 assert!(
2402 result.is_ok(),
2403 "Valid JSON should parse: {:?}",
2404 result.err()
2405 );
2406
2407 let invalid_json = r#"{
2409 "name": "test",
2410 "mode": "diff",
2411 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2412 "sources": [{"buffer_id": 1, "label": "old"}]
2413 }"#;
2414 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
2415 assert!(
2416 result.is_err(),
2417 "JSON with unknown field should fail to parse"
2418 );
2419 let err = result.unwrap_err().to_string();
2420 assert!(
2421 err.contains("unknown field") || err.contains("buffer_id"),
2422 "Error should mention unknown field: {}",
2423 err
2424 );
2425 }
2426
2427 #[test]
2428 fn test_composite_hunk_rejects_unknown_fields() {
2429 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
2431 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
2432 assert!(
2433 result.is_ok(),
2434 "Valid JSON should parse: {:?}",
2435 result.err()
2436 );
2437
2438 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
2440 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
2441 assert!(
2442 result.is_err(),
2443 "JSON with unknown field should fail to parse"
2444 );
2445 let err = result.unwrap_err().to_string();
2446 assert!(
2447 err.contains("unknown field") || err.contains("old_start"),
2448 "Error should mention unknown field: {}",
2449 err
2450 );
2451 }
2452}