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
1102#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1104#[ts(export)]
1105pub enum HunkStatus {
1106 Pending,
1107 Staged,
1108 Discarded,
1109}
1110
1111#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1113#[ts(export)]
1114pub struct ReviewHunk {
1115 pub id: String,
1116 pub file: String,
1117 pub context_header: String,
1118 pub status: HunkStatus,
1119 pub base_range: Option<(usize, usize)>,
1121 pub modified_range: Option<(usize, usize)>,
1123}
1124
1125#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1127#[ts(export, rename = "TsActionPopupAction")]
1128pub struct ActionPopupAction {
1129 pub id: String,
1131 pub label: String,
1133}
1134
1135#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1137#[ts(export)]
1138pub struct TsHighlightSpan {
1139 pub start: u32,
1140 pub end: u32,
1141 #[ts(type = "[number, number, number]")]
1142 pub color: (u8, u8, u8),
1143 pub bold: bool,
1144 pub italic: bool,
1145}
1146
1147#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1149#[ts(export)]
1150pub struct SpawnResult {
1151 pub stdout: String,
1153 pub stderr: String,
1155 pub exit_code: i32,
1157}
1158
1159#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1161#[ts(export)]
1162pub struct BackgroundProcessResult {
1163 #[ts(type = "number")]
1165 pub process_id: u64,
1166 pub exit_code: i32,
1169}
1170
1171#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1173#[ts(export, rename = "TextPropertyEntry")]
1174pub struct JsTextPropertyEntry {
1175 pub text: String,
1177 #[serde(default)]
1179 #[ts(optional, type = "Record<string, unknown>")]
1180 pub properties: Option<HashMap<String, JsonValue>>,
1181}
1182
1183#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1185#[ts(export)]
1186pub struct CreateVirtualBufferOptions {
1187 pub name: String,
1189 #[serde(default)]
1191 #[ts(optional)]
1192 pub mode: Option<String>,
1193 #[serde(default, rename = "readOnly")]
1195 #[ts(optional, rename = "readOnly")]
1196 pub read_only: Option<bool>,
1197 #[serde(default, rename = "showLineNumbers")]
1199 #[ts(optional, rename = "showLineNumbers")]
1200 pub show_line_numbers: Option<bool>,
1201 #[serde(default, rename = "showCursors")]
1203 #[ts(optional, rename = "showCursors")]
1204 pub show_cursors: Option<bool>,
1205 #[serde(default, rename = "editingDisabled")]
1207 #[ts(optional, rename = "editingDisabled")]
1208 pub editing_disabled: Option<bool>,
1209 #[serde(default, rename = "hiddenFromTabs")]
1211 #[ts(optional, rename = "hiddenFromTabs")]
1212 pub hidden_from_tabs: Option<bool>,
1213 #[serde(default)]
1215 #[ts(optional)]
1216 pub entries: Option<Vec<JsTextPropertyEntry>>,
1217}
1218
1219#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1221#[ts(export)]
1222pub struct CreateVirtualBufferInSplitOptions {
1223 pub name: String,
1225 #[serde(default)]
1227 #[ts(optional)]
1228 pub mode: Option<String>,
1229 #[serde(default, rename = "readOnly")]
1231 #[ts(optional, rename = "readOnly")]
1232 pub read_only: Option<bool>,
1233 #[serde(default)]
1235 #[ts(optional)]
1236 pub ratio: Option<f32>,
1237 #[serde(default)]
1239 #[ts(optional)]
1240 pub direction: Option<String>,
1241 #[serde(default, rename = "panelId")]
1243 #[ts(optional, rename = "panelId")]
1244 pub panel_id: Option<String>,
1245 #[serde(default, rename = "showLineNumbers")]
1247 #[ts(optional, rename = "showLineNumbers")]
1248 pub show_line_numbers: Option<bool>,
1249 #[serde(default, rename = "showCursors")]
1251 #[ts(optional, rename = "showCursors")]
1252 pub show_cursors: Option<bool>,
1253 #[serde(default, rename = "editingDisabled")]
1255 #[ts(optional, rename = "editingDisabled")]
1256 pub editing_disabled: Option<bool>,
1257 #[serde(default, rename = "lineWrap")]
1259 #[ts(optional, rename = "lineWrap")]
1260 pub line_wrap: Option<bool>,
1261 #[serde(default)]
1263 #[ts(optional)]
1264 pub entries: Option<Vec<JsTextPropertyEntry>>,
1265}
1266
1267#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1269#[ts(export)]
1270pub struct CreateVirtualBufferInExistingSplitOptions {
1271 pub name: String,
1273 #[serde(rename = "splitId")]
1275 #[ts(rename = "splitId")]
1276 pub split_id: usize,
1277 #[serde(default)]
1279 #[ts(optional)]
1280 pub mode: Option<String>,
1281 #[serde(default, rename = "readOnly")]
1283 #[ts(optional, rename = "readOnly")]
1284 pub read_only: Option<bool>,
1285 #[serde(default, rename = "showLineNumbers")]
1287 #[ts(optional, rename = "showLineNumbers")]
1288 pub show_line_numbers: Option<bool>,
1289 #[serde(default, rename = "showCursors")]
1291 #[ts(optional, rename = "showCursors")]
1292 pub show_cursors: Option<bool>,
1293 #[serde(default, rename = "editingDisabled")]
1295 #[ts(optional, rename = "editingDisabled")]
1296 pub editing_disabled: Option<bool>,
1297 #[serde(default, rename = "lineWrap")]
1299 #[ts(optional, rename = "lineWrap")]
1300 pub line_wrap: Option<bool>,
1301 #[serde(default)]
1303 #[ts(optional)]
1304 pub entries: Option<Vec<JsTextPropertyEntry>>,
1305}
1306
1307#[derive(Debug, Clone, Serialize, TS)]
1312#[ts(export, type = "Array<Record<string, unknown>>")]
1313pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
1314
1315#[cfg(feature = "plugins")]
1317mod fromjs_impls {
1318 use super::*;
1319 use rquickjs::{Ctx, FromJs, Value};
1320
1321 impl<'js> FromJs<'js> for JsTextPropertyEntry {
1322 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1323 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1324 from: "object",
1325 to: "JsTextPropertyEntry",
1326 message: Some(e.to_string()),
1327 })
1328 }
1329 }
1330
1331 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
1332 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1333 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1334 from: "object",
1335 to: "CreateVirtualBufferOptions",
1336 message: Some(e.to_string()),
1337 })
1338 }
1339 }
1340
1341 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
1342 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1343 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1344 from: "object",
1345 to: "CreateVirtualBufferInSplitOptions",
1346 message: Some(e.to_string()),
1347 })
1348 }
1349 }
1350
1351 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
1352 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1353 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1354 from: "object",
1355 to: "CreateVirtualBufferInExistingSplitOptions",
1356 message: Some(e.to_string()),
1357 })
1358 }
1359 }
1360
1361 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
1362 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
1363 rquickjs_serde::to_value(ctx.clone(), &self.0)
1364 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
1365 }
1366 }
1367}
1368
1369pub struct PluginApi {
1371 hooks: Arc<RwLock<HookRegistry>>,
1373
1374 commands: Arc<RwLock<CommandRegistry>>,
1376
1377 command_sender: std::sync::mpsc::Sender<PluginCommand>,
1379
1380 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
1382}
1383
1384impl PluginApi {
1385 pub fn new(
1387 hooks: Arc<RwLock<HookRegistry>>,
1388 commands: Arc<RwLock<CommandRegistry>>,
1389 command_sender: std::sync::mpsc::Sender<PluginCommand>,
1390 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
1391 ) -> Self {
1392 Self {
1393 hooks,
1394 commands,
1395 command_sender,
1396 state_snapshot,
1397 }
1398 }
1399
1400 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
1402 let mut hooks = self.hooks.write().unwrap();
1403 hooks.add_hook(hook_name, callback);
1404 }
1405
1406 pub fn unregister_hooks(&self, hook_name: &str) {
1408 let mut hooks = self.hooks.write().unwrap();
1409 hooks.remove_hooks(hook_name);
1410 }
1411
1412 pub fn register_command(&self, command: Command) {
1414 let commands = self.commands.read().unwrap();
1415 commands.register(command);
1416 }
1417
1418 pub fn unregister_command(&self, name: &str) {
1420 let commands = self.commands.read().unwrap();
1421 commands.unregister(name);
1422 }
1423
1424 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
1426 self.command_sender
1427 .send(command)
1428 .map_err(|e| format!("Failed to send command: {}", e))
1429 }
1430
1431 pub fn insert_text(
1433 &self,
1434 buffer_id: BufferId,
1435 position: usize,
1436 text: String,
1437 ) -> Result<(), String> {
1438 self.send_command(PluginCommand::InsertText {
1439 buffer_id,
1440 position,
1441 text,
1442 })
1443 }
1444
1445 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
1447 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
1448 }
1449
1450 #[allow(clippy::too_many_arguments)]
1453 pub fn add_overlay(
1454 &self,
1455 buffer_id: BufferId,
1456 namespace: Option<String>,
1457 range: Range<usize>,
1458 color: (u8, u8, u8),
1459 bg_color: Option<(u8, u8, u8)>,
1460 underline: bool,
1461 bold: bool,
1462 italic: bool,
1463 extend_to_line_end: bool,
1464 ) -> Result<(), String> {
1465 self.send_command(PluginCommand::AddOverlay {
1466 buffer_id,
1467 namespace: namespace.map(OverlayNamespace::from_string),
1468 range,
1469 color,
1470 bg_color,
1471 underline,
1472 bold,
1473 italic,
1474 extend_to_line_end,
1475 })
1476 }
1477
1478 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
1480 self.send_command(PluginCommand::RemoveOverlay {
1481 buffer_id,
1482 handle: OverlayHandle::from_string(handle),
1483 })
1484 }
1485
1486 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
1488 self.send_command(PluginCommand::ClearNamespace {
1489 buffer_id,
1490 namespace: OverlayNamespace::from_string(namespace),
1491 })
1492 }
1493
1494 pub fn clear_overlays_in_range(
1497 &self,
1498 buffer_id: BufferId,
1499 start: usize,
1500 end: usize,
1501 ) -> Result<(), String> {
1502 self.send_command(PluginCommand::ClearOverlaysInRange {
1503 buffer_id,
1504 start,
1505 end,
1506 })
1507 }
1508
1509 pub fn set_status(&self, message: String) -> Result<(), String> {
1511 self.send_command(PluginCommand::SetStatus { message })
1512 }
1513
1514 pub fn open_file_at_location(
1517 &self,
1518 path: PathBuf,
1519 line: Option<usize>,
1520 column: Option<usize>,
1521 ) -> Result<(), String> {
1522 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
1523 }
1524
1525 pub fn open_file_in_split(
1530 &self,
1531 split_id: usize,
1532 path: PathBuf,
1533 line: Option<usize>,
1534 column: Option<usize>,
1535 ) -> Result<(), String> {
1536 self.send_command(PluginCommand::OpenFileInSplit {
1537 split_id,
1538 path,
1539 line,
1540 column,
1541 })
1542 }
1543
1544 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
1547 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
1548 }
1549
1550 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
1553 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
1554 }
1555
1556 pub fn add_menu_item(
1558 &self,
1559 menu_label: String,
1560 item: MenuItem,
1561 position: MenuPosition,
1562 ) -> Result<(), String> {
1563 self.send_command(PluginCommand::AddMenuItem {
1564 menu_label,
1565 item,
1566 position,
1567 })
1568 }
1569
1570 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
1572 self.send_command(PluginCommand::AddMenu { menu, position })
1573 }
1574
1575 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
1577 self.send_command(PluginCommand::RemoveMenuItem {
1578 menu_label,
1579 item_label,
1580 })
1581 }
1582
1583 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
1585 self.send_command(PluginCommand::RemoveMenu { menu_label })
1586 }
1587
1588 pub fn create_virtual_buffer(
1595 &self,
1596 name: String,
1597 mode: String,
1598 read_only: bool,
1599 ) -> Result<(), String> {
1600 self.send_command(PluginCommand::CreateVirtualBuffer {
1601 name,
1602 mode,
1603 read_only,
1604 })
1605 }
1606
1607 pub fn create_virtual_buffer_with_content(
1613 &self,
1614 name: String,
1615 mode: String,
1616 read_only: bool,
1617 entries: Vec<TextPropertyEntry>,
1618 ) -> Result<(), String> {
1619 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
1620 name,
1621 mode,
1622 read_only,
1623 entries,
1624 show_line_numbers: true,
1625 show_cursors: true,
1626 editing_disabled: false,
1627 hidden_from_tabs: false,
1628 request_id: None,
1629 })
1630 }
1631
1632 pub fn set_virtual_buffer_content(
1636 &self,
1637 buffer_id: BufferId,
1638 entries: Vec<TextPropertyEntry>,
1639 ) -> Result<(), String> {
1640 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
1641 }
1642
1643 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
1647 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
1648 }
1649
1650 pub fn define_mode(
1655 &self,
1656 name: String,
1657 parent: Option<String>,
1658 bindings: Vec<(String, String)>,
1659 read_only: bool,
1660 ) -> Result<(), String> {
1661 self.send_command(PluginCommand::DefineMode {
1662 name,
1663 parent,
1664 bindings,
1665 read_only,
1666 })
1667 }
1668
1669 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
1671 self.send_command(PluginCommand::ShowBuffer { buffer_id })
1672 }
1673
1674 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
1676 self.send_command(PluginCommand::SetSplitScroll {
1677 split_id: SplitId(split_id),
1678 top_byte,
1679 })
1680 }
1681
1682 pub fn get_highlights(
1684 &self,
1685 buffer_id: BufferId,
1686 range: Range<usize>,
1687 request_id: u64,
1688 ) -> Result<(), String> {
1689 self.send_command(PluginCommand::RequestHighlights {
1690 buffer_id,
1691 range,
1692 request_id,
1693 })
1694 }
1695
1696 pub fn get_active_buffer_id(&self) -> BufferId {
1700 let snapshot = self.state_snapshot.read().unwrap();
1701 snapshot.active_buffer_id
1702 }
1703
1704 pub fn get_active_split_id(&self) -> usize {
1706 let snapshot = self.state_snapshot.read().unwrap();
1707 snapshot.active_split_id
1708 }
1709
1710 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
1712 let snapshot = self.state_snapshot.read().unwrap();
1713 snapshot.buffers.get(&buffer_id).cloned()
1714 }
1715
1716 pub fn list_buffers(&self) -> Vec<BufferInfo> {
1718 let snapshot = self.state_snapshot.read().unwrap();
1719 snapshot.buffers.values().cloned().collect()
1720 }
1721
1722 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
1724 let snapshot = self.state_snapshot.read().unwrap();
1725 snapshot.primary_cursor.clone()
1726 }
1727
1728 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
1730 let snapshot = self.state_snapshot.read().unwrap();
1731 snapshot.all_cursors.clone()
1732 }
1733
1734 pub fn get_viewport(&self) -> Option<ViewportInfo> {
1736 let snapshot = self.state_snapshot.read().unwrap();
1737 snapshot.viewport.clone()
1738 }
1739
1740 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
1742 Arc::clone(&self.state_snapshot)
1743 }
1744}
1745
1746impl Clone for PluginApi {
1747 fn clone(&self) -> Self {
1748 Self {
1749 hooks: Arc::clone(&self.hooks),
1750 commands: Arc::clone(&self.commands),
1751 command_sender: self.command_sender.clone(),
1752 state_snapshot: Arc::clone(&self.state_snapshot),
1753 }
1754 }
1755}
1756
1757#[cfg(test)]
1758mod tests {
1759 use super::*;
1760
1761 #[test]
1762 fn test_plugin_api_creation() {
1763 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1764 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1765 let (tx, _rx) = std::sync::mpsc::channel();
1766 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
1767
1768 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
1769
1770 let _clone = api.clone();
1772 }
1773
1774 #[test]
1775 fn test_register_hook() {
1776 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1777 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1778 let (tx, _rx) = std::sync::mpsc::channel();
1779 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
1780
1781 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
1782
1783 api.register_hook("test-hook", Box::new(|_| true));
1784
1785 let hook_registry = hooks.read().unwrap();
1786 assert_eq!(hook_registry.hook_count("test-hook"), 1);
1787 }
1788
1789 #[test]
1790 fn test_send_command() {
1791 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1792 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1793 let (tx, rx) = std::sync::mpsc::channel();
1794 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
1795
1796 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
1797
1798 let result = api.insert_text(BufferId(1), 0, "test".to_string());
1799 assert!(result.is_ok());
1800
1801 let received = rx.try_recv();
1803 assert!(received.is_ok());
1804
1805 match received.unwrap() {
1806 PluginCommand::InsertText {
1807 buffer_id,
1808 position,
1809 text,
1810 } => {
1811 assert_eq!(buffer_id.0, 1);
1812 assert_eq!(position, 0);
1813 assert_eq!(text, "test");
1814 }
1815 _ => panic!("Wrong command type"),
1816 }
1817 }
1818
1819 #[test]
1820 fn test_add_overlay_command() {
1821 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1822 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1823 let (tx, rx) = std::sync::mpsc::channel();
1824 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
1825
1826 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
1827
1828 let result = api.add_overlay(
1829 BufferId(1),
1830 Some("test-overlay".to_string()),
1831 0..10,
1832 (255, 0, 0),
1833 None,
1834 true,
1835 false,
1836 false,
1837 false,
1838 );
1839 assert!(result.is_ok());
1840
1841 let received = rx.try_recv().unwrap();
1842 match received {
1843 PluginCommand::AddOverlay {
1844 buffer_id,
1845 namespace,
1846 range,
1847 color,
1848 bg_color,
1849 underline,
1850 bold,
1851 italic,
1852 extend_to_line_end,
1853 } => {
1854 assert_eq!(buffer_id.0, 1);
1855 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
1856 assert_eq!(range, 0..10);
1857 assert_eq!(color, (255, 0, 0));
1858 assert_eq!(bg_color, None);
1859 assert!(underline);
1860 assert!(!bold);
1861 assert!(!italic);
1862 assert!(!extend_to_line_end);
1863 }
1864 _ => panic!("Wrong command type"),
1865 }
1866 }
1867
1868 #[test]
1869 fn test_set_status_command() {
1870 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1871 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1872 let (tx, rx) = std::sync::mpsc::channel();
1873 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
1874
1875 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
1876
1877 let result = api.set_status("Test status".to_string());
1878 assert!(result.is_ok());
1879
1880 let received = rx.try_recv().unwrap();
1881 match received {
1882 PluginCommand::SetStatus { message } => {
1883 assert_eq!(message, "Test status");
1884 }
1885 _ => panic!("Wrong command type"),
1886 }
1887 }
1888
1889 #[test]
1890 fn test_get_active_buffer_id() {
1891 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1892 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1893 let (tx, _rx) = std::sync::mpsc::channel();
1894 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
1895
1896 {
1898 let mut snapshot = state_snapshot.write().unwrap();
1899 snapshot.active_buffer_id = BufferId(5);
1900 }
1901
1902 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
1903
1904 let active_id = api.get_active_buffer_id();
1905 assert_eq!(active_id.0, 5);
1906 }
1907
1908 #[test]
1909 fn test_get_buffer_info() {
1910 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1911 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1912 let (tx, _rx) = std::sync::mpsc::channel();
1913 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
1914
1915 {
1917 let mut snapshot = state_snapshot.write().unwrap();
1918 let buffer_info = BufferInfo {
1919 id: BufferId(1),
1920 path: Some(std::path::PathBuf::from("/test/file.txt")),
1921 modified: true,
1922 length: 100,
1923 };
1924 snapshot.buffers.insert(BufferId(1), buffer_info);
1925 }
1926
1927 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
1928
1929 let info = api.get_buffer_info(BufferId(1));
1930 assert!(info.is_some());
1931 let info = info.unwrap();
1932 assert_eq!(info.id.0, 1);
1933 assert_eq!(
1934 info.path.as_ref().unwrap().to_str().unwrap(),
1935 "/test/file.txt"
1936 );
1937 assert!(info.modified);
1938 assert_eq!(info.length, 100);
1939
1940 let no_info = api.get_buffer_info(BufferId(999));
1942 assert!(no_info.is_none());
1943 }
1944
1945 #[test]
1946 fn test_list_buffers() {
1947 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1948 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1949 let (tx, _rx) = std::sync::mpsc::channel();
1950 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
1951
1952 {
1954 let mut snapshot = state_snapshot.write().unwrap();
1955 snapshot.buffers.insert(
1956 BufferId(1),
1957 BufferInfo {
1958 id: BufferId(1),
1959 path: Some(std::path::PathBuf::from("/file1.txt")),
1960 modified: false,
1961 length: 50,
1962 },
1963 );
1964 snapshot.buffers.insert(
1965 BufferId(2),
1966 BufferInfo {
1967 id: BufferId(2),
1968 path: Some(std::path::PathBuf::from("/file2.txt")),
1969 modified: true,
1970 length: 100,
1971 },
1972 );
1973 snapshot.buffers.insert(
1974 BufferId(3),
1975 BufferInfo {
1976 id: BufferId(3),
1977 path: None,
1978 modified: false,
1979 length: 0,
1980 },
1981 );
1982 }
1983
1984 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
1985
1986 let buffers = api.list_buffers();
1987 assert_eq!(buffers.len(), 3);
1988
1989 assert!(buffers.iter().any(|b| b.id.0 == 1));
1991 assert!(buffers.iter().any(|b| b.id.0 == 2));
1992 assert!(buffers.iter().any(|b| b.id.0 == 3));
1993 }
1994
1995 #[test]
1996 fn test_get_primary_cursor() {
1997 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
1998 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
1999 let (tx, _rx) = std::sync::mpsc::channel();
2000 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2001
2002 {
2004 let mut snapshot = state_snapshot.write().unwrap();
2005 snapshot.primary_cursor = Some(CursorInfo {
2006 position: 42,
2007 selection: Some(10..42),
2008 });
2009 }
2010
2011 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2012
2013 let cursor = api.get_primary_cursor();
2014 assert!(cursor.is_some());
2015 let cursor = cursor.unwrap();
2016 assert_eq!(cursor.position, 42);
2017 assert_eq!(cursor.selection, Some(10..42));
2018 }
2019
2020 #[test]
2021 fn test_get_all_cursors() {
2022 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2023 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2024 let (tx, _rx) = std::sync::mpsc::channel();
2025 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2026
2027 {
2029 let mut snapshot = state_snapshot.write().unwrap();
2030 snapshot.all_cursors = vec![
2031 CursorInfo {
2032 position: 10,
2033 selection: None,
2034 },
2035 CursorInfo {
2036 position: 20,
2037 selection: Some(15..20),
2038 },
2039 CursorInfo {
2040 position: 30,
2041 selection: Some(25..30),
2042 },
2043 ];
2044 }
2045
2046 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2047
2048 let cursors = api.get_all_cursors();
2049 assert_eq!(cursors.len(), 3);
2050 assert_eq!(cursors[0].position, 10);
2051 assert_eq!(cursors[0].selection, None);
2052 assert_eq!(cursors[1].position, 20);
2053 assert_eq!(cursors[1].selection, Some(15..20));
2054 assert_eq!(cursors[2].position, 30);
2055 assert_eq!(cursors[2].selection, Some(25..30));
2056 }
2057
2058 #[test]
2059 fn test_get_viewport() {
2060 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2061 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2062 let (tx, _rx) = std::sync::mpsc::channel();
2063 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2064
2065 {
2067 let mut snapshot = state_snapshot.write().unwrap();
2068 snapshot.viewport = Some(ViewportInfo {
2069 top_byte: 100,
2070 left_column: 5,
2071 width: 80,
2072 height: 24,
2073 });
2074 }
2075
2076 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2077
2078 let viewport = api.get_viewport();
2079 assert!(viewport.is_some());
2080 let viewport = viewport.unwrap();
2081 assert_eq!(viewport.top_byte, 100);
2082 assert_eq!(viewport.left_column, 5);
2083 assert_eq!(viewport.width, 80);
2084 assert_eq!(viewport.height, 24);
2085 }
2086}