1use crate::command::{Command, Suggestion};
7use crate::file_explorer::FileExplorerDecoration;
8use crate::hooks::{HookCallback, HookRegistry};
9use crate::menu::{Menu, MenuItem};
10use crate::overlay::{OverlayHandle, OverlayNamespace};
11use crate::text_property::{TextProperty, TextPropertyEntry};
12use crate::BufferId;
13use crate::SplitId;
14use lsp_types;
15use serde::{Deserialize, Serialize};
16use serde_json::Value as JsonValue;
17use std::collections::HashMap;
18use std::ops::Range;
19use std::path::PathBuf;
20use std::sync::{Arc, RwLock};
21use ts_rs::TS;
22
23pub struct CommandRegistry {
27 commands: std::sync::RwLock<Vec<Command>>,
28}
29
30impl CommandRegistry {
31 pub fn new() -> Self {
33 Self {
34 commands: std::sync::RwLock::new(Vec::new()),
35 }
36 }
37
38 pub fn register(&self, command: Command) {
40 let mut commands = self.commands.write().unwrap();
41 commands.retain(|c| c.name != command.name);
42 commands.push(command);
43 }
44
45 pub fn unregister(&self, name: &str) {
47 let mut commands = self.commands.write().unwrap();
48 commands.retain(|c| c.name != name);
49 }
50}
51
52impl Default for CommandRegistry {
53 fn default() -> Self {
54 Self::new()
55 }
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
64#[ts(export)]
65pub struct JsCallbackId(pub u64);
66
67impl JsCallbackId {
68 pub fn new(id: u64) -> Self {
70 Self(id)
71 }
72
73 pub fn as_u64(self) -> u64 {
75 self.0
76 }
77}
78
79impl From<u64> for JsCallbackId {
80 fn from(id: u64) -> Self {
81 Self(id)
82 }
83}
84
85impl From<JsCallbackId> for u64 {
86 fn from(id: JsCallbackId) -> u64 {
87 id.0
88 }
89}
90
91impl std::fmt::Display for JsCallbackId {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 write!(f, "{}", self.0)
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize, TS)]
99#[ts(export)]
100pub enum PluginResponse {
101 VirtualBufferCreated {
103 request_id: u64,
104 buffer_id: BufferId,
105 split_id: Option<SplitId>,
106 },
107 LspRequest {
109 request_id: u64,
110 #[ts(type = "any")]
111 result: Result<JsonValue, String>,
112 },
113 HighlightsComputed {
115 request_id: u64,
116 spans: Vec<TsHighlightSpan>,
117 },
118 BufferText {
120 request_id: u64,
121 text: Result<String, String>,
122 },
123 CompositeBufferCreated {
125 request_id: u64,
126 buffer_id: BufferId,
127 },
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize, TS)]
132#[ts(export)]
133pub enum PluginAsyncMessage {
134 ProcessOutput {
136 process_id: u64,
138 stdout: String,
140 stderr: String,
142 exit_code: i32,
144 },
145 DelayComplete {
147 callback_id: u64,
149 },
150 ProcessStdout { process_id: u64, data: String },
152 ProcessStderr { process_id: u64, data: String },
154 ProcessExit {
156 process_id: u64,
157 callback_id: u64,
158 exit_code: i32,
159 },
160 LspResponse {
162 language: String,
163 request_id: u64,
164 #[ts(type = "any")]
165 result: Result<JsonValue, String>,
166 },
167 PluginResponse(crate::api::PluginResponse),
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize, TS)]
173#[ts(export)]
174pub struct CursorInfo {
175 pub position: usize,
177 #[cfg_attr(
179 feature = "plugins",
180 ts(type = "{ start: number; end: number } | null")
181 )]
182 pub selection: Option<Range<usize>>,
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, TS)]
187#[ts(export)]
188pub struct ActionSpec {
189 pub action: String,
191 pub count: u32,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize, TS)]
197#[ts(export)]
198pub struct BufferInfo {
199 #[ts(type = "number")]
201 pub id: BufferId,
202 #[serde(serialize_with = "serialize_path")]
204 #[ts(type = "string")]
205 pub path: Option<PathBuf>,
206 pub modified: bool,
208 pub length: usize,
210}
211
212fn serialize_path<S: serde::Serializer>(path: &Option<PathBuf>, s: S) -> Result<S::Ok, S::Error> {
213 s.serialize_str(
214 &path
215 .as_ref()
216 .map(|p| p.to_string_lossy().to_string())
217 .unwrap_or_default(),
218 )
219}
220
221fn serialize_ranges_as_tuples<S>(ranges: &[Range<usize>], serializer: S) -> Result<S::Ok, S::Error>
223where
224 S: serde::Serializer,
225{
226 use serde::ser::SerializeSeq;
227 let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
228 for range in ranges {
229 seq.serialize_element(&(range.start, range.end))?;
230 }
231 seq.end()
232}
233
234fn serialize_opt_ranges_as_tuples<S>(
236 ranges: &Option<Vec<Range<usize>>>,
237 serializer: S,
238) -> Result<S::Ok, S::Error>
239where
240 S: serde::Serializer,
241{
242 match ranges {
243 Some(ranges) => {
244 use serde::ser::SerializeSeq;
245 let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
246 for range in ranges {
247 seq.serialize_element(&(range.start, range.end))?;
248 }
249 seq.end()
250 }
251 None => serializer.serialize_none(),
252 }
253}
254
255#[derive(Debug, Clone, Serialize, Deserialize, TS)]
257#[ts(export)]
258pub struct BufferSavedDiff {
259 pub equal: bool,
260 #[serde(serialize_with = "serialize_ranges_as_tuples")]
261 #[ts(type = "Array<[number, number]>")]
262 pub byte_ranges: Vec<Range<usize>>,
263 #[serde(serialize_with = "serialize_opt_ranges_as_tuples")]
264 #[ts(type = "Array<[number, number]> | null")]
265 pub line_ranges: Option<Vec<Range<usize>>>,
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize, TS)]
270#[ts(export)]
271pub struct ViewportInfo {
272 pub top_byte: usize,
274 pub left_column: usize,
276 pub width: u16,
278 pub height: u16,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize, TS)]
284#[ts(export)]
285pub struct LayoutHints {
286 pub compose_width: Option<u16>,
288 pub column_guides: Option<Vec<u16>>,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize, TS)]
298#[ts(export, rename = "TsCompositeLayoutConfig")]
299pub struct CompositeLayoutConfig {
300 #[serde(rename = "type")]
302 #[ts(rename = "type")]
303 pub layout_type: String,
304 #[serde(default)]
306 pub ratios: Option<Vec<f32>>,
307 #[serde(default = "default_true")]
309 pub show_separator: bool,
310 #[serde(default)]
312 pub spacing: Option<u16>,
313}
314
315fn default_true() -> bool {
316 true
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize, TS)]
321#[ts(export, rename = "TsCompositeSourceConfig")]
322pub struct CompositeSourceConfig {
323 pub buffer_id: usize,
325 pub label: String,
327 #[serde(default)]
329 pub editable: bool,
330 #[serde(default)]
332 pub style: Option<CompositePaneStyle>,
333}
334
335#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
337#[ts(export, rename = "TsCompositePaneStyle")]
338pub struct CompositePaneStyle {
339 #[serde(default)]
341 #[ts(type = "[number, number, number] | null")]
342 pub add_bg: Option<(u8, u8, u8)>,
343 #[serde(default)]
345 #[ts(type = "[number, number, number] | null")]
346 pub remove_bg: Option<(u8, u8, u8)>,
347 #[serde(default)]
349 #[ts(type = "[number, number, number] | null")]
350 pub modify_bg: Option<(u8, u8, u8)>,
351 #[serde(default)]
353 pub gutter_style: Option<String>,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize, TS)]
358#[ts(export, rename = "TsCompositeHunk")]
359pub struct CompositeHunk {
360 pub old_start: usize,
362 pub old_count: usize,
364 pub new_start: usize,
366 pub new_count: usize,
368}
369
370#[derive(Debug, Clone, Serialize, Deserialize, TS)]
372#[ts(export)]
373pub enum ViewTokenWireKind {
374 Text(String),
375 Newline,
376 Space,
377 Break,
380 BinaryByte(u8),
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
392#[ts(export)]
393pub struct ViewTokenStyle {
394 #[serde(default)]
396 #[ts(type = "[number, number, number] | null")]
397 pub fg: Option<(u8, u8, u8)>,
398 #[serde(default)]
400 #[ts(type = "[number, number, number] | null")]
401 pub bg: Option<(u8, u8, u8)>,
402 #[serde(default)]
404 pub bold: bool,
405 #[serde(default)]
407 pub italic: bool,
408}
409
410#[derive(Debug, Clone, Serialize, Deserialize, TS)]
412#[ts(export)]
413pub struct ViewTokenWire {
414 pub source_offset: Option<usize>,
416 pub kind: ViewTokenWireKind,
418 #[serde(default)]
420 pub style: Option<ViewTokenStyle>,
421}
422
423#[derive(Debug, Clone, Serialize, Deserialize, TS)]
425#[ts(export)]
426pub struct ViewTransformPayload {
427 pub range: Range<usize>,
429 pub tokens: Vec<ViewTokenWire>,
431 pub layout_hints: Option<LayoutHints>,
433}
434
435#[derive(Debug, Clone, Serialize, Deserialize, TS)]
438#[ts(export)]
439pub struct EditorStateSnapshot {
440 pub active_buffer_id: BufferId,
442 pub active_split_id: usize,
444 pub buffers: HashMap<BufferId, BufferInfo>,
446 pub buffer_saved_diffs: HashMap<BufferId, BufferSavedDiff>,
448 pub primary_cursor: Option<CursorInfo>,
450 pub all_cursors: Vec<CursorInfo>,
452 pub viewport: Option<ViewportInfo>,
454 pub buffer_cursor_positions: HashMap<BufferId, usize>,
456 pub buffer_text_properties: HashMap<BufferId, Vec<TextProperty>>,
458 pub selected_text: Option<String>,
461 pub clipboard: String,
463 pub working_dir: PathBuf,
465 #[ts(type = "any")]
468 pub diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
469 #[ts(type = "any")]
472 pub config: serde_json::Value,
473 #[ts(type = "any")]
476 pub user_config: serde_json::Value,
477 pub editor_mode: Option<String>,
480}
481
482impl EditorStateSnapshot {
483 pub fn new() -> Self {
484 Self {
485 active_buffer_id: BufferId(0),
486 active_split_id: 0,
487 buffers: HashMap::new(),
488 buffer_saved_diffs: HashMap::new(),
489 primary_cursor: None,
490 all_cursors: Vec::new(),
491 viewport: None,
492 buffer_cursor_positions: HashMap::new(),
493 buffer_text_properties: HashMap::new(),
494 selected_text: None,
495 clipboard: String::new(),
496 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
497 diagnostics: HashMap::new(),
498 config: serde_json::Value::Null,
499 user_config: serde_json::Value::Null,
500 editor_mode: None,
501 }
502 }
503}
504
505impl Default for EditorStateSnapshot {
506 fn default() -> Self {
507 Self::new()
508 }
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize, TS)]
513#[ts(export)]
514pub enum MenuPosition {
515 Top,
517 Bottom,
519 Before(String),
521 After(String),
523}
524
525#[derive(Debug, Clone, Serialize, Deserialize, TS)]
527#[ts(export)]
528pub enum PluginCommand {
529 InsertText {
531 buffer_id: BufferId,
532 position: usize,
533 text: String,
534 },
535
536 DeleteRange {
538 buffer_id: BufferId,
539 range: Range<usize>,
540 },
541
542 AddOverlay {
544 buffer_id: BufferId,
545 namespace: Option<OverlayNamespace>,
546 range: Range<usize>,
547 color: (u8, u8, u8),
548 bg_color: Option<(u8, u8, u8)>,
549 underline: bool,
550 bold: bool,
551 italic: bool,
552 extend_to_line_end: bool,
553 },
554
555 RemoveOverlay {
557 buffer_id: BufferId,
558 handle: OverlayHandle,
559 },
560
561 SetStatus { message: String },
563
564 ApplyTheme { theme_name: String },
566
567 ReloadConfig,
570
571 RegisterCommand { command: Command },
573
574 UnregisterCommand { name: String },
576
577 OpenFileInBackground { path: PathBuf },
579
580 InsertAtCursor { text: String },
582
583 SpawnProcess {
585 command: String,
586 args: Vec<String>,
587 cwd: Option<String>,
588 callback_id: JsCallbackId,
589 },
590
591 Delay {
593 callback_id: JsCallbackId,
594 duration_ms: u64,
595 },
596
597 SpawnBackgroundProcess {
601 process_id: u64,
603 command: String,
605 args: Vec<String>,
607 cwd: Option<String>,
609 callback_id: JsCallbackId,
611 },
612
613 KillBackgroundProcess { process_id: u64 },
615
616 SpawnProcessWait {
619 process_id: u64,
621 callback_id: JsCallbackId,
623 },
624
625 SetLayoutHints {
627 buffer_id: BufferId,
628 split_id: Option<SplitId>,
629 range: Range<usize>,
630 hints: LayoutHints,
631 },
632
633 SetLineNumbers { buffer_id: BufferId, enabled: bool },
635
636 SubmitViewTransform {
638 buffer_id: BufferId,
639 split_id: Option<SplitId>,
640 payload: ViewTransformPayload,
641 },
642
643 ClearViewTransform {
645 buffer_id: BufferId,
646 split_id: Option<SplitId>,
647 },
648
649 ClearAllOverlays { buffer_id: BufferId },
651
652 ClearNamespace {
654 buffer_id: BufferId,
655 namespace: OverlayNamespace,
656 },
657
658 ClearOverlaysInRange {
661 buffer_id: BufferId,
662 start: usize,
663 end: usize,
664 },
665
666 AddVirtualText {
669 buffer_id: BufferId,
670 virtual_text_id: String,
671 position: usize,
672 text: String,
673 color: (u8, u8, u8),
674 use_bg: bool, before: bool, },
677
678 RemoveVirtualText {
680 buffer_id: BufferId,
681 virtual_text_id: String,
682 },
683
684 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
686
687 ClearVirtualTexts { buffer_id: BufferId },
689
690 AddVirtualLine {
694 buffer_id: BufferId,
695 position: usize,
697 text: String,
699 fg_color: (u8, u8, u8),
701 bg_color: Option<(u8, u8, u8)>,
703 above: bool,
705 namespace: String,
707 priority: i32,
709 },
710
711 ClearVirtualTextNamespace {
714 buffer_id: BufferId,
715 namespace: String,
716 },
717
718 RefreshLines { buffer_id: BufferId },
720
721 SetLineIndicator {
724 buffer_id: BufferId,
725 line: usize,
727 namespace: String,
729 symbol: String,
731 color: (u8, u8, u8),
733 priority: i32,
735 },
736
737 ClearLineIndicators {
739 buffer_id: BufferId,
740 namespace: String,
742 },
743
744 SetFileExplorerDecorations {
746 namespace: String,
748 decorations: Vec<FileExplorerDecoration>,
750 },
751
752 ClearFileExplorerDecorations {
754 namespace: String,
756 },
757
758 OpenFileAtLocation {
761 path: PathBuf,
762 line: Option<usize>, column: Option<usize>, },
765
766 OpenFileInSplit {
769 split_id: usize,
770 path: PathBuf,
771 line: Option<usize>, column: Option<usize>, },
774
775 StartPrompt {
778 label: String,
779 prompt_type: String, },
781
782 StartPromptWithInitial {
784 label: String,
785 prompt_type: String,
786 initial_value: String,
787 },
788
789 SetPromptSuggestions { suggestions: Vec<Suggestion> },
792
793 AddMenuItem {
796 menu_label: String,
797 item: MenuItem,
798 position: MenuPosition,
799 },
800
801 AddMenu { menu: Menu, position: MenuPosition },
803
804 RemoveMenuItem {
806 menu_label: String,
807 item_label: String,
808 },
809
810 RemoveMenu { menu_label: String },
812
813 CreateVirtualBuffer {
815 name: String,
817 mode: String,
819 read_only: bool,
821 },
822
823 CreateVirtualBufferWithContent {
827 name: String,
829 mode: String,
831 read_only: bool,
833 entries: Vec<TextPropertyEntry>,
835 show_line_numbers: bool,
837 show_cursors: bool,
839 editing_disabled: bool,
841 hidden_from_tabs: bool,
843 request_id: Option<u64>,
845 },
846
847 CreateVirtualBufferInSplit {
850 name: String,
852 mode: String,
854 read_only: bool,
856 entries: Vec<TextPropertyEntry>,
858 ratio: f32,
860 direction: Option<String>,
862 panel_id: Option<String>,
864 show_line_numbers: bool,
866 show_cursors: bool,
868 editing_disabled: bool,
870 line_wrap: Option<bool>,
872 request_id: Option<u64>,
874 },
875
876 SetVirtualBufferContent {
878 buffer_id: BufferId,
879 entries: Vec<TextPropertyEntry>,
881 },
882
883 GetTextPropertiesAtCursor { buffer_id: BufferId },
885
886 DefineMode {
888 name: String,
889 parent: Option<String>,
890 bindings: Vec<(String, String)>, read_only: bool,
892 },
893
894 ShowBuffer { buffer_id: BufferId },
896
897 CreateVirtualBufferInExistingSplit {
899 name: String,
901 mode: String,
903 read_only: bool,
905 entries: Vec<TextPropertyEntry>,
907 split_id: SplitId,
909 show_line_numbers: bool,
911 show_cursors: bool,
913 editing_disabled: bool,
915 line_wrap: Option<bool>,
917 request_id: Option<u64>,
919 },
920
921 CloseBuffer { buffer_id: BufferId },
923
924 CreateCompositeBuffer {
927 name: String,
929 mode: String,
931 layout: CompositeLayoutConfig,
933 sources: Vec<CompositeSourceConfig>,
935 hunks: Option<Vec<CompositeHunk>>,
937 request_id: Option<u64>,
939 },
940
941 UpdateCompositeAlignment {
943 buffer_id: BufferId,
944 hunks: Vec<CompositeHunk>,
945 },
946
947 CloseCompositeBuffer { buffer_id: BufferId },
949
950 FocusSplit { split_id: SplitId },
952
953 SetSplitBuffer {
955 split_id: SplitId,
956 buffer_id: BufferId,
957 },
958
959 SetSplitScroll { split_id: SplitId, top_byte: usize },
961
962 RequestHighlights {
964 buffer_id: BufferId,
965 range: Range<usize>,
966 request_id: u64,
967 },
968
969 CloseSplit { split_id: SplitId },
971
972 SetSplitRatio {
974 split_id: SplitId,
975 ratio: f32,
977 },
978
979 DistributeSplitsEvenly {
981 split_ids: Vec<SplitId>,
983 },
984
985 SetBufferCursor {
987 buffer_id: BufferId,
988 position: usize,
990 },
991
992 SendLspRequest {
994 language: String,
995 method: String,
996 #[ts(type = "any")]
997 params: Option<JsonValue>,
998 request_id: u64,
999 },
1000
1001 SetClipboard { text: String },
1003
1004 DeleteSelection,
1007
1008 SetContext {
1012 name: String,
1014 active: bool,
1016 },
1017
1018 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1020
1021 ExecuteAction {
1024 action_name: String,
1026 },
1027
1028 ExecuteActions {
1032 actions: Vec<ActionSpec>,
1034 },
1035
1036 GetBufferText {
1038 buffer_id: BufferId,
1040 start: usize,
1042 end: usize,
1044 request_id: u64,
1046 },
1047
1048 SetEditorMode {
1051 mode: Option<String>,
1053 },
1054
1055 ShowActionPopup {
1058 popup_id: String,
1060 title: String,
1062 message: String,
1064 actions: Vec<ActionPopupAction>,
1066 },
1067
1068 DisableLspForLanguage {
1070 language: String,
1072 },
1073
1074 CreateScrollSyncGroup {
1078 group_id: u32,
1080 left_split: SplitId,
1082 right_split: SplitId,
1084 },
1085
1086 SetScrollSyncAnchors {
1089 group_id: u32,
1091 anchors: Vec<(usize, usize)>,
1093 },
1094
1095 RemoveScrollSyncGroup {
1097 group_id: u32,
1099 },
1100
1101 SaveBufferToPath {
1104 buffer_id: BufferId,
1106 path: PathBuf,
1108 },
1109}
1110
1111#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1113#[ts(export)]
1114pub enum HunkStatus {
1115 Pending,
1116 Staged,
1117 Discarded,
1118}
1119
1120#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1122#[ts(export)]
1123pub struct ReviewHunk {
1124 pub id: String,
1125 pub file: String,
1126 pub context_header: String,
1127 pub status: HunkStatus,
1128 pub base_range: Option<(usize, usize)>,
1130 pub modified_range: Option<(usize, usize)>,
1132}
1133
1134#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1136#[ts(export, rename = "TsActionPopupAction")]
1137pub struct ActionPopupAction {
1138 pub id: String,
1140 pub label: String,
1142}
1143
1144#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1146#[ts(export)]
1147pub struct TsHighlightSpan {
1148 pub start: u32,
1149 pub end: u32,
1150 #[ts(type = "[number, number, number]")]
1151 pub color: (u8, u8, u8),
1152 pub bold: bool,
1153 pub italic: bool,
1154}
1155
1156#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1158#[ts(export)]
1159pub struct SpawnResult {
1160 pub stdout: String,
1162 pub stderr: String,
1164 pub exit_code: i32,
1166}
1167
1168#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1170#[ts(export)]
1171pub struct BackgroundProcessResult {
1172 #[ts(type = "number")]
1174 pub process_id: u64,
1175 pub exit_code: i32,
1178}
1179
1180#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1182#[ts(export, rename = "TextPropertyEntry")]
1183pub struct JsTextPropertyEntry {
1184 pub text: String,
1186 #[serde(default)]
1188 #[ts(optional, type = "Record<string, unknown>")]
1189 pub properties: Option<HashMap<String, JsonValue>>,
1190}
1191
1192#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1194#[ts(export)]
1195pub struct CreateVirtualBufferOptions {
1196 pub name: String,
1198 #[serde(default)]
1200 #[ts(optional)]
1201 pub mode: Option<String>,
1202 #[serde(default, rename = "readOnly")]
1204 #[ts(optional, rename = "readOnly")]
1205 pub read_only: Option<bool>,
1206 #[serde(default, rename = "showLineNumbers")]
1208 #[ts(optional, rename = "showLineNumbers")]
1209 pub show_line_numbers: Option<bool>,
1210 #[serde(default, rename = "showCursors")]
1212 #[ts(optional, rename = "showCursors")]
1213 pub show_cursors: Option<bool>,
1214 #[serde(default, rename = "editingDisabled")]
1216 #[ts(optional, rename = "editingDisabled")]
1217 pub editing_disabled: Option<bool>,
1218 #[serde(default, rename = "hiddenFromTabs")]
1220 #[ts(optional, rename = "hiddenFromTabs")]
1221 pub hidden_from_tabs: Option<bool>,
1222 #[serde(default)]
1224 #[ts(optional)]
1225 pub entries: Option<Vec<JsTextPropertyEntry>>,
1226}
1227
1228#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1230#[ts(export)]
1231pub struct CreateVirtualBufferInSplitOptions {
1232 pub name: String,
1234 #[serde(default)]
1236 #[ts(optional)]
1237 pub mode: Option<String>,
1238 #[serde(default, rename = "readOnly")]
1240 #[ts(optional, rename = "readOnly")]
1241 pub read_only: Option<bool>,
1242 #[serde(default)]
1244 #[ts(optional)]
1245 pub ratio: Option<f32>,
1246 #[serde(default)]
1248 #[ts(optional)]
1249 pub direction: Option<String>,
1250 #[serde(default, rename = "panelId")]
1252 #[ts(optional, rename = "panelId")]
1253 pub panel_id: Option<String>,
1254 #[serde(default, rename = "showLineNumbers")]
1256 #[ts(optional, rename = "showLineNumbers")]
1257 pub show_line_numbers: Option<bool>,
1258 #[serde(default, rename = "showCursors")]
1260 #[ts(optional, rename = "showCursors")]
1261 pub show_cursors: Option<bool>,
1262 #[serde(default, rename = "editingDisabled")]
1264 #[ts(optional, rename = "editingDisabled")]
1265 pub editing_disabled: Option<bool>,
1266 #[serde(default, rename = "lineWrap")]
1268 #[ts(optional, rename = "lineWrap")]
1269 pub line_wrap: Option<bool>,
1270 #[serde(default)]
1272 #[ts(optional)]
1273 pub entries: Option<Vec<JsTextPropertyEntry>>,
1274}
1275
1276#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1278#[ts(export)]
1279pub struct CreateVirtualBufferInExistingSplitOptions {
1280 pub name: String,
1282 #[serde(rename = "splitId")]
1284 #[ts(rename = "splitId")]
1285 pub split_id: usize,
1286 #[serde(default)]
1288 #[ts(optional)]
1289 pub mode: Option<String>,
1290 #[serde(default, rename = "readOnly")]
1292 #[ts(optional, rename = "readOnly")]
1293 pub read_only: Option<bool>,
1294 #[serde(default, rename = "showLineNumbers")]
1296 #[ts(optional, rename = "showLineNumbers")]
1297 pub show_line_numbers: Option<bool>,
1298 #[serde(default, rename = "showCursors")]
1300 #[ts(optional, rename = "showCursors")]
1301 pub show_cursors: Option<bool>,
1302 #[serde(default, rename = "editingDisabled")]
1304 #[ts(optional, rename = "editingDisabled")]
1305 pub editing_disabled: Option<bool>,
1306 #[serde(default, rename = "lineWrap")]
1308 #[ts(optional, rename = "lineWrap")]
1309 pub line_wrap: Option<bool>,
1310 #[serde(default)]
1312 #[ts(optional)]
1313 pub entries: Option<Vec<JsTextPropertyEntry>>,
1314}
1315
1316#[derive(Debug, Clone, Serialize, TS)]
1321#[ts(export, type = "Array<Record<string, unknown>>")]
1322pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
1323
1324#[cfg(feature = "plugins")]
1326mod fromjs_impls {
1327 use super::*;
1328 use rquickjs::{Ctx, FromJs, Value};
1329
1330 impl<'js> FromJs<'js> for JsTextPropertyEntry {
1331 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1332 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1333 from: "object",
1334 to: "JsTextPropertyEntry",
1335 message: Some(e.to_string()),
1336 })
1337 }
1338 }
1339
1340 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
1341 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1342 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1343 from: "object",
1344 to: "CreateVirtualBufferOptions",
1345 message: Some(e.to_string()),
1346 })
1347 }
1348 }
1349
1350 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
1351 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1352 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1353 from: "object",
1354 to: "CreateVirtualBufferInSplitOptions",
1355 message: Some(e.to_string()),
1356 })
1357 }
1358 }
1359
1360 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
1361 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1362 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1363 from: "object",
1364 to: "CreateVirtualBufferInExistingSplitOptions",
1365 message: Some(e.to_string()),
1366 })
1367 }
1368 }
1369
1370 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
1371 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
1372 rquickjs_serde::to_value(ctx.clone(), &self.0)
1373 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
1374 }
1375 }
1376}
1377
1378pub struct PluginApi {
1380 hooks: Arc<RwLock<HookRegistry>>,
1382
1383 commands: Arc<RwLock<CommandRegistry>>,
1385
1386 command_sender: std::sync::mpsc::Sender<PluginCommand>,
1388
1389 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
1391}
1392
1393impl PluginApi {
1394 pub fn new(
1396 hooks: Arc<RwLock<HookRegistry>>,
1397 commands: Arc<RwLock<CommandRegistry>>,
1398 command_sender: std::sync::mpsc::Sender<PluginCommand>,
1399 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
1400 ) -> Self {
1401 Self {
1402 hooks,
1403 commands,
1404 command_sender,
1405 state_snapshot,
1406 }
1407 }
1408
1409 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
1411 let mut hooks = self.hooks.write().unwrap();
1412 hooks.add_hook(hook_name, callback);
1413 }
1414
1415 pub fn unregister_hooks(&self, hook_name: &str) {
1417 let mut hooks = self.hooks.write().unwrap();
1418 hooks.remove_hooks(hook_name);
1419 }
1420
1421 pub fn register_command(&self, command: Command) {
1423 let commands = self.commands.read().unwrap();
1424 commands.register(command);
1425 }
1426
1427 pub fn unregister_command(&self, name: &str) {
1429 let commands = self.commands.read().unwrap();
1430 commands.unregister(name);
1431 }
1432
1433 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
1435 self.command_sender
1436 .send(command)
1437 .map_err(|e| format!("Failed to send command: {}", e))
1438 }
1439
1440 pub fn insert_text(
1442 &self,
1443 buffer_id: BufferId,
1444 position: usize,
1445 text: String,
1446 ) -> Result<(), String> {
1447 self.send_command(PluginCommand::InsertText {
1448 buffer_id,
1449 position,
1450 text,
1451 })
1452 }
1453
1454 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
1456 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
1457 }
1458
1459 #[allow(clippy::too_many_arguments)]
1462 pub fn add_overlay(
1463 &self,
1464 buffer_id: BufferId,
1465 namespace: Option<String>,
1466 range: Range<usize>,
1467 color: (u8, u8, u8),
1468 bg_color: Option<(u8, u8, u8)>,
1469 underline: bool,
1470 bold: bool,
1471 italic: bool,
1472 extend_to_line_end: bool,
1473 ) -> Result<(), String> {
1474 self.send_command(PluginCommand::AddOverlay {
1475 buffer_id,
1476 namespace: namespace.map(OverlayNamespace::from_string),
1477 range,
1478 color,
1479 bg_color,
1480 underline,
1481 bold,
1482 italic,
1483 extend_to_line_end,
1484 })
1485 }
1486
1487 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
1489 self.send_command(PluginCommand::RemoveOverlay {
1490 buffer_id,
1491 handle: OverlayHandle::from_string(handle),
1492 })
1493 }
1494
1495 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
1497 self.send_command(PluginCommand::ClearNamespace {
1498 buffer_id,
1499 namespace: OverlayNamespace::from_string(namespace),
1500 })
1501 }
1502
1503 pub fn clear_overlays_in_range(
1506 &self,
1507 buffer_id: BufferId,
1508 start: usize,
1509 end: usize,
1510 ) -> Result<(), String> {
1511 self.send_command(PluginCommand::ClearOverlaysInRange {
1512 buffer_id,
1513 start,
1514 end,
1515 })
1516 }
1517
1518 pub fn set_status(&self, message: String) -> Result<(), String> {
1520 self.send_command(PluginCommand::SetStatus { message })
1521 }
1522
1523 pub fn open_file_at_location(
1526 &self,
1527 path: PathBuf,
1528 line: Option<usize>,
1529 column: Option<usize>,
1530 ) -> Result<(), String> {
1531 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
1532 }
1533
1534 pub fn open_file_in_split(
1539 &self,
1540 split_id: usize,
1541 path: PathBuf,
1542 line: Option<usize>,
1543 column: Option<usize>,
1544 ) -> Result<(), String> {
1545 self.send_command(PluginCommand::OpenFileInSplit {
1546 split_id,
1547 path,
1548 line,
1549 column,
1550 })
1551 }
1552
1553 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
1556 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
1557 }
1558
1559 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
1562 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
1563 }
1564
1565 pub fn add_menu_item(
1567 &self,
1568 menu_label: String,
1569 item: MenuItem,
1570 position: MenuPosition,
1571 ) -> Result<(), String> {
1572 self.send_command(PluginCommand::AddMenuItem {
1573 menu_label,
1574 item,
1575 position,
1576 })
1577 }
1578
1579 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
1581 self.send_command(PluginCommand::AddMenu { menu, position })
1582 }
1583
1584 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
1586 self.send_command(PluginCommand::RemoveMenuItem {
1587 menu_label,
1588 item_label,
1589 })
1590 }
1591
1592 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
1594 self.send_command(PluginCommand::RemoveMenu { menu_label })
1595 }
1596
1597 pub fn create_virtual_buffer(
1604 &self,
1605 name: String,
1606 mode: String,
1607 read_only: bool,
1608 ) -> Result<(), String> {
1609 self.send_command(PluginCommand::CreateVirtualBuffer {
1610 name,
1611 mode,
1612 read_only,
1613 })
1614 }
1615
1616 pub fn create_virtual_buffer_with_content(
1622 &self,
1623 name: String,
1624 mode: String,
1625 read_only: bool,
1626 entries: Vec<TextPropertyEntry>,
1627 ) -> Result<(), String> {
1628 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
1629 name,
1630 mode,
1631 read_only,
1632 entries,
1633 show_line_numbers: true,
1634 show_cursors: true,
1635 editing_disabled: false,
1636 hidden_from_tabs: false,
1637 request_id: None,
1638 })
1639 }
1640
1641 pub fn set_virtual_buffer_content(
1645 &self,
1646 buffer_id: BufferId,
1647 entries: Vec<TextPropertyEntry>,
1648 ) -> Result<(), String> {
1649 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
1650 }
1651
1652 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
1656 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
1657 }
1658
1659 pub fn define_mode(
1664 &self,
1665 name: String,
1666 parent: Option<String>,
1667 bindings: Vec<(String, String)>,
1668 read_only: bool,
1669 ) -> Result<(), String> {
1670 self.send_command(PluginCommand::DefineMode {
1671 name,
1672 parent,
1673 bindings,
1674 read_only,
1675 })
1676 }
1677
1678 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
1680 self.send_command(PluginCommand::ShowBuffer { buffer_id })
1681 }
1682
1683 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
1685 self.send_command(PluginCommand::SetSplitScroll {
1686 split_id: SplitId(split_id),
1687 top_byte,
1688 })
1689 }
1690
1691 pub fn get_highlights(
1693 &self,
1694 buffer_id: BufferId,
1695 range: Range<usize>,
1696 request_id: u64,
1697 ) -> Result<(), String> {
1698 self.send_command(PluginCommand::RequestHighlights {
1699 buffer_id,
1700 range,
1701 request_id,
1702 })
1703 }
1704
1705 pub fn get_active_buffer_id(&self) -> BufferId {
1709 let snapshot = self.state_snapshot.read().unwrap();
1710 snapshot.active_buffer_id
1711 }
1712
1713 pub fn get_active_split_id(&self) -> usize {
1715 let snapshot = self.state_snapshot.read().unwrap();
1716 snapshot.active_split_id
1717 }
1718
1719 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
1721 let snapshot = self.state_snapshot.read().unwrap();
1722 snapshot.buffers.get(&buffer_id).cloned()
1723 }
1724
1725 pub fn list_buffers(&self) -> Vec<BufferInfo> {
1727 let snapshot = self.state_snapshot.read().unwrap();
1728 snapshot.buffers.values().cloned().collect()
1729 }
1730
1731 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
1733 let snapshot = self.state_snapshot.read().unwrap();
1734 snapshot.primary_cursor.clone()
1735 }
1736
1737 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
1739 let snapshot = self.state_snapshot.read().unwrap();
1740 snapshot.all_cursors.clone()
1741 }
1742
1743 pub fn get_viewport(&self) -> Option<ViewportInfo> {
1745 let snapshot = self.state_snapshot.read().unwrap();
1746 snapshot.viewport.clone()
1747 }
1748
1749 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
1751 Arc::clone(&self.state_snapshot)
1752 }
1753}
1754
1755impl Clone for PluginApi {
1756 fn clone(&self) -> Self {
1757 Self {
1758 hooks: Arc::clone(&self.hooks),
1759 commands: Arc::clone(&self.commands),
1760 command_sender: self.command_sender.clone(),
1761 state_snapshot: Arc::clone(&self.state_snapshot),
1762 }
1763 }
1764}
1765
1766#[cfg(test)]
1767mod tests {
1768 use super::*;
1769
1770 #[test]
1771 fn test_plugin_api_creation() {
1772 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1773 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1774 let (tx, _rx) = std::sync::mpsc::channel();
1775 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
1776
1777 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
1778
1779 let _clone = api.clone();
1781 }
1782
1783 #[test]
1784 fn test_register_hook() {
1785 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1786 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1787 let (tx, _rx) = std::sync::mpsc::channel();
1788 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
1789
1790 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
1791
1792 api.register_hook("test-hook", Box::new(|_| true));
1793
1794 let hook_registry = hooks.read().unwrap();
1795 assert_eq!(hook_registry.hook_count("test-hook"), 1);
1796 }
1797
1798 #[test]
1799 fn test_send_command() {
1800 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1801 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1802 let (tx, rx) = std::sync::mpsc::channel();
1803 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
1804
1805 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
1806
1807 let result = api.insert_text(BufferId(1), 0, "test".to_string());
1808 assert!(result.is_ok());
1809
1810 let received = rx.try_recv();
1812 assert!(received.is_ok());
1813
1814 match received.unwrap() {
1815 PluginCommand::InsertText {
1816 buffer_id,
1817 position,
1818 text,
1819 } => {
1820 assert_eq!(buffer_id.0, 1);
1821 assert_eq!(position, 0);
1822 assert_eq!(text, "test");
1823 }
1824 _ => panic!("Wrong command type"),
1825 }
1826 }
1827
1828 #[test]
1829 fn test_add_overlay_command() {
1830 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1831 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1832 let (tx, rx) = std::sync::mpsc::channel();
1833 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
1834
1835 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
1836
1837 let result = api.add_overlay(
1838 BufferId(1),
1839 Some("test-overlay".to_string()),
1840 0..10,
1841 (255, 0, 0),
1842 None,
1843 true,
1844 false,
1845 false,
1846 false,
1847 );
1848 assert!(result.is_ok());
1849
1850 let received = rx.try_recv().unwrap();
1851 match received {
1852 PluginCommand::AddOverlay {
1853 buffer_id,
1854 namespace,
1855 range,
1856 color,
1857 bg_color,
1858 underline,
1859 bold,
1860 italic,
1861 extend_to_line_end,
1862 } => {
1863 assert_eq!(buffer_id.0, 1);
1864 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
1865 assert_eq!(range, 0..10);
1866 assert_eq!(color, (255, 0, 0));
1867 assert_eq!(bg_color, None);
1868 assert!(underline);
1869 assert!(!bold);
1870 assert!(!italic);
1871 assert!(!extend_to_line_end);
1872 }
1873 _ => panic!("Wrong command type"),
1874 }
1875 }
1876
1877 #[test]
1878 fn test_set_status_command() {
1879 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1880 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1881 let (tx, rx) = std::sync::mpsc::channel();
1882 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
1883
1884 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
1885
1886 let result = api.set_status("Test status".to_string());
1887 assert!(result.is_ok());
1888
1889 let received = rx.try_recv().unwrap();
1890 match received {
1891 PluginCommand::SetStatus { message } => {
1892 assert_eq!(message, "Test status");
1893 }
1894 _ => panic!("Wrong command type"),
1895 }
1896 }
1897
1898 #[test]
1899 fn test_get_active_buffer_id() {
1900 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1901 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1902 let (tx, _rx) = std::sync::mpsc::channel();
1903 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
1904
1905 {
1907 let mut snapshot = state_snapshot.write().unwrap();
1908 snapshot.active_buffer_id = BufferId(5);
1909 }
1910
1911 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
1912
1913 let active_id = api.get_active_buffer_id();
1914 assert_eq!(active_id.0, 5);
1915 }
1916
1917 #[test]
1918 fn test_get_buffer_info() {
1919 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1920 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1921 let (tx, _rx) = std::sync::mpsc::channel();
1922 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
1923
1924 {
1926 let mut snapshot = state_snapshot.write().unwrap();
1927 let buffer_info = BufferInfo {
1928 id: BufferId(1),
1929 path: Some(std::path::PathBuf::from("/test/file.txt")),
1930 modified: true,
1931 length: 100,
1932 };
1933 snapshot.buffers.insert(BufferId(1), buffer_info);
1934 }
1935
1936 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
1937
1938 let info = api.get_buffer_info(BufferId(1));
1939 assert!(info.is_some());
1940 let info = info.unwrap();
1941 assert_eq!(info.id.0, 1);
1942 assert_eq!(
1943 info.path.as_ref().unwrap().to_str().unwrap(),
1944 "/test/file.txt"
1945 );
1946 assert!(info.modified);
1947 assert_eq!(info.length, 100);
1948
1949 let no_info = api.get_buffer_info(BufferId(999));
1951 assert!(no_info.is_none());
1952 }
1953
1954 #[test]
1955 fn test_list_buffers() {
1956 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1957 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1958 let (tx, _rx) = std::sync::mpsc::channel();
1959 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
1960
1961 {
1963 let mut snapshot = state_snapshot.write().unwrap();
1964 snapshot.buffers.insert(
1965 BufferId(1),
1966 BufferInfo {
1967 id: BufferId(1),
1968 path: Some(std::path::PathBuf::from("/file1.txt")),
1969 modified: false,
1970 length: 50,
1971 },
1972 );
1973 snapshot.buffers.insert(
1974 BufferId(2),
1975 BufferInfo {
1976 id: BufferId(2),
1977 path: Some(std::path::PathBuf::from("/file2.txt")),
1978 modified: true,
1979 length: 100,
1980 },
1981 );
1982 snapshot.buffers.insert(
1983 BufferId(3),
1984 BufferInfo {
1985 id: BufferId(3),
1986 path: None,
1987 modified: false,
1988 length: 0,
1989 },
1990 );
1991 }
1992
1993 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
1994
1995 let buffers = api.list_buffers();
1996 assert_eq!(buffers.len(), 3);
1997
1998 assert!(buffers.iter().any(|b| b.id.0 == 1));
2000 assert!(buffers.iter().any(|b| b.id.0 == 2));
2001 assert!(buffers.iter().any(|b| b.id.0 == 3));
2002 }
2003
2004 #[test]
2005 fn test_get_primary_cursor() {
2006 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2007 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2008 let (tx, _rx) = std::sync::mpsc::channel();
2009 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2010
2011 {
2013 let mut snapshot = state_snapshot.write().unwrap();
2014 snapshot.primary_cursor = Some(CursorInfo {
2015 position: 42,
2016 selection: Some(10..42),
2017 });
2018 }
2019
2020 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2021
2022 let cursor = api.get_primary_cursor();
2023 assert!(cursor.is_some());
2024 let cursor = cursor.unwrap();
2025 assert_eq!(cursor.position, 42);
2026 assert_eq!(cursor.selection, Some(10..42));
2027 }
2028
2029 #[test]
2030 fn test_get_all_cursors() {
2031 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2032 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2033 let (tx, _rx) = std::sync::mpsc::channel();
2034 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2035
2036 {
2038 let mut snapshot = state_snapshot.write().unwrap();
2039 snapshot.all_cursors = vec![
2040 CursorInfo {
2041 position: 10,
2042 selection: None,
2043 },
2044 CursorInfo {
2045 position: 20,
2046 selection: Some(15..20),
2047 },
2048 CursorInfo {
2049 position: 30,
2050 selection: Some(25..30),
2051 },
2052 ];
2053 }
2054
2055 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2056
2057 let cursors = api.get_all_cursors();
2058 assert_eq!(cursors.len(), 3);
2059 assert_eq!(cursors[0].position, 10);
2060 assert_eq!(cursors[0].selection, None);
2061 assert_eq!(cursors[1].position, 20);
2062 assert_eq!(cursors[1].selection, Some(15..20));
2063 assert_eq!(cursors[2].position, 30);
2064 assert_eq!(cursors[2].selection, Some(25..30));
2065 }
2066
2067 #[test]
2068 fn test_get_viewport() {
2069 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2070 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2071 let (tx, _rx) = std::sync::mpsc::channel();
2072 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2073
2074 {
2076 let mut snapshot = state_snapshot.write().unwrap();
2077 snapshot.viewport = Some(ViewportInfo {
2078 top_byte: 100,
2079 left_column: 5,
2080 width: 80,
2081 height: 24,
2082 });
2083 }
2084
2085 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2086
2087 let viewport = api.get_viewport();
2088 assert!(viewport.is_some());
2089 let viewport = viewport.unwrap();
2090 assert_eq!(viewport.top_byte, 100);
2091 assert_eq!(viewport.left_column, 5);
2092 assert_eq!(viewport.width, 80);
2093 assert_eq!(viewport.height, 24);
2094 }
2095}