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)]
374#[serde(untagged)]
375#[ts(export)]
376pub enum OverlayColorSpec {
377 #[ts(type = "[number, number, number]")]
379 Rgb(u8, u8, u8),
380 ThemeKey(String),
382}
383
384impl OverlayColorSpec {
385 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
387 Self::Rgb(r, g, b)
388 }
389
390 pub fn theme_key(key: impl Into<String>) -> Self {
392 Self::ThemeKey(key.into())
393 }
394
395 pub fn as_rgb(&self) -> Option<(u8, u8, u8)> {
397 match self {
398 Self::Rgb(r, g, b) => Some((*r, *g, *b)),
399 Self::ThemeKey(_) => None,
400 }
401 }
402
403 pub fn as_theme_key(&self) -> Option<&str> {
405 match self {
406 Self::ThemeKey(key) => Some(key),
407 Self::Rgb(_, _, _) => None,
408 }
409 }
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize, TS)]
417#[serde(deny_unknown_fields, rename_all = "camelCase")]
418#[ts(export, rename_all = "camelCase")]
419pub struct OverlayOptions {
420 #[serde(default, skip_serializing_if = "Option::is_none")]
422 pub fg: Option<OverlayColorSpec>,
423
424 #[serde(default, skip_serializing_if = "Option::is_none")]
426 pub bg: Option<OverlayColorSpec>,
427
428 #[serde(default)]
430 pub underline: bool,
431
432 #[serde(default)]
434 pub bold: bool,
435
436 #[serde(default)]
438 pub italic: bool,
439
440 #[serde(default)]
442 pub extend_to_line_end: bool,
443}
444
445impl Default for OverlayOptions {
446 fn default() -> Self {
447 Self {
448 fg: None,
449 bg: None,
450 underline: false,
451 bold: false,
452 italic: false,
453 extend_to_line_end: false,
454 }
455 }
456}
457
458#[derive(Debug, Clone, Serialize, Deserialize, TS)]
464#[serde(deny_unknown_fields)]
465#[ts(export, rename = "TsCompositeLayoutConfig")]
466pub struct CompositeLayoutConfig {
467 #[serde(rename = "type")]
469 #[ts(rename = "type")]
470 pub layout_type: String,
471 #[serde(default)]
473 pub ratios: Option<Vec<f32>>,
474 #[serde(default = "default_true", rename = "showSeparator")]
476 #[ts(rename = "showSeparator")]
477 pub show_separator: bool,
478 #[serde(default)]
480 pub spacing: Option<u16>,
481}
482
483fn default_true() -> bool {
484 true
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize, TS)]
489#[serde(deny_unknown_fields)]
490#[ts(export, rename = "TsCompositeSourceConfig")]
491pub struct CompositeSourceConfig {
492 #[serde(rename = "bufferId")]
494 #[ts(rename = "bufferId")]
495 pub buffer_id: usize,
496 pub label: String,
498 #[serde(default)]
500 pub editable: bool,
501 #[serde(default)]
503 pub style: Option<CompositePaneStyle>,
504}
505
506#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
508#[serde(deny_unknown_fields)]
509#[ts(export, rename = "TsCompositePaneStyle")]
510pub struct CompositePaneStyle {
511 #[serde(default, rename = "addBg")]
514 #[ts(rename = "addBg", type = "[number, number, number] | null")]
515 pub add_bg: Option<[u8; 3]>,
516 #[serde(default, rename = "removeBg")]
518 #[ts(rename = "removeBg", type = "[number, number, number] | null")]
519 pub remove_bg: Option<[u8; 3]>,
520 #[serde(default, rename = "modifyBg")]
522 #[ts(rename = "modifyBg", type = "[number, number, number] | null")]
523 pub modify_bg: Option<[u8; 3]>,
524 #[serde(default, rename = "gutterStyle")]
526 #[ts(rename = "gutterStyle")]
527 pub gutter_style: Option<String>,
528}
529
530#[derive(Debug, Clone, Serialize, Deserialize, TS)]
532#[serde(deny_unknown_fields)]
533#[ts(export, rename = "TsCompositeHunk")]
534pub struct CompositeHunk {
535 #[serde(rename = "oldStart")]
537 #[ts(rename = "oldStart")]
538 pub old_start: usize,
539 #[serde(rename = "oldCount")]
541 #[ts(rename = "oldCount")]
542 pub old_count: usize,
543 #[serde(rename = "newStart")]
545 #[ts(rename = "newStart")]
546 pub new_start: usize,
547 #[serde(rename = "newCount")]
549 #[ts(rename = "newCount")]
550 pub new_count: usize,
551}
552
553#[derive(Debug, Clone, Serialize, Deserialize, TS)]
555#[serde(deny_unknown_fields)]
556#[ts(export, rename = "TsCreateCompositeBufferOptions")]
557pub struct CreateCompositeBufferOptions {
558 #[serde(default)]
560 pub name: String,
561 #[serde(default)]
563 pub mode: String,
564 pub layout: CompositeLayoutConfig,
566 pub sources: Vec<CompositeSourceConfig>,
568 #[serde(default)]
570 pub hunks: Option<Vec<CompositeHunk>>,
571}
572
573#[derive(Debug, Clone, Serialize, Deserialize, TS)]
575#[ts(export)]
576pub enum ViewTokenWireKind {
577 Text(String),
578 Newline,
579 Space,
580 Break,
583 BinaryByte(u8),
587}
588
589#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
595#[serde(deny_unknown_fields)]
596#[ts(export)]
597pub struct ViewTokenStyle {
598 #[serde(default)]
600 #[ts(type = "[number, number, number] | null")]
601 pub fg: Option<(u8, u8, u8)>,
602 #[serde(default)]
604 #[ts(type = "[number, number, number] | null")]
605 pub bg: Option<(u8, u8, u8)>,
606 #[serde(default)]
608 pub bold: bool,
609 #[serde(default)]
611 pub italic: bool,
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize, TS)]
616#[serde(deny_unknown_fields)]
617#[ts(export)]
618pub struct ViewTokenWire {
619 #[ts(type = "number | null")]
621 pub source_offset: Option<usize>,
622 pub kind: ViewTokenWireKind,
624 #[serde(default)]
626 #[ts(optional)]
627 pub style: Option<ViewTokenStyle>,
628}
629
630#[derive(Debug, Clone, Serialize, Deserialize, TS)]
632#[ts(export)]
633pub struct ViewTransformPayload {
634 pub range: Range<usize>,
636 pub tokens: Vec<ViewTokenWire>,
638 pub layout_hints: Option<LayoutHints>,
640}
641
642#[derive(Debug, Clone, Serialize, Deserialize, TS)]
645#[ts(export)]
646pub struct EditorStateSnapshot {
647 pub active_buffer_id: BufferId,
649 pub active_split_id: usize,
651 pub buffers: HashMap<BufferId, BufferInfo>,
653 pub buffer_saved_diffs: HashMap<BufferId, BufferSavedDiff>,
655 pub primary_cursor: Option<CursorInfo>,
657 pub all_cursors: Vec<CursorInfo>,
659 pub viewport: Option<ViewportInfo>,
661 pub buffer_cursor_positions: HashMap<BufferId, usize>,
663 pub buffer_text_properties: HashMap<BufferId, Vec<TextProperty>>,
665 pub selected_text: Option<String>,
668 pub clipboard: String,
670 pub working_dir: PathBuf,
672 #[ts(type = "any")]
675 pub diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
676 #[ts(type = "any")]
679 pub config: serde_json::Value,
680 #[ts(type = "any")]
683 pub user_config: serde_json::Value,
684 pub editor_mode: Option<String>,
687}
688
689impl EditorStateSnapshot {
690 pub fn new() -> Self {
691 Self {
692 active_buffer_id: BufferId(0),
693 active_split_id: 0,
694 buffers: HashMap::new(),
695 buffer_saved_diffs: HashMap::new(),
696 primary_cursor: None,
697 all_cursors: Vec::new(),
698 viewport: None,
699 buffer_cursor_positions: HashMap::new(),
700 buffer_text_properties: HashMap::new(),
701 selected_text: None,
702 clipboard: String::new(),
703 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
704 diagnostics: HashMap::new(),
705 config: serde_json::Value::Null,
706 user_config: serde_json::Value::Null,
707 editor_mode: None,
708 }
709 }
710}
711
712impl Default for EditorStateSnapshot {
713 fn default() -> Self {
714 Self::new()
715 }
716}
717
718#[derive(Debug, Clone, Serialize, Deserialize, TS)]
720#[ts(export)]
721pub enum MenuPosition {
722 Top,
724 Bottom,
726 Before(String),
728 After(String),
730}
731
732#[derive(Debug, Clone, Serialize, Deserialize, TS)]
734#[ts(export)]
735pub enum PluginCommand {
736 InsertText {
738 buffer_id: BufferId,
739 position: usize,
740 text: String,
741 },
742
743 DeleteRange {
745 buffer_id: BufferId,
746 range: Range<usize>,
747 },
748
749 AddOverlay {
754 buffer_id: BufferId,
755 namespace: Option<OverlayNamespace>,
756 range: Range<usize>,
757 options: OverlayOptions,
759 },
760
761 RemoveOverlay {
763 buffer_id: BufferId,
764 handle: OverlayHandle,
765 },
766
767 SetStatus { message: String },
769
770 ApplyTheme { theme_name: String },
772
773 ReloadConfig,
776
777 RegisterCommand { command: Command },
779
780 UnregisterCommand { name: String },
782
783 OpenFileInBackground { path: PathBuf },
785
786 InsertAtCursor { text: String },
788
789 SpawnProcess {
791 command: String,
792 args: Vec<String>,
793 cwd: Option<String>,
794 callback_id: JsCallbackId,
795 },
796
797 Delay {
799 callback_id: JsCallbackId,
800 duration_ms: u64,
801 },
802
803 SpawnBackgroundProcess {
807 process_id: u64,
809 command: String,
811 args: Vec<String>,
813 cwd: Option<String>,
815 callback_id: JsCallbackId,
817 },
818
819 KillBackgroundProcess { process_id: u64 },
821
822 SpawnProcessWait {
825 process_id: u64,
827 callback_id: JsCallbackId,
829 },
830
831 SetLayoutHints {
833 buffer_id: BufferId,
834 split_id: Option<SplitId>,
835 range: Range<usize>,
836 hints: LayoutHints,
837 },
838
839 SetLineNumbers { buffer_id: BufferId, enabled: bool },
841
842 SetLineWrap {
844 buffer_id: BufferId,
845 split_id: Option<SplitId>,
846 enabled: bool,
847 },
848
849 SubmitViewTransform {
851 buffer_id: BufferId,
852 split_id: Option<SplitId>,
853 payload: ViewTransformPayload,
854 },
855
856 ClearViewTransform {
858 buffer_id: BufferId,
859 split_id: Option<SplitId>,
860 },
861
862 ClearAllOverlays { buffer_id: BufferId },
864
865 ClearNamespace {
867 buffer_id: BufferId,
868 namespace: OverlayNamespace,
869 },
870
871 ClearOverlaysInRange {
874 buffer_id: BufferId,
875 start: usize,
876 end: usize,
877 },
878
879 AddVirtualText {
882 buffer_id: BufferId,
883 virtual_text_id: String,
884 position: usize,
885 text: String,
886 color: (u8, u8, u8),
887 use_bg: bool, before: bool, },
890
891 RemoveVirtualText {
893 buffer_id: BufferId,
894 virtual_text_id: String,
895 },
896
897 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
899
900 ClearVirtualTexts { buffer_id: BufferId },
902
903 AddVirtualLine {
907 buffer_id: BufferId,
908 position: usize,
910 text: String,
912 fg_color: (u8, u8, u8),
914 bg_color: Option<(u8, u8, u8)>,
916 above: bool,
918 namespace: String,
920 priority: i32,
922 },
923
924 ClearVirtualTextNamespace {
927 buffer_id: BufferId,
928 namespace: String,
929 },
930
931 RefreshLines { buffer_id: BufferId },
933
934 SetLineIndicator {
937 buffer_id: BufferId,
938 line: usize,
940 namespace: String,
942 symbol: String,
944 color: (u8, u8, u8),
946 priority: i32,
948 },
949
950 ClearLineIndicators {
952 buffer_id: BufferId,
953 namespace: String,
955 },
956
957 SetFileExplorerDecorations {
959 namespace: String,
961 decorations: Vec<FileExplorerDecoration>,
963 },
964
965 ClearFileExplorerDecorations {
967 namespace: String,
969 },
970
971 OpenFileAtLocation {
974 path: PathBuf,
975 line: Option<usize>, column: Option<usize>, },
978
979 OpenFileInSplit {
982 split_id: usize,
983 path: PathBuf,
984 line: Option<usize>, column: Option<usize>, },
987
988 StartPrompt {
991 label: String,
992 prompt_type: String, },
994
995 StartPromptWithInitial {
997 label: String,
998 prompt_type: String,
999 initial_value: String,
1000 },
1001
1002 StartPromptAsync {
1005 label: String,
1006 initial_value: String,
1007 callback_id: JsCallbackId,
1008 },
1009
1010 SetPromptSuggestions { suggestions: Vec<Suggestion> },
1013
1014 AddMenuItem {
1017 menu_label: String,
1018 item: MenuItem,
1019 position: MenuPosition,
1020 },
1021
1022 AddMenu { menu: Menu, position: MenuPosition },
1024
1025 RemoveMenuItem {
1027 menu_label: String,
1028 item_label: String,
1029 },
1030
1031 RemoveMenu { menu_label: String },
1033
1034 CreateVirtualBuffer {
1036 name: String,
1038 mode: String,
1040 read_only: bool,
1042 },
1043
1044 CreateVirtualBufferWithContent {
1048 name: String,
1050 mode: String,
1052 read_only: bool,
1054 entries: Vec<TextPropertyEntry>,
1056 show_line_numbers: bool,
1058 show_cursors: bool,
1060 editing_disabled: bool,
1062 hidden_from_tabs: bool,
1064 request_id: Option<u64>,
1066 },
1067
1068 CreateVirtualBufferInSplit {
1071 name: String,
1073 mode: String,
1075 read_only: bool,
1077 entries: Vec<TextPropertyEntry>,
1079 ratio: f32,
1081 direction: Option<String>,
1083 panel_id: Option<String>,
1085 show_line_numbers: bool,
1087 show_cursors: bool,
1089 editing_disabled: bool,
1091 line_wrap: Option<bool>,
1093 request_id: Option<u64>,
1095 },
1096
1097 SetVirtualBufferContent {
1099 buffer_id: BufferId,
1100 entries: Vec<TextPropertyEntry>,
1102 },
1103
1104 GetTextPropertiesAtCursor { buffer_id: BufferId },
1106
1107 DefineMode {
1109 name: String,
1110 parent: Option<String>,
1111 bindings: Vec<(String, String)>, read_only: bool,
1113 },
1114
1115 ShowBuffer { buffer_id: BufferId },
1117
1118 CreateVirtualBufferInExistingSplit {
1120 name: String,
1122 mode: String,
1124 read_only: bool,
1126 entries: Vec<TextPropertyEntry>,
1128 split_id: SplitId,
1130 show_line_numbers: bool,
1132 show_cursors: bool,
1134 editing_disabled: bool,
1136 line_wrap: Option<bool>,
1138 request_id: Option<u64>,
1140 },
1141
1142 CloseBuffer { buffer_id: BufferId },
1144
1145 CreateCompositeBuffer {
1148 name: String,
1150 mode: String,
1152 layout: CompositeLayoutConfig,
1154 sources: Vec<CompositeSourceConfig>,
1156 hunks: Option<Vec<CompositeHunk>>,
1158 request_id: Option<u64>,
1160 },
1161
1162 UpdateCompositeAlignment {
1164 buffer_id: BufferId,
1165 hunks: Vec<CompositeHunk>,
1166 },
1167
1168 CloseCompositeBuffer { buffer_id: BufferId },
1170
1171 FocusSplit { split_id: SplitId },
1173
1174 SetSplitBuffer {
1176 split_id: SplitId,
1177 buffer_id: BufferId,
1178 },
1179
1180 SetSplitScroll { split_id: SplitId, top_byte: usize },
1182
1183 RequestHighlights {
1185 buffer_id: BufferId,
1186 range: Range<usize>,
1187 request_id: u64,
1188 },
1189
1190 CloseSplit { split_id: SplitId },
1192
1193 SetSplitRatio {
1195 split_id: SplitId,
1196 ratio: f32,
1198 },
1199
1200 DistributeSplitsEvenly {
1202 split_ids: Vec<SplitId>,
1204 },
1205
1206 SetBufferCursor {
1208 buffer_id: BufferId,
1209 position: usize,
1211 },
1212
1213 SendLspRequest {
1215 language: String,
1216 method: String,
1217 #[ts(type = "any")]
1218 params: Option<JsonValue>,
1219 request_id: u64,
1220 },
1221
1222 SetClipboard { text: String },
1224
1225 DeleteSelection,
1228
1229 SetContext {
1233 name: String,
1235 active: bool,
1237 },
1238
1239 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1241
1242 ExecuteAction {
1245 action_name: String,
1247 },
1248
1249 ExecuteActions {
1253 actions: Vec<ActionSpec>,
1255 },
1256
1257 GetBufferText {
1259 buffer_id: BufferId,
1261 start: usize,
1263 end: usize,
1265 request_id: u64,
1267 },
1268
1269 GetLineStartPosition {
1272 buffer_id: BufferId,
1274 line: u32,
1276 request_id: u64,
1278 },
1279
1280 SetEditorMode {
1283 mode: Option<String>,
1285 },
1286
1287 ShowActionPopup {
1290 popup_id: String,
1292 title: String,
1294 message: String,
1296 actions: Vec<ActionPopupAction>,
1298 },
1299
1300 DisableLspForLanguage {
1302 language: String,
1304 },
1305
1306 SetLspRootUri {
1310 language: String,
1312 uri: String,
1314 },
1315
1316 CreateScrollSyncGroup {
1320 group_id: u32,
1322 left_split: SplitId,
1324 right_split: SplitId,
1326 },
1327
1328 SetScrollSyncAnchors {
1331 group_id: u32,
1333 anchors: Vec<(usize, usize)>,
1335 },
1336
1337 RemoveScrollSyncGroup {
1339 group_id: u32,
1341 },
1342
1343 SaveBufferToPath {
1346 buffer_id: BufferId,
1348 path: PathBuf,
1350 },
1351
1352 LoadPlugin {
1355 path: PathBuf,
1357 callback_id: JsCallbackId,
1359 },
1360
1361 UnloadPlugin {
1364 name: String,
1366 callback_id: JsCallbackId,
1368 },
1369
1370 ReloadPlugin {
1373 name: String,
1375 callback_id: JsCallbackId,
1377 },
1378
1379 ListPlugins {
1382 callback_id: JsCallbackId,
1384 },
1385
1386 ReloadThemes,
1389
1390 RegisterGrammar {
1393 language: String,
1395 grammar_path: String,
1397 extensions: Vec<String>,
1399 },
1400
1401 RegisterLanguageConfig {
1404 language: String,
1406 config: LanguagePackConfig,
1408 },
1409
1410 RegisterLspServer {
1413 language: String,
1415 config: LspServerPackConfig,
1417 },
1418
1419 ReloadGrammars,
1422}
1423
1424#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
1433#[serde(rename_all = "camelCase")]
1434#[ts(export)]
1435pub struct LanguagePackConfig {
1436 #[serde(default)]
1438 pub comment_prefix: Option<String>,
1439
1440 #[serde(default)]
1442 pub block_comment_start: Option<String>,
1443
1444 #[serde(default)]
1446 pub block_comment_end: Option<String>,
1447
1448 #[serde(default)]
1450 pub use_tabs: Option<bool>,
1451
1452 #[serde(default)]
1454 pub tab_size: Option<usize>,
1455
1456 #[serde(default)]
1458 pub auto_indent: Option<bool>,
1459
1460 #[serde(default)]
1463 pub show_whitespace_tabs: Option<bool>,
1464
1465 #[serde(default)]
1467 pub formatter: Option<FormatterPackConfig>,
1468}
1469
1470#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1472#[serde(rename_all = "camelCase")]
1473#[ts(export)]
1474pub struct FormatterPackConfig {
1475 pub command: String,
1477
1478 #[serde(default)]
1480 pub args: Vec<String>,
1481}
1482
1483#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1485#[serde(rename_all = "camelCase")]
1486#[ts(export)]
1487pub struct LspServerPackConfig {
1488 pub command: String,
1490
1491 #[serde(default)]
1493 pub args: Vec<String>,
1494
1495 #[serde(default)]
1497 pub auto_start: Option<bool>,
1498
1499 #[serde(default)]
1501 #[ts(type = "Record<string, unknown> | null")]
1502 pub initialization_options: Option<JsonValue>,
1503}
1504
1505#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1507#[ts(export)]
1508pub enum HunkStatus {
1509 Pending,
1510 Staged,
1511 Discarded,
1512}
1513
1514#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1516#[ts(export)]
1517pub struct ReviewHunk {
1518 pub id: String,
1519 pub file: String,
1520 pub context_header: String,
1521 pub status: HunkStatus,
1522 pub base_range: Option<(usize, usize)>,
1524 pub modified_range: Option<(usize, usize)>,
1526}
1527
1528#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1530#[serde(deny_unknown_fields)]
1531#[ts(export, rename = "TsActionPopupAction")]
1532pub struct ActionPopupAction {
1533 pub id: String,
1535 pub label: String,
1537}
1538
1539#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1541#[serde(deny_unknown_fields)]
1542#[ts(export)]
1543pub struct ActionPopupOptions {
1544 pub id: String,
1546 pub title: String,
1548 pub message: String,
1550 pub actions: Vec<ActionPopupAction>,
1552}
1553
1554#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1556#[ts(export)]
1557pub struct TsHighlightSpan {
1558 pub start: u32,
1559 pub end: u32,
1560 #[ts(type = "[number, number, number]")]
1561 pub color: (u8, u8, u8),
1562 pub bold: bool,
1563 pub italic: bool,
1564}
1565
1566#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1568#[ts(export)]
1569pub struct SpawnResult {
1570 pub stdout: String,
1572 pub stderr: String,
1574 pub exit_code: i32,
1576}
1577
1578#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1580#[ts(export)]
1581pub struct BackgroundProcessResult {
1582 #[ts(type = "number")]
1584 pub process_id: u64,
1585 pub exit_code: i32,
1588}
1589
1590#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1592#[serde(deny_unknown_fields)]
1593#[ts(export, rename = "TextPropertyEntry")]
1594pub struct JsTextPropertyEntry {
1595 pub text: String,
1597 #[serde(default)]
1599 #[ts(optional, type = "Record<string, unknown>")]
1600 pub properties: Option<HashMap<String, JsonValue>>,
1601}
1602
1603#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1605#[ts(export)]
1606pub struct DirEntry {
1607 pub name: String,
1609 pub is_file: bool,
1611 pub is_dir: bool,
1613}
1614
1615#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1617#[ts(export)]
1618pub struct JsPosition {
1619 pub line: u32,
1621 pub character: u32,
1623}
1624
1625#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1627#[ts(export)]
1628pub struct JsRange {
1629 pub start: JsPosition,
1631 pub end: JsPosition,
1633}
1634
1635#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1637#[ts(export)]
1638pub struct JsDiagnostic {
1639 pub uri: String,
1641 pub message: String,
1643 pub severity: Option<u8>,
1645 pub range: JsRange,
1647 #[ts(optional)]
1649 pub source: Option<String>,
1650}
1651
1652#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1654#[serde(deny_unknown_fields)]
1655#[ts(export)]
1656pub struct CreateVirtualBufferOptions {
1657 pub name: String,
1659 #[serde(default)]
1661 #[ts(optional)]
1662 pub mode: Option<String>,
1663 #[serde(default, rename = "readOnly")]
1665 #[ts(optional, rename = "readOnly")]
1666 pub read_only: Option<bool>,
1667 #[serde(default, rename = "showLineNumbers")]
1669 #[ts(optional, rename = "showLineNumbers")]
1670 pub show_line_numbers: Option<bool>,
1671 #[serde(default, rename = "showCursors")]
1673 #[ts(optional, rename = "showCursors")]
1674 pub show_cursors: Option<bool>,
1675 #[serde(default, rename = "editingDisabled")]
1677 #[ts(optional, rename = "editingDisabled")]
1678 pub editing_disabled: Option<bool>,
1679 #[serde(default, rename = "hiddenFromTabs")]
1681 #[ts(optional, rename = "hiddenFromTabs")]
1682 pub hidden_from_tabs: Option<bool>,
1683 #[serde(default)]
1685 #[ts(optional)]
1686 pub entries: Option<Vec<JsTextPropertyEntry>>,
1687}
1688
1689#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1691#[serde(deny_unknown_fields)]
1692#[ts(export)]
1693pub struct CreateVirtualBufferInSplitOptions {
1694 pub name: String,
1696 #[serde(default)]
1698 #[ts(optional)]
1699 pub mode: Option<String>,
1700 #[serde(default, rename = "readOnly")]
1702 #[ts(optional, rename = "readOnly")]
1703 pub read_only: Option<bool>,
1704 #[serde(default)]
1706 #[ts(optional)]
1707 pub ratio: Option<f32>,
1708 #[serde(default)]
1710 #[ts(optional)]
1711 pub direction: Option<String>,
1712 #[serde(default, rename = "panelId")]
1714 #[ts(optional, rename = "panelId")]
1715 pub panel_id: Option<String>,
1716 #[serde(default, rename = "showLineNumbers")]
1718 #[ts(optional, rename = "showLineNumbers")]
1719 pub show_line_numbers: Option<bool>,
1720 #[serde(default, rename = "showCursors")]
1722 #[ts(optional, rename = "showCursors")]
1723 pub show_cursors: Option<bool>,
1724 #[serde(default, rename = "editingDisabled")]
1726 #[ts(optional, rename = "editingDisabled")]
1727 pub editing_disabled: Option<bool>,
1728 #[serde(default, rename = "lineWrap")]
1730 #[ts(optional, rename = "lineWrap")]
1731 pub line_wrap: Option<bool>,
1732 #[serde(default)]
1734 #[ts(optional)]
1735 pub entries: Option<Vec<JsTextPropertyEntry>>,
1736}
1737
1738#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1740#[serde(deny_unknown_fields)]
1741#[ts(export)]
1742pub struct CreateVirtualBufferInExistingSplitOptions {
1743 pub name: String,
1745 #[serde(rename = "splitId")]
1747 #[ts(rename = "splitId")]
1748 pub split_id: usize,
1749 #[serde(default)]
1751 #[ts(optional)]
1752 pub mode: Option<String>,
1753 #[serde(default, rename = "readOnly")]
1755 #[ts(optional, rename = "readOnly")]
1756 pub read_only: Option<bool>,
1757 #[serde(default, rename = "showLineNumbers")]
1759 #[ts(optional, rename = "showLineNumbers")]
1760 pub show_line_numbers: Option<bool>,
1761 #[serde(default, rename = "showCursors")]
1763 #[ts(optional, rename = "showCursors")]
1764 pub show_cursors: Option<bool>,
1765 #[serde(default, rename = "editingDisabled")]
1767 #[ts(optional, rename = "editingDisabled")]
1768 pub editing_disabled: Option<bool>,
1769 #[serde(default, rename = "lineWrap")]
1771 #[ts(optional, rename = "lineWrap")]
1772 pub line_wrap: Option<bool>,
1773 #[serde(default)]
1775 #[ts(optional)]
1776 pub entries: Option<Vec<JsTextPropertyEntry>>,
1777}
1778
1779#[derive(Debug, Clone, Serialize, TS)]
1784#[ts(export, type = "Array<Record<string, unknown>>")]
1785pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
1786
1787#[cfg(feature = "plugins")]
1789mod fromjs_impls {
1790 use super::*;
1791 use rquickjs::{Ctx, FromJs, Value};
1792
1793 impl<'js> FromJs<'js> for JsTextPropertyEntry {
1794 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1795 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1796 from: "object",
1797 to: "JsTextPropertyEntry",
1798 message: Some(e.to_string()),
1799 })
1800 }
1801 }
1802
1803 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
1804 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1805 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1806 from: "object",
1807 to: "CreateVirtualBufferOptions",
1808 message: Some(e.to_string()),
1809 })
1810 }
1811 }
1812
1813 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
1814 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1815 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1816 from: "object",
1817 to: "CreateVirtualBufferInSplitOptions",
1818 message: Some(e.to_string()),
1819 })
1820 }
1821 }
1822
1823 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
1824 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1825 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1826 from: "object",
1827 to: "CreateVirtualBufferInExistingSplitOptions",
1828 message: Some(e.to_string()),
1829 })
1830 }
1831 }
1832
1833 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
1834 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
1835 rquickjs_serde::to_value(ctx.clone(), &self.0)
1836 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
1837 }
1838 }
1839
1840 impl<'js> FromJs<'js> for ActionSpec {
1843 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1844 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1845 from: "object",
1846 to: "ActionSpec",
1847 message: Some(e.to_string()),
1848 })
1849 }
1850 }
1851
1852 impl<'js> FromJs<'js> for ActionPopupAction {
1853 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1854 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1855 from: "object",
1856 to: "ActionPopupAction",
1857 message: Some(e.to_string()),
1858 })
1859 }
1860 }
1861
1862 impl<'js> FromJs<'js> for ActionPopupOptions {
1863 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1864 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1865 from: "object",
1866 to: "ActionPopupOptions",
1867 message: Some(e.to_string()),
1868 })
1869 }
1870 }
1871
1872 impl<'js> FromJs<'js> for ViewTokenWire {
1873 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1874 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1875 from: "object",
1876 to: "ViewTokenWire",
1877 message: Some(e.to_string()),
1878 })
1879 }
1880 }
1881
1882 impl<'js> FromJs<'js> for ViewTokenStyle {
1883 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1884 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1885 from: "object",
1886 to: "ViewTokenStyle",
1887 message: Some(e.to_string()),
1888 })
1889 }
1890 }
1891
1892 impl<'js> FromJs<'js> for LayoutHints {
1893 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1894 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1895 from: "object",
1896 to: "LayoutHints",
1897 message: Some(e.to_string()),
1898 })
1899 }
1900 }
1901
1902 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
1903 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1904 let json: serde_json::Value =
1906 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1907 from: "object",
1908 to: "CreateCompositeBufferOptions (json)",
1909 message: Some(e.to_string()),
1910 })?;
1911 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
1912 from: "json",
1913 to: "CreateCompositeBufferOptions",
1914 message: Some(e.to_string()),
1915 })
1916 }
1917 }
1918
1919 impl<'js> FromJs<'js> for CompositeHunk {
1920 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1921 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1922 from: "object",
1923 to: "CompositeHunk",
1924 message: Some(e.to_string()),
1925 })
1926 }
1927 }
1928
1929 impl<'js> FromJs<'js> for LanguagePackConfig {
1930 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1931 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1932 from: "object",
1933 to: "LanguagePackConfig",
1934 message: Some(e.to_string()),
1935 })
1936 }
1937 }
1938
1939 impl<'js> FromJs<'js> for LspServerPackConfig {
1940 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1941 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1942 from: "object",
1943 to: "LspServerPackConfig",
1944 message: Some(e.to_string()),
1945 })
1946 }
1947 }
1948}
1949
1950pub struct PluginApi {
1952 hooks: Arc<RwLock<HookRegistry>>,
1954
1955 commands: Arc<RwLock<CommandRegistry>>,
1957
1958 command_sender: std::sync::mpsc::Sender<PluginCommand>,
1960
1961 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
1963}
1964
1965impl PluginApi {
1966 pub fn new(
1968 hooks: Arc<RwLock<HookRegistry>>,
1969 commands: Arc<RwLock<CommandRegistry>>,
1970 command_sender: std::sync::mpsc::Sender<PluginCommand>,
1971 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
1972 ) -> Self {
1973 Self {
1974 hooks,
1975 commands,
1976 command_sender,
1977 state_snapshot,
1978 }
1979 }
1980
1981 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
1983 let mut hooks = self.hooks.write().unwrap();
1984 hooks.add_hook(hook_name, callback);
1985 }
1986
1987 pub fn unregister_hooks(&self, hook_name: &str) {
1989 let mut hooks = self.hooks.write().unwrap();
1990 hooks.remove_hooks(hook_name);
1991 }
1992
1993 pub fn register_command(&self, command: Command) {
1995 let commands = self.commands.read().unwrap();
1996 commands.register(command);
1997 }
1998
1999 pub fn unregister_command(&self, name: &str) {
2001 let commands = self.commands.read().unwrap();
2002 commands.unregister(name);
2003 }
2004
2005 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
2007 self.command_sender
2008 .send(command)
2009 .map_err(|e| format!("Failed to send command: {}", e))
2010 }
2011
2012 pub fn insert_text(
2014 &self,
2015 buffer_id: BufferId,
2016 position: usize,
2017 text: String,
2018 ) -> Result<(), String> {
2019 self.send_command(PluginCommand::InsertText {
2020 buffer_id,
2021 position,
2022 text,
2023 })
2024 }
2025
2026 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
2028 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
2029 }
2030
2031 pub fn add_overlay(
2039 &self,
2040 buffer_id: BufferId,
2041 namespace: Option<String>,
2042 range: Range<usize>,
2043 options: OverlayOptions,
2044 ) -> Result<(), String> {
2045 self.send_command(PluginCommand::AddOverlay {
2046 buffer_id,
2047 namespace: namespace.map(OverlayNamespace::from_string),
2048 range,
2049 options,
2050 })
2051 }
2052
2053 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
2055 self.send_command(PluginCommand::RemoveOverlay {
2056 buffer_id,
2057 handle: OverlayHandle::from_string(handle),
2058 })
2059 }
2060
2061 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
2063 self.send_command(PluginCommand::ClearNamespace {
2064 buffer_id,
2065 namespace: OverlayNamespace::from_string(namespace),
2066 })
2067 }
2068
2069 pub fn clear_overlays_in_range(
2072 &self,
2073 buffer_id: BufferId,
2074 start: usize,
2075 end: usize,
2076 ) -> Result<(), String> {
2077 self.send_command(PluginCommand::ClearOverlaysInRange {
2078 buffer_id,
2079 start,
2080 end,
2081 })
2082 }
2083
2084 pub fn set_status(&self, message: String) -> Result<(), String> {
2086 self.send_command(PluginCommand::SetStatus { message })
2087 }
2088
2089 pub fn open_file_at_location(
2092 &self,
2093 path: PathBuf,
2094 line: Option<usize>,
2095 column: Option<usize>,
2096 ) -> Result<(), String> {
2097 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
2098 }
2099
2100 pub fn open_file_in_split(
2105 &self,
2106 split_id: usize,
2107 path: PathBuf,
2108 line: Option<usize>,
2109 column: Option<usize>,
2110 ) -> Result<(), String> {
2111 self.send_command(PluginCommand::OpenFileInSplit {
2112 split_id,
2113 path,
2114 line,
2115 column,
2116 })
2117 }
2118
2119 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
2122 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
2123 }
2124
2125 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
2128 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
2129 }
2130
2131 pub fn add_menu_item(
2133 &self,
2134 menu_label: String,
2135 item: MenuItem,
2136 position: MenuPosition,
2137 ) -> Result<(), String> {
2138 self.send_command(PluginCommand::AddMenuItem {
2139 menu_label,
2140 item,
2141 position,
2142 })
2143 }
2144
2145 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
2147 self.send_command(PluginCommand::AddMenu { menu, position })
2148 }
2149
2150 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
2152 self.send_command(PluginCommand::RemoveMenuItem {
2153 menu_label,
2154 item_label,
2155 })
2156 }
2157
2158 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
2160 self.send_command(PluginCommand::RemoveMenu { menu_label })
2161 }
2162
2163 pub fn create_virtual_buffer(
2170 &self,
2171 name: String,
2172 mode: String,
2173 read_only: bool,
2174 ) -> Result<(), String> {
2175 self.send_command(PluginCommand::CreateVirtualBuffer {
2176 name,
2177 mode,
2178 read_only,
2179 })
2180 }
2181
2182 pub fn create_virtual_buffer_with_content(
2188 &self,
2189 name: String,
2190 mode: String,
2191 read_only: bool,
2192 entries: Vec<TextPropertyEntry>,
2193 ) -> Result<(), String> {
2194 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
2195 name,
2196 mode,
2197 read_only,
2198 entries,
2199 show_line_numbers: true,
2200 show_cursors: true,
2201 editing_disabled: false,
2202 hidden_from_tabs: false,
2203 request_id: None,
2204 })
2205 }
2206
2207 pub fn set_virtual_buffer_content(
2211 &self,
2212 buffer_id: BufferId,
2213 entries: Vec<TextPropertyEntry>,
2214 ) -> Result<(), String> {
2215 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
2216 }
2217
2218 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
2222 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
2223 }
2224
2225 pub fn define_mode(
2230 &self,
2231 name: String,
2232 parent: Option<String>,
2233 bindings: Vec<(String, String)>,
2234 read_only: bool,
2235 ) -> Result<(), String> {
2236 self.send_command(PluginCommand::DefineMode {
2237 name,
2238 parent,
2239 bindings,
2240 read_only,
2241 })
2242 }
2243
2244 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
2246 self.send_command(PluginCommand::ShowBuffer { buffer_id })
2247 }
2248
2249 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
2251 self.send_command(PluginCommand::SetSplitScroll {
2252 split_id: SplitId(split_id),
2253 top_byte,
2254 })
2255 }
2256
2257 pub fn get_highlights(
2259 &self,
2260 buffer_id: BufferId,
2261 range: Range<usize>,
2262 request_id: u64,
2263 ) -> Result<(), String> {
2264 self.send_command(PluginCommand::RequestHighlights {
2265 buffer_id,
2266 range,
2267 request_id,
2268 })
2269 }
2270
2271 pub fn get_active_buffer_id(&self) -> BufferId {
2275 let snapshot = self.state_snapshot.read().unwrap();
2276 snapshot.active_buffer_id
2277 }
2278
2279 pub fn get_active_split_id(&self) -> usize {
2281 let snapshot = self.state_snapshot.read().unwrap();
2282 snapshot.active_split_id
2283 }
2284
2285 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
2287 let snapshot = self.state_snapshot.read().unwrap();
2288 snapshot.buffers.get(&buffer_id).cloned()
2289 }
2290
2291 pub fn list_buffers(&self) -> Vec<BufferInfo> {
2293 let snapshot = self.state_snapshot.read().unwrap();
2294 snapshot.buffers.values().cloned().collect()
2295 }
2296
2297 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
2299 let snapshot = self.state_snapshot.read().unwrap();
2300 snapshot.primary_cursor.clone()
2301 }
2302
2303 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
2305 let snapshot = self.state_snapshot.read().unwrap();
2306 snapshot.all_cursors.clone()
2307 }
2308
2309 pub fn get_viewport(&self) -> Option<ViewportInfo> {
2311 let snapshot = self.state_snapshot.read().unwrap();
2312 snapshot.viewport.clone()
2313 }
2314
2315 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
2317 Arc::clone(&self.state_snapshot)
2318 }
2319}
2320
2321impl Clone for PluginApi {
2322 fn clone(&self) -> Self {
2323 Self {
2324 hooks: Arc::clone(&self.hooks),
2325 commands: Arc::clone(&self.commands),
2326 command_sender: self.command_sender.clone(),
2327 state_snapshot: Arc::clone(&self.state_snapshot),
2328 }
2329 }
2330}
2331
2332#[cfg(test)]
2333mod tests {
2334 use super::*;
2335
2336 #[test]
2337 fn test_plugin_api_creation() {
2338 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2339 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2340 let (tx, _rx) = std::sync::mpsc::channel();
2341 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2342
2343 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2344
2345 let _clone = api.clone();
2347 }
2348
2349 #[test]
2350 fn test_register_hook() {
2351 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2352 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2353 let (tx, _rx) = std::sync::mpsc::channel();
2354 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2355
2356 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
2357
2358 api.register_hook("test-hook", Box::new(|_| true));
2359
2360 let hook_registry = hooks.read().unwrap();
2361 assert_eq!(hook_registry.hook_count("test-hook"), 1);
2362 }
2363
2364 #[test]
2365 fn test_send_command() {
2366 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2367 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2368 let (tx, rx) = std::sync::mpsc::channel();
2369 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2370
2371 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2372
2373 let result = api.insert_text(BufferId(1), 0, "test".to_string());
2374 assert!(result.is_ok());
2375
2376 let received = rx.try_recv();
2378 assert!(received.is_ok());
2379
2380 match received.unwrap() {
2381 PluginCommand::InsertText {
2382 buffer_id,
2383 position,
2384 text,
2385 } => {
2386 assert_eq!(buffer_id.0, 1);
2387 assert_eq!(position, 0);
2388 assert_eq!(text, "test");
2389 }
2390 _ => panic!("Wrong command type"),
2391 }
2392 }
2393
2394 #[test]
2395 fn test_add_overlay_command() {
2396 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2397 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2398 let (tx, rx) = std::sync::mpsc::channel();
2399 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2400
2401 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2402
2403 let result = api.add_overlay(
2404 BufferId(1),
2405 Some("test-overlay".to_string()),
2406 0..10,
2407 OverlayOptions {
2408 fg: Some(OverlayColorSpec::ThemeKey("ui.status_bar_fg".to_string())),
2409 bg: None,
2410 underline: true,
2411 bold: false,
2412 italic: false,
2413 extend_to_line_end: false,
2414 },
2415 );
2416 assert!(result.is_ok());
2417
2418 let received = rx.try_recv().unwrap();
2419 match received {
2420 PluginCommand::AddOverlay {
2421 buffer_id,
2422 namespace,
2423 range,
2424 options,
2425 } => {
2426 assert_eq!(buffer_id.0, 1);
2427 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
2428 assert_eq!(range, 0..10);
2429 assert!(matches!(
2430 options.fg,
2431 Some(OverlayColorSpec::ThemeKey(ref k)) if k == "ui.status_bar_fg"
2432 ));
2433 assert!(options.bg.is_none());
2434 assert!(options.underline);
2435 assert!(!options.bold);
2436 assert!(!options.italic);
2437 assert!(!options.extend_to_line_end);
2438 }
2439 _ => panic!("Wrong command type"),
2440 }
2441 }
2442
2443 #[test]
2444 fn test_set_status_command() {
2445 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2446 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2447 let (tx, rx) = std::sync::mpsc::channel();
2448 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2449
2450 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2451
2452 let result = api.set_status("Test status".to_string());
2453 assert!(result.is_ok());
2454
2455 let received = rx.try_recv().unwrap();
2456 match received {
2457 PluginCommand::SetStatus { message } => {
2458 assert_eq!(message, "Test status");
2459 }
2460 _ => panic!("Wrong command type"),
2461 }
2462 }
2463
2464 #[test]
2465 fn test_get_active_buffer_id() {
2466 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2467 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2468 let (tx, _rx) = std::sync::mpsc::channel();
2469 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2470
2471 {
2473 let mut snapshot = state_snapshot.write().unwrap();
2474 snapshot.active_buffer_id = BufferId(5);
2475 }
2476
2477 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2478
2479 let active_id = api.get_active_buffer_id();
2480 assert_eq!(active_id.0, 5);
2481 }
2482
2483 #[test]
2484 fn test_get_buffer_info() {
2485 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2486 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2487 let (tx, _rx) = std::sync::mpsc::channel();
2488 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2489
2490 {
2492 let mut snapshot = state_snapshot.write().unwrap();
2493 let buffer_info = BufferInfo {
2494 id: BufferId(1),
2495 path: Some(std::path::PathBuf::from("/test/file.txt")),
2496 modified: true,
2497 length: 100,
2498 };
2499 snapshot.buffers.insert(BufferId(1), buffer_info);
2500 }
2501
2502 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2503
2504 let info = api.get_buffer_info(BufferId(1));
2505 assert!(info.is_some());
2506 let info = info.unwrap();
2507 assert_eq!(info.id.0, 1);
2508 assert_eq!(
2509 info.path.as_ref().unwrap().to_str().unwrap(),
2510 "/test/file.txt"
2511 );
2512 assert!(info.modified);
2513 assert_eq!(info.length, 100);
2514
2515 let no_info = api.get_buffer_info(BufferId(999));
2517 assert!(no_info.is_none());
2518 }
2519
2520 #[test]
2521 fn test_list_buffers() {
2522 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2523 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2524 let (tx, _rx) = std::sync::mpsc::channel();
2525 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2526
2527 {
2529 let mut snapshot = state_snapshot.write().unwrap();
2530 snapshot.buffers.insert(
2531 BufferId(1),
2532 BufferInfo {
2533 id: BufferId(1),
2534 path: Some(std::path::PathBuf::from("/file1.txt")),
2535 modified: false,
2536 length: 50,
2537 },
2538 );
2539 snapshot.buffers.insert(
2540 BufferId(2),
2541 BufferInfo {
2542 id: BufferId(2),
2543 path: Some(std::path::PathBuf::from("/file2.txt")),
2544 modified: true,
2545 length: 100,
2546 },
2547 );
2548 snapshot.buffers.insert(
2549 BufferId(3),
2550 BufferInfo {
2551 id: BufferId(3),
2552 path: None,
2553 modified: false,
2554 length: 0,
2555 },
2556 );
2557 }
2558
2559 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2560
2561 let buffers = api.list_buffers();
2562 assert_eq!(buffers.len(), 3);
2563
2564 assert!(buffers.iter().any(|b| b.id.0 == 1));
2566 assert!(buffers.iter().any(|b| b.id.0 == 2));
2567 assert!(buffers.iter().any(|b| b.id.0 == 3));
2568 }
2569
2570 #[test]
2571 fn test_get_primary_cursor() {
2572 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2573 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2574 let (tx, _rx) = std::sync::mpsc::channel();
2575 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2576
2577 {
2579 let mut snapshot = state_snapshot.write().unwrap();
2580 snapshot.primary_cursor = Some(CursorInfo {
2581 position: 42,
2582 selection: Some(10..42),
2583 });
2584 }
2585
2586 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2587
2588 let cursor = api.get_primary_cursor();
2589 assert!(cursor.is_some());
2590 let cursor = cursor.unwrap();
2591 assert_eq!(cursor.position, 42);
2592 assert_eq!(cursor.selection, Some(10..42));
2593 }
2594
2595 #[test]
2596 fn test_get_all_cursors() {
2597 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2598 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2599 let (tx, _rx) = std::sync::mpsc::channel();
2600 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2601
2602 {
2604 let mut snapshot = state_snapshot.write().unwrap();
2605 snapshot.all_cursors = vec![
2606 CursorInfo {
2607 position: 10,
2608 selection: None,
2609 },
2610 CursorInfo {
2611 position: 20,
2612 selection: Some(15..20),
2613 },
2614 CursorInfo {
2615 position: 30,
2616 selection: Some(25..30),
2617 },
2618 ];
2619 }
2620
2621 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2622
2623 let cursors = api.get_all_cursors();
2624 assert_eq!(cursors.len(), 3);
2625 assert_eq!(cursors[0].position, 10);
2626 assert_eq!(cursors[0].selection, None);
2627 assert_eq!(cursors[1].position, 20);
2628 assert_eq!(cursors[1].selection, Some(15..20));
2629 assert_eq!(cursors[2].position, 30);
2630 assert_eq!(cursors[2].selection, Some(25..30));
2631 }
2632
2633 #[test]
2634 fn test_get_viewport() {
2635 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2636 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2637 let (tx, _rx) = std::sync::mpsc::channel();
2638 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2639
2640 {
2642 let mut snapshot = state_snapshot.write().unwrap();
2643 snapshot.viewport = Some(ViewportInfo {
2644 top_byte: 100,
2645 left_column: 5,
2646 width: 80,
2647 height: 24,
2648 });
2649 }
2650
2651 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2652
2653 let viewport = api.get_viewport();
2654 assert!(viewport.is_some());
2655 let viewport = viewport.unwrap();
2656 assert_eq!(viewport.top_byte, 100);
2657 assert_eq!(viewport.left_column, 5);
2658 assert_eq!(viewport.width, 80);
2659 assert_eq!(viewport.height, 24);
2660 }
2661
2662 #[test]
2663 fn test_composite_buffer_options_rejects_unknown_fields() {
2664 let valid_json = r#"{
2666 "name": "test",
2667 "mode": "diff",
2668 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2669 "sources": [{"bufferId": 1, "label": "old"}]
2670 }"#;
2671 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
2672 assert!(
2673 result.is_ok(),
2674 "Valid JSON should parse: {:?}",
2675 result.err()
2676 );
2677
2678 let invalid_json = r#"{
2680 "name": "test",
2681 "mode": "diff",
2682 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2683 "sources": [{"buffer_id": 1, "label": "old"}]
2684 }"#;
2685 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
2686 assert!(
2687 result.is_err(),
2688 "JSON with unknown field should fail to parse"
2689 );
2690 let err = result.unwrap_err().to_string();
2691 assert!(
2692 err.contains("unknown field") || err.contains("buffer_id"),
2693 "Error should mention unknown field: {}",
2694 err
2695 );
2696 }
2697
2698 #[test]
2699 fn test_composite_hunk_rejects_unknown_fields() {
2700 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
2702 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
2703 assert!(
2704 result.is_ok(),
2705 "Valid JSON should parse: {:?}",
2706 result.err()
2707 );
2708
2709 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
2711 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
2712 assert!(
2713 result.is_err(),
2714 "JSON with unknown field should fail to parse"
2715 );
2716 let err = result.unwrap_err().to_string();
2717 assert!(
2718 err.contains("unknown field") || err.contains("old_start"),
2719 "Error should mention unknown field: {}",
2720 err
2721 );
2722 }
2723}