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 SubmitViewTransform {
844 buffer_id: BufferId,
845 split_id: Option<SplitId>,
846 payload: ViewTransformPayload,
847 },
848
849 ClearViewTransform {
851 buffer_id: BufferId,
852 split_id: Option<SplitId>,
853 },
854
855 ClearAllOverlays { buffer_id: BufferId },
857
858 ClearNamespace {
860 buffer_id: BufferId,
861 namespace: OverlayNamespace,
862 },
863
864 ClearOverlaysInRange {
867 buffer_id: BufferId,
868 start: usize,
869 end: usize,
870 },
871
872 AddVirtualText {
875 buffer_id: BufferId,
876 virtual_text_id: String,
877 position: usize,
878 text: String,
879 color: (u8, u8, u8),
880 use_bg: bool, before: bool, },
883
884 RemoveVirtualText {
886 buffer_id: BufferId,
887 virtual_text_id: String,
888 },
889
890 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
892
893 ClearVirtualTexts { buffer_id: BufferId },
895
896 AddVirtualLine {
900 buffer_id: BufferId,
901 position: usize,
903 text: String,
905 fg_color: (u8, u8, u8),
907 bg_color: Option<(u8, u8, u8)>,
909 above: bool,
911 namespace: String,
913 priority: i32,
915 },
916
917 ClearVirtualTextNamespace {
920 buffer_id: BufferId,
921 namespace: String,
922 },
923
924 RefreshLines { buffer_id: BufferId },
926
927 SetLineIndicator {
930 buffer_id: BufferId,
931 line: usize,
933 namespace: String,
935 symbol: String,
937 color: (u8, u8, u8),
939 priority: i32,
941 },
942
943 ClearLineIndicators {
945 buffer_id: BufferId,
946 namespace: String,
948 },
949
950 SetFileExplorerDecorations {
952 namespace: String,
954 decorations: Vec<FileExplorerDecoration>,
956 },
957
958 ClearFileExplorerDecorations {
960 namespace: String,
962 },
963
964 OpenFileAtLocation {
967 path: PathBuf,
968 line: Option<usize>, column: Option<usize>, },
971
972 OpenFileInSplit {
975 split_id: usize,
976 path: PathBuf,
977 line: Option<usize>, column: Option<usize>, },
980
981 StartPrompt {
984 label: String,
985 prompt_type: String, },
987
988 StartPromptWithInitial {
990 label: String,
991 prompt_type: String,
992 initial_value: String,
993 },
994
995 StartPromptAsync {
998 label: String,
999 initial_value: String,
1000 callback_id: JsCallbackId,
1001 },
1002
1003 SetPromptSuggestions { suggestions: Vec<Suggestion> },
1006
1007 AddMenuItem {
1010 menu_label: String,
1011 item: MenuItem,
1012 position: MenuPosition,
1013 },
1014
1015 AddMenu { menu: Menu, position: MenuPosition },
1017
1018 RemoveMenuItem {
1020 menu_label: String,
1021 item_label: String,
1022 },
1023
1024 RemoveMenu { menu_label: String },
1026
1027 CreateVirtualBuffer {
1029 name: String,
1031 mode: String,
1033 read_only: bool,
1035 },
1036
1037 CreateVirtualBufferWithContent {
1041 name: String,
1043 mode: String,
1045 read_only: bool,
1047 entries: Vec<TextPropertyEntry>,
1049 show_line_numbers: bool,
1051 show_cursors: bool,
1053 editing_disabled: bool,
1055 hidden_from_tabs: bool,
1057 request_id: Option<u64>,
1059 },
1060
1061 CreateVirtualBufferInSplit {
1064 name: String,
1066 mode: String,
1068 read_only: bool,
1070 entries: Vec<TextPropertyEntry>,
1072 ratio: f32,
1074 direction: Option<String>,
1076 panel_id: Option<String>,
1078 show_line_numbers: bool,
1080 show_cursors: bool,
1082 editing_disabled: bool,
1084 line_wrap: Option<bool>,
1086 request_id: Option<u64>,
1088 },
1089
1090 SetVirtualBufferContent {
1092 buffer_id: BufferId,
1093 entries: Vec<TextPropertyEntry>,
1095 },
1096
1097 GetTextPropertiesAtCursor { buffer_id: BufferId },
1099
1100 DefineMode {
1102 name: String,
1103 parent: Option<String>,
1104 bindings: Vec<(String, String)>, read_only: bool,
1106 },
1107
1108 ShowBuffer { buffer_id: BufferId },
1110
1111 CreateVirtualBufferInExistingSplit {
1113 name: String,
1115 mode: String,
1117 read_only: bool,
1119 entries: Vec<TextPropertyEntry>,
1121 split_id: SplitId,
1123 show_line_numbers: bool,
1125 show_cursors: bool,
1127 editing_disabled: bool,
1129 line_wrap: Option<bool>,
1131 request_id: Option<u64>,
1133 },
1134
1135 CloseBuffer { buffer_id: BufferId },
1137
1138 CreateCompositeBuffer {
1141 name: String,
1143 mode: String,
1145 layout: CompositeLayoutConfig,
1147 sources: Vec<CompositeSourceConfig>,
1149 hunks: Option<Vec<CompositeHunk>>,
1151 request_id: Option<u64>,
1153 },
1154
1155 UpdateCompositeAlignment {
1157 buffer_id: BufferId,
1158 hunks: Vec<CompositeHunk>,
1159 },
1160
1161 CloseCompositeBuffer { buffer_id: BufferId },
1163
1164 FocusSplit { split_id: SplitId },
1166
1167 SetSplitBuffer {
1169 split_id: SplitId,
1170 buffer_id: BufferId,
1171 },
1172
1173 SetSplitScroll { split_id: SplitId, top_byte: usize },
1175
1176 RequestHighlights {
1178 buffer_id: BufferId,
1179 range: Range<usize>,
1180 request_id: u64,
1181 },
1182
1183 CloseSplit { split_id: SplitId },
1185
1186 SetSplitRatio {
1188 split_id: SplitId,
1189 ratio: f32,
1191 },
1192
1193 DistributeSplitsEvenly {
1195 split_ids: Vec<SplitId>,
1197 },
1198
1199 SetBufferCursor {
1201 buffer_id: BufferId,
1202 position: usize,
1204 },
1205
1206 SendLspRequest {
1208 language: String,
1209 method: String,
1210 #[ts(type = "any")]
1211 params: Option<JsonValue>,
1212 request_id: u64,
1213 },
1214
1215 SetClipboard { text: String },
1217
1218 DeleteSelection,
1221
1222 SetContext {
1226 name: String,
1228 active: bool,
1230 },
1231
1232 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1234
1235 ExecuteAction {
1238 action_name: String,
1240 },
1241
1242 ExecuteActions {
1246 actions: Vec<ActionSpec>,
1248 },
1249
1250 GetBufferText {
1252 buffer_id: BufferId,
1254 start: usize,
1256 end: usize,
1258 request_id: u64,
1260 },
1261
1262 GetLineStartPosition {
1265 buffer_id: BufferId,
1267 line: u32,
1269 request_id: u64,
1271 },
1272
1273 SetEditorMode {
1276 mode: Option<String>,
1278 },
1279
1280 ShowActionPopup {
1283 popup_id: String,
1285 title: String,
1287 message: String,
1289 actions: Vec<ActionPopupAction>,
1291 },
1292
1293 DisableLspForLanguage {
1295 language: String,
1297 },
1298
1299 SetLspRootUri {
1303 language: String,
1305 uri: String,
1307 },
1308
1309 CreateScrollSyncGroup {
1313 group_id: u32,
1315 left_split: SplitId,
1317 right_split: SplitId,
1319 },
1320
1321 SetScrollSyncAnchors {
1324 group_id: u32,
1326 anchors: Vec<(usize, usize)>,
1328 },
1329
1330 RemoveScrollSyncGroup {
1332 group_id: u32,
1334 },
1335
1336 SaveBufferToPath {
1339 buffer_id: BufferId,
1341 path: PathBuf,
1343 },
1344
1345 LoadPlugin {
1348 path: PathBuf,
1350 callback_id: JsCallbackId,
1352 },
1353
1354 UnloadPlugin {
1357 name: String,
1359 callback_id: JsCallbackId,
1361 },
1362
1363 ReloadPlugin {
1366 name: String,
1368 callback_id: JsCallbackId,
1370 },
1371
1372 ListPlugins {
1375 callback_id: JsCallbackId,
1377 },
1378
1379 ReloadThemes,
1382
1383 RegisterGrammar {
1386 language: String,
1388 grammar_path: String,
1390 extensions: Vec<String>,
1392 },
1393
1394 RegisterLanguageConfig {
1397 language: String,
1399 config: LanguagePackConfig,
1401 },
1402
1403 RegisterLspServer {
1406 language: String,
1408 config: LspServerPackConfig,
1410 },
1411
1412 ReloadGrammars,
1415}
1416
1417#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
1426#[serde(rename_all = "camelCase")]
1427#[ts(export)]
1428pub struct LanguagePackConfig {
1429 #[serde(default)]
1431 pub comment_prefix: Option<String>,
1432
1433 #[serde(default)]
1435 pub block_comment_start: Option<String>,
1436
1437 #[serde(default)]
1439 pub block_comment_end: Option<String>,
1440
1441 #[serde(default)]
1443 pub use_tabs: Option<bool>,
1444
1445 #[serde(default)]
1447 pub tab_size: Option<usize>,
1448
1449 #[serde(default)]
1451 pub auto_indent: Option<bool>,
1452
1453 #[serde(default)]
1456 pub show_whitespace_tabs: Option<bool>,
1457
1458 #[serde(default)]
1460 pub formatter: Option<FormatterPackConfig>,
1461}
1462
1463#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1465#[serde(rename_all = "camelCase")]
1466#[ts(export)]
1467pub struct FormatterPackConfig {
1468 pub command: String,
1470
1471 #[serde(default)]
1473 pub args: Vec<String>,
1474}
1475
1476#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1478#[serde(rename_all = "camelCase")]
1479#[ts(export)]
1480pub struct LspServerPackConfig {
1481 pub command: String,
1483
1484 #[serde(default)]
1486 pub args: Vec<String>,
1487
1488 #[serde(default)]
1490 pub auto_start: Option<bool>,
1491
1492 #[serde(default)]
1494 #[ts(type = "Record<string, unknown> | null")]
1495 pub initialization_options: Option<JsonValue>,
1496}
1497
1498#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1500#[ts(export)]
1501pub enum HunkStatus {
1502 Pending,
1503 Staged,
1504 Discarded,
1505}
1506
1507#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1509#[ts(export)]
1510pub struct ReviewHunk {
1511 pub id: String,
1512 pub file: String,
1513 pub context_header: String,
1514 pub status: HunkStatus,
1515 pub base_range: Option<(usize, usize)>,
1517 pub modified_range: Option<(usize, usize)>,
1519}
1520
1521#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1523#[serde(deny_unknown_fields)]
1524#[ts(export, rename = "TsActionPopupAction")]
1525pub struct ActionPopupAction {
1526 pub id: String,
1528 pub label: String,
1530}
1531
1532#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1534#[serde(deny_unknown_fields)]
1535#[ts(export)]
1536pub struct ActionPopupOptions {
1537 pub id: String,
1539 pub title: String,
1541 pub message: String,
1543 pub actions: Vec<ActionPopupAction>,
1545}
1546
1547#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1549#[ts(export)]
1550pub struct TsHighlightSpan {
1551 pub start: u32,
1552 pub end: u32,
1553 #[ts(type = "[number, number, number]")]
1554 pub color: (u8, u8, u8),
1555 pub bold: bool,
1556 pub italic: bool,
1557}
1558
1559#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1561#[ts(export)]
1562pub struct SpawnResult {
1563 pub stdout: String,
1565 pub stderr: String,
1567 pub exit_code: i32,
1569}
1570
1571#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1573#[ts(export)]
1574pub struct BackgroundProcessResult {
1575 #[ts(type = "number")]
1577 pub process_id: u64,
1578 pub exit_code: i32,
1581}
1582
1583#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1585#[serde(deny_unknown_fields)]
1586#[ts(export, rename = "TextPropertyEntry")]
1587pub struct JsTextPropertyEntry {
1588 pub text: String,
1590 #[serde(default)]
1592 #[ts(optional, type = "Record<string, unknown>")]
1593 pub properties: Option<HashMap<String, JsonValue>>,
1594}
1595
1596#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1598#[ts(export)]
1599pub struct DirEntry {
1600 pub name: String,
1602 pub is_file: bool,
1604 pub is_dir: bool,
1606}
1607
1608#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1610#[ts(export)]
1611pub struct JsPosition {
1612 pub line: u32,
1614 pub character: u32,
1616}
1617
1618#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1620#[ts(export)]
1621pub struct JsRange {
1622 pub start: JsPosition,
1624 pub end: JsPosition,
1626}
1627
1628#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1630#[ts(export)]
1631pub struct JsDiagnostic {
1632 pub uri: String,
1634 pub message: String,
1636 pub severity: Option<u8>,
1638 pub range: JsRange,
1640 #[ts(optional)]
1642 pub source: Option<String>,
1643}
1644
1645#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1647#[serde(deny_unknown_fields)]
1648#[ts(export)]
1649pub struct CreateVirtualBufferOptions {
1650 pub name: String,
1652 #[serde(default)]
1654 #[ts(optional)]
1655 pub mode: Option<String>,
1656 #[serde(default, rename = "readOnly")]
1658 #[ts(optional, rename = "readOnly")]
1659 pub read_only: Option<bool>,
1660 #[serde(default, rename = "showLineNumbers")]
1662 #[ts(optional, rename = "showLineNumbers")]
1663 pub show_line_numbers: Option<bool>,
1664 #[serde(default, rename = "showCursors")]
1666 #[ts(optional, rename = "showCursors")]
1667 pub show_cursors: Option<bool>,
1668 #[serde(default, rename = "editingDisabled")]
1670 #[ts(optional, rename = "editingDisabled")]
1671 pub editing_disabled: Option<bool>,
1672 #[serde(default, rename = "hiddenFromTabs")]
1674 #[ts(optional, rename = "hiddenFromTabs")]
1675 pub hidden_from_tabs: Option<bool>,
1676 #[serde(default)]
1678 #[ts(optional)]
1679 pub entries: Option<Vec<JsTextPropertyEntry>>,
1680}
1681
1682#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1684#[serde(deny_unknown_fields)]
1685#[ts(export)]
1686pub struct CreateVirtualBufferInSplitOptions {
1687 pub name: String,
1689 #[serde(default)]
1691 #[ts(optional)]
1692 pub mode: Option<String>,
1693 #[serde(default, rename = "readOnly")]
1695 #[ts(optional, rename = "readOnly")]
1696 pub read_only: Option<bool>,
1697 #[serde(default)]
1699 #[ts(optional)]
1700 pub ratio: Option<f32>,
1701 #[serde(default)]
1703 #[ts(optional)]
1704 pub direction: Option<String>,
1705 #[serde(default, rename = "panelId")]
1707 #[ts(optional, rename = "panelId")]
1708 pub panel_id: Option<String>,
1709 #[serde(default, rename = "showLineNumbers")]
1711 #[ts(optional, rename = "showLineNumbers")]
1712 pub show_line_numbers: Option<bool>,
1713 #[serde(default, rename = "showCursors")]
1715 #[ts(optional, rename = "showCursors")]
1716 pub show_cursors: Option<bool>,
1717 #[serde(default, rename = "editingDisabled")]
1719 #[ts(optional, rename = "editingDisabled")]
1720 pub editing_disabled: Option<bool>,
1721 #[serde(default, rename = "lineWrap")]
1723 #[ts(optional, rename = "lineWrap")]
1724 pub line_wrap: Option<bool>,
1725 #[serde(default)]
1727 #[ts(optional)]
1728 pub entries: Option<Vec<JsTextPropertyEntry>>,
1729}
1730
1731#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1733#[serde(deny_unknown_fields)]
1734#[ts(export)]
1735pub struct CreateVirtualBufferInExistingSplitOptions {
1736 pub name: String,
1738 #[serde(rename = "splitId")]
1740 #[ts(rename = "splitId")]
1741 pub split_id: usize,
1742 #[serde(default)]
1744 #[ts(optional)]
1745 pub mode: Option<String>,
1746 #[serde(default, rename = "readOnly")]
1748 #[ts(optional, rename = "readOnly")]
1749 pub read_only: Option<bool>,
1750 #[serde(default, rename = "showLineNumbers")]
1752 #[ts(optional, rename = "showLineNumbers")]
1753 pub show_line_numbers: Option<bool>,
1754 #[serde(default, rename = "showCursors")]
1756 #[ts(optional, rename = "showCursors")]
1757 pub show_cursors: Option<bool>,
1758 #[serde(default, rename = "editingDisabled")]
1760 #[ts(optional, rename = "editingDisabled")]
1761 pub editing_disabled: Option<bool>,
1762 #[serde(default, rename = "lineWrap")]
1764 #[ts(optional, rename = "lineWrap")]
1765 pub line_wrap: Option<bool>,
1766 #[serde(default)]
1768 #[ts(optional)]
1769 pub entries: Option<Vec<JsTextPropertyEntry>>,
1770}
1771
1772#[derive(Debug, Clone, Serialize, TS)]
1777#[ts(export, type = "Array<Record<string, unknown>>")]
1778pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
1779
1780#[cfg(feature = "plugins")]
1782mod fromjs_impls {
1783 use super::*;
1784 use rquickjs::{Ctx, FromJs, Value};
1785
1786 impl<'js> FromJs<'js> for JsTextPropertyEntry {
1787 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1788 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1789 from: "object",
1790 to: "JsTextPropertyEntry",
1791 message: Some(e.to_string()),
1792 })
1793 }
1794 }
1795
1796 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
1797 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1798 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1799 from: "object",
1800 to: "CreateVirtualBufferOptions",
1801 message: Some(e.to_string()),
1802 })
1803 }
1804 }
1805
1806 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
1807 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1808 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1809 from: "object",
1810 to: "CreateVirtualBufferInSplitOptions",
1811 message: Some(e.to_string()),
1812 })
1813 }
1814 }
1815
1816 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
1817 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1818 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1819 from: "object",
1820 to: "CreateVirtualBufferInExistingSplitOptions",
1821 message: Some(e.to_string()),
1822 })
1823 }
1824 }
1825
1826 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
1827 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
1828 rquickjs_serde::to_value(ctx.clone(), &self.0)
1829 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
1830 }
1831 }
1832
1833 impl<'js> FromJs<'js> for ActionSpec {
1836 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1837 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1838 from: "object",
1839 to: "ActionSpec",
1840 message: Some(e.to_string()),
1841 })
1842 }
1843 }
1844
1845 impl<'js> FromJs<'js> for ActionPopupAction {
1846 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1847 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1848 from: "object",
1849 to: "ActionPopupAction",
1850 message: Some(e.to_string()),
1851 })
1852 }
1853 }
1854
1855 impl<'js> FromJs<'js> for ActionPopupOptions {
1856 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1857 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1858 from: "object",
1859 to: "ActionPopupOptions",
1860 message: Some(e.to_string()),
1861 })
1862 }
1863 }
1864
1865 impl<'js> FromJs<'js> for ViewTokenWire {
1866 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1867 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1868 from: "object",
1869 to: "ViewTokenWire",
1870 message: Some(e.to_string()),
1871 })
1872 }
1873 }
1874
1875 impl<'js> FromJs<'js> for ViewTokenStyle {
1876 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1877 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1878 from: "object",
1879 to: "ViewTokenStyle",
1880 message: Some(e.to_string()),
1881 })
1882 }
1883 }
1884
1885 impl<'js> FromJs<'js> for LayoutHints {
1886 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1887 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1888 from: "object",
1889 to: "LayoutHints",
1890 message: Some(e.to_string()),
1891 })
1892 }
1893 }
1894
1895 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
1896 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1897 let json: serde_json::Value =
1899 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1900 from: "object",
1901 to: "CreateCompositeBufferOptions (json)",
1902 message: Some(e.to_string()),
1903 })?;
1904 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
1905 from: "json",
1906 to: "CreateCompositeBufferOptions",
1907 message: Some(e.to_string()),
1908 })
1909 }
1910 }
1911
1912 impl<'js> FromJs<'js> for CompositeHunk {
1913 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1914 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1915 from: "object",
1916 to: "CompositeHunk",
1917 message: Some(e.to_string()),
1918 })
1919 }
1920 }
1921
1922 impl<'js> FromJs<'js> for LanguagePackConfig {
1923 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1924 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1925 from: "object",
1926 to: "LanguagePackConfig",
1927 message: Some(e.to_string()),
1928 })
1929 }
1930 }
1931
1932 impl<'js> FromJs<'js> for LspServerPackConfig {
1933 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1934 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1935 from: "object",
1936 to: "LspServerPackConfig",
1937 message: Some(e.to_string()),
1938 })
1939 }
1940 }
1941}
1942
1943pub struct PluginApi {
1945 hooks: Arc<RwLock<HookRegistry>>,
1947
1948 commands: Arc<RwLock<CommandRegistry>>,
1950
1951 command_sender: std::sync::mpsc::Sender<PluginCommand>,
1953
1954 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
1956}
1957
1958impl PluginApi {
1959 pub fn new(
1961 hooks: Arc<RwLock<HookRegistry>>,
1962 commands: Arc<RwLock<CommandRegistry>>,
1963 command_sender: std::sync::mpsc::Sender<PluginCommand>,
1964 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
1965 ) -> Self {
1966 Self {
1967 hooks,
1968 commands,
1969 command_sender,
1970 state_snapshot,
1971 }
1972 }
1973
1974 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
1976 let mut hooks = self.hooks.write().unwrap();
1977 hooks.add_hook(hook_name, callback);
1978 }
1979
1980 pub fn unregister_hooks(&self, hook_name: &str) {
1982 let mut hooks = self.hooks.write().unwrap();
1983 hooks.remove_hooks(hook_name);
1984 }
1985
1986 pub fn register_command(&self, command: Command) {
1988 let commands = self.commands.read().unwrap();
1989 commands.register(command);
1990 }
1991
1992 pub fn unregister_command(&self, name: &str) {
1994 let commands = self.commands.read().unwrap();
1995 commands.unregister(name);
1996 }
1997
1998 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
2000 self.command_sender
2001 .send(command)
2002 .map_err(|e| format!("Failed to send command: {}", e))
2003 }
2004
2005 pub fn insert_text(
2007 &self,
2008 buffer_id: BufferId,
2009 position: usize,
2010 text: String,
2011 ) -> Result<(), String> {
2012 self.send_command(PluginCommand::InsertText {
2013 buffer_id,
2014 position,
2015 text,
2016 })
2017 }
2018
2019 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
2021 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
2022 }
2023
2024 pub fn add_overlay(
2032 &self,
2033 buffer_id: BufferId,
2034 namespace: Option<String>,
2035 range: Range<usize>,
2036 options: OverlayOptions,
2037 ) -> Result<(), String> {
2038 self.send_command(PluginCommand::AddOverlay {
2039 buffer_id,
2040 namespace: namespace.map(OverlayNamespace::from_string),
2041 range,
2042 options,
2043 })
2044 }
2045
2046 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
2048 self.send_command(PluginCommand::RemoveOverlay {
2049 buffer_id,
2050 handle: OverlayHandle::from_string(handle),
2051 })
2052 }
2053
2054 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
2056 self.send_command(PluginCommand::ClearNamespace {
2057 buffer_id,
2058 namespace: OverlayNamespace::from_string(namespace),
2059 })
2060 }
2061
2062 pub fn clear_overlays_in_range(
2065 &self,
2066 buffer_id: BufferId,
2067 start: usize,
2068 end: usize,
2069 ) -> Result<(), String> {
2070 self.send_command(PluginCommand::ClearOverlaysInRange {
2071 buffer_id,
2072 start,
2073 end,
2074 })
2075 }
2076
2077 pub fn set_status(&self, message: String) -> Result<(), String> {
2079 self.send_command(PluginCommand::SetStatus { message })
2080 }
2081
2082 pub fn open_file_at_location(
2085 &self,
2086 path: PathBuf,
2087 line: Option<usize>,
2088 column: Option<usize>,
2089 ) -> Result<(), String> {
2090 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
2091 }
2092
2093 pub fn open_file_in_split(
2098 &self,
2099 split_id: usize,
2100 path: PathBuf,
2101 line: Option<usize>,
2102 column: Option<usize>,
2103 ) -> Result<(), String> {
2104 self.send_command(PluginCommand::OpenFileInSplit {
2105 split_id,
2106 path,
2107 line,
2108 column,
2109 })
2110 }
2111
2112 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
2115 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
2116 }
2117
2118 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
2121 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
2122 }
2123
2124 pub fn add_menu_item(
2126 &self,
2127 menu_label: String,
2128 item: MenuItem,
2129 position: MenuPosition,
2130 ) -> Result<(), String> {
2131 self.send_command(PluginCommand::AddMenuItem {
2132 menu_label,
2133 item,
2134 position,
2135 })
2136 }
2137
2138 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
2140 self.send_command(PluginCommand::AddMenu { menu, position })
2141 }
2142
2143 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
2145 self.send_command(PluginCommand::RemoveMenuItem {
2146 menu_label,
2147 item_label,
2148 })
2149 }
2150
2151 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
2153 self.send_command(PluginCommand::RemoveMenu { menu_label })
2154 }
2155
2156 pub fn create_virtual_buffer(
2163 &self,
2164 name: String,
2165 mode: String,
2166 read_only: bool,
2167 ) -> Result<(), String> {
2168 self.send_command(PluginCommand::CreateVirtualBuffer {
2169 name,
2170 mode,
2171 read_only,
2172 })
2173 }
2174
2175 pub fn create_virtual_buffer_with_content(
2181 &self,
2182 name: String,
2183 mode: String,
2184 read_only: bool,
2185 entries: Vec<TextPropertyEntry>,
2186 ) -> Result<(), String> {
2187 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
2188 name,
2189 mode,
2190 read_only,
2191 entries,
2192 show_line_numbers: true,
2193 show_cursors: true,
2194 editing_disabled: false,
2195 hidden_from_tabs: false,
2196 request_id: None,
2197 })
2198 }
2199
2200 pub fn set_virtual_buffer_content(
2204 &self,
2205 buffer_id: BufferId,
2206 entries: Vec<TextPropertyEntry>,
2207 ) -> Result<(), String> {
2208 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
2209 }
2210
2211 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
2215 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
2216 }
2217
2218 pub fn define_mode(
2223 &self,
2224 name: String,
2225 parent: Option<String>,
2226 bindings: Vec<(String, String)>,
2227 read_only: bool,
2228 ) -> Result<(), String> {
2229 self.send_command(PluginCommand::DefineMode {
2230 name,
2231 parent,
2232 bindings,
2233 read_only,
2234 })
2235 }
2236
2237 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
2239 self.send_command(PluginCommand::ShowBuffer { buffer_id })
2240 }
2241
2242 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
2244 self.send_command(PluginCommand::SetSplitScroll {
2245 split_id: SplitId(split_id),
2246 top_byte,
2247 })
2248 }
2249
2250 pub fn get_highlights(
2252 &self,
2253 buffer_id: BufferId,
2254 range: Range<usize>,
2255 request_id: u64,
2256 ) -> Result<(), String> {
2257 self.send_command(PluginCommand::RequestHighlights {
2258 buffer_id,
2259 range,
2260 request_id,
2261 })
2262 }
2263
2264 pub fn get_active_buffer_id(&self) -> BufferId {
2268 let snapshot = self.state_snapshot.read().unwrap();
2269 snapshot.active_buffer_id
2270 }
2271
2272 pub fn get_active_split_id(&self) -> usize {
2274 let snapshot = self.state_snapshot.read().unwrap();
2275 snapshot.active_split_id
2276 }
2277
2278 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
2280 let snapshot = self.state_snapshot.read().unwrap();
2281 snapshot.buffers.get(&buffer_id).cloned()
2282 }
2283
2284 pub fn list_buffers(&self) -> Vec<BufferInfo> {
2286 let snapshot = self.state_snapshot.read().unwrap();
2287 snapshot.buffers.values().cloned().collect()
2288 }
2289
2290 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
2292 let snapshot = self.state_snapshot.read().unwrap();
2293 snapshot.primary_cursor.clone()
2294 }
2295
2296 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
2298 let snapshot = self.state_snapshot.read().unwrap();
2299 snapshot.all_cursors.clone()
2300 }
2301
2302 pub fn get_viewport(&self) -> Option<ViewportInfo> {
2304 let snapshot = self.state_snapshot.read().unwrap();
2305 snapshot.viewport.clone()
2306 }
2307
2308 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
2310 Arc::clone(&self.state_snapshot)
2311 }
2312}
2313
2314impl Clone for PluginApi {
2315 fn clone(&self) -> Self {
2316 Self {
2317 hooks: Arc::clone(&self.hooks),
2318 commands: Arc::clone(&self.commands),
2319 command_sender: self.command_sender.clone(),
2320 state_snapshot: Arc::clone(&self.state_snapshot),
2321 }
2322 }
2323}
2324
2325#[cfg(test)]
2326mod tests {
2327 use super::*;
2328
2329 #[test]
2330 fn test_plugin_api_creation() {
2331 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2332 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2333 let (tx, _rx) = std::sync::mpsc::channel();
2334 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2335
2336 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2337
2338 let _clone = api.clone();
2340 }
2341
2342 #[test]
2343 fn test_register_hook() {
2344 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2345 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2346 let (tx, _rx) = std::sync::mpsc::channel();
2347 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2348
2349 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
2350
2351 api.register_hook("test-hook", Box::new(|_| true));
2352
2353 let hook_registry = hooks.read().unwrap();
2354 assert_eq!(hook_registry.hook_count("test-hook"), 1);
2355 }
2356
2357 #[test]
2358 fn test_send_command() {
2359 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2360 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2361 let (tx, rx) = std::sync::mpsc::channel();
2362 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2363
2364 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2365
2366 let result = api.insert_text(BufferId(1), 0, "test".to_string());
2367 assert!(result.is_ok());
2368
2369 let received = rx.try_recv();
2371 assert!(received.is_ok());
2372
2373 match received.unwrap() {
2374 PluginCommand::InsertText {
2375 buffer_id,
2376 position,
2377 text,
2378 } => {
2379 assert_eq!(buffer_id.0, 1);
2380 assert_eq!(position, 0);
2381 assert_eq!(text, "test");
2382 }
2383 _ => panic!("Wrong command type"),
2384 }
2385 }
2386
2387 #[test]
2388 fn test_add_overlay_command() {
2389 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2390 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2391 let (tx, rx) = std::sync::mpsc::channel();
2392 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2393
2394 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2395
2396 let result = api.add_overlay(
2397 BufferId(1),
2398 Some("test-overlay".to_string()),
2399 0..10,
2400 OverlayOptions {
2401 fg: Some(OverlayColorSpec::ThemeKey("ui.status_bar_fg".to_string())),
2402 bg: None,
2403 underline: true,
2404 bold: false,
2405 italic: false,
2406 extend_to_line_end: false,
2407 },
2408 );
2409 assert!(result.is_ok());
2410
2411 let received = rx.try_recv().unwrap();
2412 match received {
2413 PluginCommand::AddOverlay {
2414 buffer_id,
2415 namespace,
2416 range,
2417 options,
2418 } => {
2419 assert_eq!(buffer_id.0, 1);
2420 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
2421 assert_eq!(range, 0..10);
2422 assert!(matches!(
2423 options.fg,
2424 Some(OverlayColorSpec::ThemeKey(ref k)) if k == "ui.status_bar_fg"
2425 ));
2426 assert!(options.bg.is_none());
2427 assert!(options.underline);
2428 assert!(!options.bold);
2429 assert!(!options.italic);
2430 assert!(!options.extend_to_line_end);
2431 }
2432 _ => panic!("Wrong command type"),
2433 }
2434 }
2435
2436 #[test]
2437 fn test_set_status_command() {
2438 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2439 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2440 let (tx, rx) = std::sync::mpsc::channel();
2441 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2442
2443 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2444
2445 let result = api.set_status("Test status".to_string());
2446 assert!(result.is_ok());
2447
2448 let received = rx.try_recv().unwrap();
2449 match received {
2450 PluginCommand::SetStatus { message } => {
2451 assert_eq!(message, "Test status");
2452 }
2453 _ => panic!("Wrong command type"),
2454 }
2455 }
2456
2457 #[test]
2458 fn test_get_active_buffer_id() {
2459 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2460 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2461 let (tx, _rx) = std::sync::mpsc::channel();
2462 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2463
2464 {
2466 let mut snapshot = state_snapshot.write().unwrap();
2467 snapshot.active_buffer_id = BufferId(5);
2468 }
2469
2470 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2471
2472 let active_id = api.get_active_buffer_id();
2473 assert_eq!(active_id.0, 5);
2474 }
2475
2476 #[test]
2477 fn test_get_buffer_info() {
2478 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2479 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2480 let (tx, _rx) = std::sync::mpsc::channel();
2481 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2482
2483 {
2485 let mut snapshot = state_snapshot.write().unwrap();
2486 let buffer_info = BufferInfo {
2487 id: BufferId(1),
2488 path: Some(std::path::PathBuf::from("/test/file.txt")),
2489 modified: true,
2490 length: 100,
2491 };
2492 snapshot.buffers.insert(BufferId(1), buffer_info);
2493 }
2494
2495 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2496
2497 let info = api.get_buffer_info(BufferId(1));
2498 assert!(info.is_some());
2499 let info = info.unwrap();
2500 assert_eq!(info.id.0, 1);
2501 assert_eq!(
2502 info.path.as_ref().unwrap().to_str().unwrap(),
2503 "/test/file.txt"
2504 );
2505 assert!(info.modified);
2506 assert_eq!(info.length, 100);
2507
2508 let no_info = api.get_buffer_info(BufferId(999));
2510 assert!(no_info.is_none());
2511 }
2512
2513 #[test]
2514 fn test_list_buffers() {
2515 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2516 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2517 let (tx, _rx) = std::sync::mpsc::channel();
2518 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2519
2520 {
2522 let mut snapshot = state_snapshot.write().unwrap();
2523 snapshot.buffers.insert(
2524 BufferId(1),
2525 BufferInfo {
2526 id: BufferId(1),
2527 path: Some(std::path::PathBuf::from("/file1.txt")),
2528 modified: false,
2529 length: 50,
2530 },
2531 );
2532 snapshot.buffers.insert(
2533 BufferId(2),
2534 BufferInfo {
2535 id: BufferId(2),
2536 path: Some(std::path::PathBuf::from("/file2.txt")),
2537 modified: true,
2538 length: 100,
2539 },
2540 );
2541 snapshot.buffers.insert(
2542 BufferId(3),
2543 BufferInfo {
2544 id: BufferId(3),
2545 path: None,
2546 modified: false,
2547 length: 0,
2548 },
2549 );
2550 }
2551
2552 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2553
2554 let buffers = api.list_buffers();
2555 assert_eq!(buffers.len(), 3);
2556
2557 assert!(buffers.iter().any(|b| b.id.0 == 1));
2559 assert!(buffers.iter().any(|b| b.id.0 == 2));
2560 assert!(buffers.iter().any(|b| b.id.0 == 3));
2561 }
2562
2563 #[test]
2564 fn test_get_primary_cursor() {
2565 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2566 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2567 let (tx, _rx) = std::sync::mpsc::channel();
2568 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2569
2570 {
2572 let mut snapshot = state_snapshot.write().unwrap();
2573 snapshot.primary_cursor = Some(CursorInfo {
2574 position: 42,
2575 selection: Some(10..42),
2576 });
2577 }
2578
2579 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2580
2581 let cursor = api.get_primary_cursor();
2582 assert!(cursor.is_some());
2583 let cursor = cursor.unwrap();
2584 assert_eq!(cursor.position, 42);
2585 assert_eq!(cursor.selection, Some(10..42));
2586 }
2587
2588 #[test]
2589 fn test_get_all_cursors() {
2590 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2591 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2592 let (tx, _rx) = std::sync::mpsc::channel();
2593 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2594
2595 {
2597 let mut snapshot = state_snapshot.write().unwrap();
2598 snapshot.all_cursors = vec![
2599 CursorInfo {
2600 position: 10,
2601 selection: None,
2602 },
2603 CursorInfo {
2604 position: 20,
2605 selection: Some(15..20),
2606 },
2607 CursorInfo {
2608 position: 30,
2609 selection: Some(25..30),
2610 },
2611 ];
2612 }
2613
2614 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2615
2616 let cursors = api.get_all_cursors();
2617 assert_eq!(cursors.len(), 3);
2618 assert_eq!(cursors[0].position, 10);
2619 assert_eq!(cursors[0].selection, None);
2620 assert_eq!(cursors[1].position, 20);
2621 assert_eq!(cursors[1].selection, Some(15..20));
2622 assert_eq!(cursors[2].position, 30);
2623 assert_eq!(cursors[2].selection, Some(25..30));
2624 }
2625
2626 #[test]
2627 fn test_get_viewport() {
2628 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2629 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2630 let (tx, _rx) = std::sync::mpsc::channel();
2631 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2632
2633 {
2635 let mut snapshot = state_snapshot.write().unwrap();
2636 snapshot.viewport = Some(ViewportInfo {
2637 top_byte: 100,
2638 left_column: 5,
2639 width: 80,
2640 height: 24,
2641 });
2642 }
2643
2644 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2645
2646 let viewport = api.get_viewport();
2647 assert!(viewport.is_some());
2648 let viewport = viewport.unwrap();
2649 assert_eq!(viewport.top_byte, 100);
2650 assert_eq!(viewport.left_column, 5);
2651 assert_eq!(viewport.width, 80);
2652 assert_eq!(viewport.height, 24);
2653 }
2654
2655 #[test]
2656 fn test_composite_buffer_options_rejects_unknown_fields() {
2657 let valid_json = r#"{
2659 "name": "test",
2660 "mode": "diff",
2661 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2662 "sources": [{"bufferId": 1, "label": "old"}]
2663 }"#;
2664 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
2665 assert!(
2666 result.is_ok(),
2667 "Valid JSON should parse: {:?}",
2668 result.err()
2669 );
2670
2671 let invalid_json = r#"{
2673 "name": "test",
2674 "mode": "diff",
2675 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2676 "sources": [{"buffer_id": 1, "label": "old"}]
2677 }"#;
2678 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
2679 assert!(
2680 result.is_err(),
2681 "JSON with unknown field should fail to parse"
2682 );
2683 let err = result.unwrap_err().to_string();
2684 assert!(
2685 err.contains("unknown field") || err.contains("buffer_id"),
2686 "Error should mention unknown field: {}",
2687 err
2688 );
2689 }
2690
2691 #[test]
2692 fn test_composite_hunk_rejects_unknown_fields() {
2693 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
2695 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
2696 assert!(
2697 result.is_ok(),
2698 "Valid JSON should parse: {:?}",
2699 result.err()
2700 );
2701
2702 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
2704 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
2705 assert!(
2706 result.is_err(),
2707 "JSON with unknown field should fail to parse"
2708 );
2709 let err = result.unwrap_err().to_string();
2710 assert!(
2711 err.contains("unknown field") || err.contains("old_start"),
2712 "Error should mention unknown field: {}",
2713 err
2714 );
2715 }
2716}