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)]
1455 pub formatter: Option<FormatterPackConfig>,
1456}
1457
1458#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1460#[serde(rename_all = "camelCase")]
1461#[ts(export)]
1462pub struct FormatterPackConfig {
1463 pub command: String,
1465
1466 #[serde(default)]
1468 pub args: Vec<String>,
1469}
1470
1471#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1473#[serde(rename_all = "camelCase")]
1474#[ts(export)]
1475pub struct LspServerPackConfig {
1476 pub command: String,
1478
1479 #[serde(default)]
1481 pub args: Vec<String>,
1482
1483 #[serde(default)]
1485 pub auto_start: Option<bool>,
1486
1487 #[serde(default)]
1489 #[ts(type = "Record<string, unknown> | null")]
1490 pub initialization_options: Option<JsonValue>,
1491}
1492
1493#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1495#[ts(export)]
1496pub enum HunkStatus {
1497 Pending,
1498 Staged,
1499 Discarded,
1500}
1501
1502#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1504#[ts(export)]
1505pub struct ReviewHunk {
1506 pub id: String,
1507 pub file: String,
1508 pub context_header: String,
1509 pub status: HunkStatus,
1510 pub base_range: Option<(usize, usize)>,
1512 pub modified_range: Option<(usize, usize)>,
1514}
1515
1516#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1518#[serde(deny_unknown_fields)]
1519#[ts(export, rename = "TsActionPopupAction")]
1520pub struct ActionPopupAction {
1521 pub id: String,
1523 pub label: String,
1525}
1526
1527#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1529#[serde(deny_unknown_fields)]
1530#[ts(export)]
1531pub struct ActionPopupOptions {
1532 pub id: String,
1534 pub title: String,
1536 pub message: String,
1538 pub actions: Vec<ActionPopupAction>,
1540}
1541
1542#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1544#[ts(export)]
1545pub struct TsHighlightSpan {
1546 pub start: u32,
1547 pub end: u32,
1548 #[ts(type = "[number, number, number]")]
1549 pub color: (u8, u8, u8),
1550 pub bold: bool,
1551 pub italic: bool,
1552}
1553
1554#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1556#[ts(export)]
1557pub struct SpawnResult {
1558 pub stdout: String,
1560 pub stderr: String,
1562 pub exit_code: i32,
1564}
1565
1566#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1568#[ts(export)]
1569pub struct BackgroundProcessResult {
1570 #[ts(type = "number")]
1572 pub process_id: u64,
1573 pub exit_code: i32,
1576}
1577
1578#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1580#[serde(deny_unknown_fields)]
1581#[ts(export, rename = "TextPropertyEntry")]
1582pub struct JsTextPropertyEntry {
1583 pub text: String,
1585 #[serde(default)]
1587 #[ts(optional, type = "Record<string, unknown>")]
1588 pub properties: Option<HashMap<String, JsonValue>>,
1589}
1590
1591#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1593#[ts(export)]
1594pub struct DirEntry {
1595 pub name: String,
1597 pub is_file: bool,
1599 pub is_dir: bool,
1601}
1602
1603#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1605#[ts(export)]
1606pub struct JsPosition {
1607 pub line: u32,
1609 pub character: u32,
1611}
1612
1613#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1615#[ts(export)]
1616pub struct JsRange {
1617 pub start: JsPosition,
1619 pub end: JsPosition,
1621}
1622
1623#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1625#[ts(export)]
1626pub struct JsDiagnostic {
1627 pub uri: String,
1629 pub message: String,
1631 pub severity: Option<u8>,
1633 pub range: JsRange,
1635 #[ts(optional)]
1637 pub source: Option<String>,
1638}
1639
1640#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1642#[serde(deny_unknown_fields)]
1643#[ts(export)]
1644pub struct CreateVirtualBufferOptions {
1645 pub name: String,
1647 #[serde(default)]
1649 #[ts(optional)]
1650 pub mode: Option<String>,
1651 #[serde(default, rename = "readOnly")]
1653 #[ts(optional, rename = "readOnly")]
1654 pub read_only: Option<bool>,
1655 #[serde(default, rename = "showLineNumbers")]
1657 #[ts(optional, rename = "showLineNumbers")]
1658 pub show_line_numbers: Option<bool>,
1659 #[serde(default, rename = "showCursors")]
1661 #[ts(optional, rename = "showCursors")]
1662 pub show_cursors: Option<bool>,
1663 #[serde(default, rename = "editingDisabled")]
1665 #[ts(optional, rename = "editingDisabled")]
1666 pub editing_disabled: Option<bool>,
1667 #[serde(default, rename = "hiddenFromTabs")]
1669 #[ts(optional, rename = "hiddenFromTabs")]
1670 pub hidden_from_tabs: Option<bool>,
1671 #[serde(default)]
1673 #[ts(optional)]
1674 pub entries: Option<Vec<JsTextPropertyEntry>>,
1675}
1676
1677#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1679#[serde(deny_unknown_fields)]
1680#[ts(export)]
1681pub struct CreateVirtualBufferInSplitOptions {
1682 pub name: String,
1684 #[serde(default)]
1686 #[ts(optional)]
1687 pub mode: Option<String>,
1688 #[serde(default, rename = "readOnly")]
1690 #[ts(optional, rename = "readOnly")]
1691 pub read_only: Option<bool>,
1692 #[serde(default)]
1694 #[ts(optional)]
1695 pub ratio: Option<f32>,
1696 #[serde(default)]
1698 #[ts(optional)]
1699 pub direction: Option<String>,
1700 #[serde(default, rename = "panelId")]
1702 #[ts(optional, rename = "panelId")]
1703 pub panel_id: Option<String>,
1704 #[serde(default, rename = "showLineNumbers")]
1706 #[ts(optional, rename = "showLineNumbers")]
1707 pub show_line_numbers: Option<bool>,
1708 #[serde(default, rename = "showCursors")]
1710 #[ts(optional, rename = "showCursors")]
1711 pub show_cursors: Option<bool>,
1712 #[serde(default, rename = "editingDisabled")]
1714 #[ts(optional, rename = "editingDisabled")]
1715 pub editing_disabled: Option<bool>,
1716 #[serde(default, rename = "lineWrap")]
1718 #[ts(optional, rename = "lineWrap")]
1719 pub line_wrap: Option<bool>,
1720 #[serde(default)]
1722 #[ts(optional)]
1723 pub entries: Option<Vec<JsTextPropertyEntry>>,
1724}
1725
1726#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1728#[serde(deny_unknown_fields)]
1729#[ts(export)]
1730pub struct CreateVirtualBufferInExistingSplitOptions {
1731 pub name: String,
1733 #[serde(rename = "splitId")]
1735 #[ts(rename = "splitId")]
1736 pub split_id: usize,
1737 #[serde(default)]
1739 #[ts(optional)]
1740 pub mode: Option<String>,
1741 #[serde(default, rename = "readOnly")]
1743 #[ts(optional, rename = "readOnly")]
1744 pub read_only: Option<bool>,
1745 #[serde(default, rename = "showLineNumbers")]
1747 #[ts(optional, rename = "showLineNumbers")]
1748 pub show_line_numbers: Option<bool>,
1749 #[serde(default, rename = "showCursors")]
1751 #[ts(optional, rename = "showCursors")]
1752 pub show_cursors: Option<bool>,
1753 #[serde(default, rename = "editingDisabled")]
1755 #[ts(optional, rename = "editingDisabled")]
1756 pub editing_disabled: Option<bool>,
1757 #[serde(default, rename = "lineWrap")]
1759 #[ts(optional, rename = "lineWrap")]
1760 pub line_wrap: Option<bool>,
1761 #[serde(default)]
1763 #[ts(optional)]
1764 pub entries: Option<Vec<JsTextPropertyEntry>>,
1765}
1766
1767#[derive(Debug, Clone, Serialize, TS)]
1772#[ts(export, type = "Array<Record<string, unknown>>")]
1773pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
1774
1775#[cfg(feature = "plugins")]
1777mod fromjs_impls {
1778 use super::*;
1779 use rquickjs::{Ctx, FromJs, Value};
1780
1781 impl<'js> FromJs<'js> for JsTextPropertyEntry {
1782 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1783 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1784 from: "object",
1785 to: "JsTextPropertyEntry",
1786 message: Some(e.to_string()),
1787 })
1788 }
1789 }
1790
1791 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
1792 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1793 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1794 from: "object",
1795 to: "CreateVirtualBufferOptions",
1796 message: Some(e.to_string()),
1797 })
1798 }
1799 }
1800
1801 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
1802 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1803 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1804 from: "object",
1805 to: "CreateVirtualBufferInSplitOptions",
1806 message: Some(e.to_string()),
1807 })
1808 }
1809 }
1810
1811 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
1812 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1813 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1814 from: "object",
1815 to: "CreateVirtualBufferInExistingSplitOptions",
1816 message: Some(e.to_string()),
1817 })
1818 }
1819 }
1820
1821 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
1822 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
1823 rquickjs_serde::to_value(ctx.clone(), &self.0)
1824 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
1825 }
1826 }
1827
1828 impl<'js> FromJs<'js> for ActionSpec {
1831 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1832 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1833 from: "object",
1834 to: "ActionSpec",
1835 message: Some(e.to_string()),
1836 })
1837 }
1838 }
1839
1840 impl<'js> FromJs<'js> for ActionPopupAction {
1841 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1842 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1843 from: "object",
1844 to: "ActionPopupAction",
1845 message: Some(e.to_string()),
1846 })
1847 }
1848 }
1849
1850 impl<'js> FromJs<'js> for ActionPopupOptions {
1851 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1852 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1853 from: "object",
1854 to: "ActionPopupOptions",
1855 message: Some(e.to_string()),
1856 })
1857 }
1858 }
1859
1860 impl<'js> FromJs<'js> for ViewTokenWire {
1861 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1862 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1863 from: "object",
1864 to: "ViewTokenWire",
1865 message: Some(e.to_string()),
1866 })
1867 }
1868 }
1869
1870 impl<'js> FromJs<'js> for ViewTokenStyle {
1871 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1872 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1873 from: "object",
1874 to: "ViewTokenStyle",
1875 message: Some(e.to_string()),
1876 })
1877 }
1878 }
1879
1880 impl<'js> FromJs<'js> for LayoutHints {
1881 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1882 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1883 from: "object",
1884 to: "LayoutHints",
1885 message: Some(e.to_string()),
1886 })
1887 }
1888 }
1889
1890 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
1891 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1892 let json: serde_json::Value =
1894 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1895 from: "object",
1896 to: "CreateCompositeBufferOptions (json)",
1897 message: Some(e.to_string()),
1898 })?;
1899 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
1900 from: "json",
1901 to: "CreateCompositeBufferOptions",
1902 message: Some(e.to_string()),
1903 })
1904 }
1905 }
1906
1907 impl<'js> FromJs<'js> for CompositeHunk {
1908 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1909 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1910 from: "object",
1911 to: "CompositeHunk",
1912 message: Some(e.to_string()),
1913 })
1914 }
1915 }
1916
1917 impl<'js> FromJs<'js> for LanguagePackConfig {
1918 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1919 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1920 from: "object",
1921 to: "LanguagePackConfig",
1922 message: Some(e.to_string()),
1923 })
1924 }
1925 }
1926
1927 impl<'js> FromJs<'js> for LspServerPackConfig {
1928 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1929 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1930 from: "object",
1931 to: "LspServerPackConfig",
1932 message: Some(e.to_string()),
1933 })
1934 }
1935 }
1936}
1937
1938pub struct PluginApi {
1940 hooks: Arc<RwLock<HookRegistry>>,
1942
1943 commands: Arc<RwLock<CommandRegistry>>,
1945
1946 command_sender: std::sync::mpsc::Sender<PluginCommand>,
1948
1949 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
1951}
1952
1953impl PluginApi {
1954 pub fn new(
1956 hooks: Arc<RwLock<HookRegistry>>,
1957 commands: Arc<RwLock<CommandRegistry>>,
1958 command_sender: std::sync::mpsc::Sender<PluginCommand>,
1959 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
1960 ) -> Self {
1961 Self {
1962 hooks,
1963 commands,
1964 command_sender,
1965 state_snapshot,
1966 }
1967 }
1968
1969 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
1971 let mut hooks = self.hooks.write().unwrap();
1972 hooks.add_hook(hook_name, callback);
1973 }
1974
1975 pub fn unregister_hooks(&self, hook_name: &str) {
1977 let mut hooks = self.hooks.write().unwrap();
1978 hooks.remove_hooks(hook_name);
1979 }
1980
1981 pub fn register_command(&self, command: Command) {
1983 let commands = self.commands.read().unwrap();
1984 commands.register(command);
1985 }
1986
1987 pub fn unregister_command(&self, name: &str) {
1989 let commands = self.commands.read().unwrap();
1990 commands.unregister(name);
1991 }
1992
1993 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
1995 self.command_sender
1996 .send(command)
1997 .map_err(|e| format!("Failed to send command: {}", e))
1998 }
1999
2000 pub fn insert_text(
2002 &self,
2003 buffer_id: BufferId,
2004 position: usize,
2005 text: String,
2006 ) -> Result<(), String> {
2007 self.send_command(PluginCommand::InsertText {
2008 buffer_id,
2009 position,
2010 text,
2011 })
2012 }
2013
2014 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
2016 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
2017 }
2018
2019 pub fn add_overlay(
2027 &self,
2028 buffer_id: BufferId,
2029 namespace: Option<String>,
2030 range: Range<usize>,
2031 options: OverlayOptions,
2032 ) -> Result<(), String> {
2033 self.send_command(PluginCommand::AddOverlay {
2034 buffer_id,
2035 namespace: namespace.map(OverlayNamespace::from_string),
2036 range,
2037 options,
2038 })
2039 }
2040
2041 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
2043 self.send_command(PluginCommand::RemoveOverlay {
2044 buffer_id,
2045 handle: OverlayHandle::from_string(handle),
2046 })
2047 }
2048
2049 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
2051 self.send_command(PluginCommand::ClearNamespace {
2052 buffer_id,
2053 namespace: OverlayNamespace::from_string(namespace),
2054 })
2055 }
2056
2057 pub fn clear_overlays_in_range(
2060 &self,
2061 buffer_id: BufferId,
2062 start: usize,
2063 end: usize,
2064 ) -> Result<(), String> {
2065 self.send_command(PluginCommand::ClearOverlaysInRange {
2066 buffer_id,
2067 start,
2068 end,
2069 })
2070 }
2071
2072 pub fn set_status(&self, message: String) -> Result<(), String> {
2074 self.send_command(PluginCommand::SetStatus { message })
2075 }
2076
2077 pub fn open_file_at_location(
2080 &self,
2081 path: PathBuf,
2082 line: Option<usize>,
2083 column: Option<usize>,
2084 ) -> Result<(), String> {
2085 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
2086 }
2087
2088 pub fn open_file_in_split(
2093 &self,
2094 split_id: usize,
2095 path: PathBuf,
2096 line: Option<usize>,
2097 column: Option<usize>,
2098 ) -> Result<(), String> {
2099 self.send_command(PluginCommand::OpenFileInSplit {
2100 split_id,
2101 path,
2102 line,
2103 column,
2104 })
2105 }
2106
2107 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
2110 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
2111 }
2112
2113 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
2116 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
2117 }
2118
2119 pub fn add_menu_item(
2121 &self,
2122 menu_label: String,
2123 item: MenuItem,
2124 position: MenuPosition,
2125 ) -> Result<(), String> {
2126 self.send_command(PluginCommand::AddMenuItem {
2127 menu_label,
2128 item,
2129 position,
2130 })
2131 }
2132
2133 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
2135 self.send_command(PluginCommand::AddMenu { menu, position })
2136 }
2137
2138 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
2140 self.send_command(PluginCommand::RemoveMenuItem {
2141 menu_label,
2142 item_label,
2143 })
2144 }
2145
2146 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
2148 self.send_command(PluginCommand::RemoveMenu { menu_label })
2149 }
2150
2151 pub fn create_virtual_buffer(
2158 &self,
2159 name: String,
2160 mode: String,
2161 read_only: bool,
2162 ) -> Result<(), String> {
2163 self.send_command(PluginCommand::CreateVirtualBuffer {
2164 name,
2165 mode,
2166 read_only,
2167 })
2168 }
2169
2170 pub fn create_virtual_buffer_with_content(
2176 &self,
2177 name: String,
2178 mode: String,
2179 read_only: bool,
2180 entries: Vec<TextPropertyEntry>,
2181 ) -> Result<(), String> {
2182 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
2183 name,
2184 mode,
2185 read_only,
2186 entries,
2187 show_line_numbers: true,
2188 show_cursors: true,
2189 editing_disabled: false,
2190 hidden_from_tabs: false,
2191 request_id: None,
2192 })
2193 }
2194
2195 pub fn set_virtual_buffer_content(
2199 &self,
2200 buffer_id: BufferId,
2201 entries: Vec<TextPropertyEntry>,
2202 ) -> Result<(), String> {
2203 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
2204 }
2205
2206 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
2210 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
2211 }
2212
2213 pub fn define_mode(
2218 &self,
2219 name: String,
2220 parent: Option<String>,
2221 bindings: Vec<(String, String)>,
2222 read_only: bool,
2223 ) -> Result<(), String> {
2224 self.send_command(PluginCommand::DefineMode {
2225 name,
2226 parent,
2227 bindings,
2228 read_only,
2229 })
2230 }
2231
2232 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
2234 self.send_command(PluginCommand::ShowBuffer { buffer_id })
2235 }
2236
2237 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
2239 self.send_command(PluginCommand::SetSplitScroll {
2240 split_id: SplitId(split_id),
2241 top_byte,
2242 })
2243 }
2244
2245 pub fn get_highlights(
2247 &self,
2248 buffer_id: BufferId,
2249 range: Range<usize>,
2250 request_id: u64,
2251 ) -> Result<(), String> {
2252 self.send_command(PluginCommand::RequestHighlights {
2253 buffer_id,
2254 range,
2255 request_id,
2256 })
2257 }
2258
2259 pub fn get_active_buffer_id(&self) -> BufferId {
2263 let snapshot = self.state_snapshot.read().unwrap();
2264 snapshot.active_buffer_id
2265 }
2266
2267 pub fn get_active_split_id(&self) -> usize {
2269 let snapshot = self.state_snapshot.read().unwrap();
2270 snapshot.active_split_id
2271 }
2272
2273 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
2275 let snapshot = self.state_snapshot.read().unwrap();
2276 snapshot.buffers.get(&buffer_id).cloned()
2277 }
2278
2279 pub fn list_buffers(&self) -> Vec<BufferInfo> {
2281 let snapshot = self.state_snapshot.read().unwrap();
2282 snapshot.buffers.values().cloned().collect()
2283 }
2284
2285 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
2287 let snapshot = self.state_snapshot.read().unwrap();
2288 snapshot.primary_cursor.clone()
2289 }
2290
2291 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
2293 let snapshot = self.state_snapshot.read().unwrap();
2294 snapshot.all_cursors.clone()
2295 }
2296
2297 pub fn get_viewport(&self) -> Option<ViewportInfo> {
2299 let snapshot = self.state_snapshot.read().unwrap();
2300 snapshot.viewport.clone()
2301 }
2302
2303 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
2305 Arc::clone(&self.state_snapshot)
2306 }
2307}
2308
2309impl Clone for PluginApi {
2310 fn clone(&self) -> Self {
2311 Self {
2312 hooks: Arc::clone(&self.hooks),
2313 commands: Arc::clone(&self.commands),
2314 command_sender: self.command_sender.clone(),
2315 state_snapshot: Arc::clone(&self.state_snapshot),
2316 }
2317 }
2318}
2319
2320#[cfg(test)]
2321mod tests {
2322 use super::*;
2323
2324 #[test]
2325 fn test_plugin_api_creation() {
2326 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2327 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2328 let (tx, _rx) = std::sync::mpsc::channel();
2329 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2330
2331 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2332
2333 let _clone = api.clone();
2335 }
2336
2337 #[test]
2338 fn test_register_hook() {
2339 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2340 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2341 let (tx, _rx) = std::sync::mpsc::channel();
2342 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2343
2344 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
2345
2346 api.register_hook("test-hook", Box::new(|_| true));
2347
2348 let hook_registry = hooks.read().unwrap();
2349 assert_eq!(hook_registry.hook_count("test-hook"), 1);
2350 }
2351
2352 #[test]
2353 fn test_send_command() {
2354 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2355 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2356 let (tx, rx) = std::sync::mpsc::channel();
2357 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2358
2359 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2360
2361 let result = api.insert_text(BufferId(1), 0, "test".to_string());
2362 assert!(result.is_ok());
2363
2364 let received = rx.try_recv();
2366 assert!(received.is_ok());
2367
2368 match received.unwrap() {
2369 PluginCommand::InsertText {
2370 buffer_id,
2371 position,
2372 text,
2373 } => {
2374 assert_eq!(buffer_id.0, 1);
2375 assert_eq!(position, 0);
2376 assert_eq!(text, "test");
2377 }
2378 _ => panic!("Wrong command type"),
2379 }
2380 }
2381
2382 #[test]
2383 fn test_add_overlay_command() {
2384 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2385 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2386 let (tx, rx) = std::sync::mpsc::channel();
2387 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2388
2389 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2390
2391 let result = api.add_overlay(
2392 BufferId(1),
2393 Some("test-overlay".to_string()),
2394 0..10,
2395 OverlayOptions {
2396 fg: Some(OverlayColorSpec::ThemeKey("ui.status_bar_fg".to_string())),
2397 bg: None,
2398 underline: true,
2399 bold: false,
2400 italic: false,
2401 extend_to_line_end: false,
2402 },
2403 );
2404 assert!(result.is_ok());
2405
2406 let received = rx.try_recv().unwrap();
2407 match received {
2408 PluginCommand::AddOverlay {
2409 buffer_id,
2410 namespace,
2411 range,
2412 options,
2413 } => {
2414 assert_eq!(buffer_id.0, 1);
2415 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
2416 assert_eq!(range, 0..10);
2417 assert!(matches!(
2418 options.fg,
2419 Some(OverlayColorSpec::ThemeKey(ref k)) if k == "ui.status_bar_fg"
2420 ));
2421 assert!(options.bg.is_none());
2422 assert!(options.underline);
2423 assert!(!options.bold);
2424 assert!(!options.italic);
2425 assert!(!options.extend_to_line_end);
2426 }
2427 _ => panic!("Wrong command type"),
2428 }
2429 }
2430
2431 #[test]
2432 fn test_set_status_command() {
2433 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2434 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2435 let (tx, rx) = std::sync::mpsc::channel();
2436 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2437
2438 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2439
2440 let result = api.set_status("Test status".to_string());
2441 assert!(result.is_ok());
2442
2443 let received = rx.try_recv().unwrap();
2444 match received {
2445 PluginCommand::SetStatus { message } => {
2446 assert_eq!(message, "Test status");
2447 }
2448 _ => panic!("Wrong command type"),
2449 }
2450 }
2451
2452 #[test]
2453 fn test_get_active_buffer_id() {
2454 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2455 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2456 let (tx, _rx) = std::sync::mpsc::channel();
2457 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2458
2459 {
2461 let mut snapshot = state_snapshot.write().unwrap();
2462 snapshot.active_buffer_id = BufferId(5);
2463 }
2464
2465 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2466
2467 let active_id = api.get_active_buffer_id();
2468 assert_eq!(active_id.0, 5);
2469 }
2470
2471 #[test]
2472 fn test_get_buffer_info() {
2473 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2474 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2475 let (tx, _rx) = std::sync::mpsc::channel();
2476 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2477
2478 {
2480 let mut snapshot = state_snapshot.write().unwrap();
2481 let buffer_info = BufferInfo {
2482 id: BufferId(1),
2483 path: Some(std::path::PathBuf::from("/test/file.txt")),
2484 modified: true,
2485 length: 100,
2486 };
2487 snapshot.buffers.insert(BufferId(1), buffer_info);
2488 }
2489
2490 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2491
2492 let info = api.get_buffer_info(BufferId(1));
2493 assert!(info.is_some());
2494 let info = info.unwrap();
2495 assert_eq!(info.id.0, 1);
2496 assert_eq!(
2497 info.path.as_ref().unwrap().to_str().unwrap(),
2498 "/test/file.txt"
2499 );
2500 assert!(info.modified);
2501 assert_eq!(info.length, 100);
2502
2503 let no_info = api.get_buffer_info(BufferId(999));
2505 assert!(no_info.is_none());
2506 }
2507
2508 #[test]
2509 fn test_list_buffers() {
2510 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2511 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2512 let (tx, _rx) = std::sync::mpsc::channel();
2513 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2514
2515 {
2517 let mut snapshot = state_snapshot.write().unwrap();
2518 snapshot.buffers.insert(
2519 BufferId(1),
2520 BufferInfo {
2521 id: BufferId(1),
2522 path: Some(std::path::PathBuf::from("/file1.txt")),
2523 modified: false,
2524 length: 50,
2525 },
2526 );
2527 snapshot.buffers.insert(
2528 BufferId(2),
2529 BufferInfo {
2530 id: BufferId(2),
2531 path: Some(std::path::PathBuf::from("/file2.txt")),
2532 modified: true,
2533 length: 100,
2534 },
2535 );
2536 snapshot.buffers.insert(
2537 BufferId(3),
2538 BufferInfo {
2539 id: BufferId(3),
2540 path: None,
2541 modified: false,
2542 length: 0,
2543 },
2544 );
2545 }
2546
2547 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2548
2549 let buffers = api.list_buffers();
2550 assert_eq!(buffers.len(), 3);
2551
2552 assert!(buffers.iter().any(|b| b.id.0 == 1));
2554 assert!(buffers.iter().any(|b| b.id.0 == 2));
2555 assert!(buffers.iter().any(|b| b.id.0 == 3));
2556 }
2557
2558 #[test]
2559 fn test_get_primary_cursor() {
2560 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2561 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2562 let (tx, _rx) = std::sync::mpsc::channel();
2563 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2564
2565 {
2567 let mut snapshot = state_snapshot.write().unwrap();
2568 snapshot.primary_cursor = Some(CursorInfo {
2569 position: 42,
2570 selection: Some(10..42),
2571 });
2572 }
2573
2574 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2575
2576 let cursor = api.get_primary_cursor();
2577 assert!(cursor.is_some());
2578 let cursor = cursor.unwrap();
2579 assert_eq!(cursor.position, 42);
2580 assert_eq!(cursor.selection, Some(10..42));
2581 }
2582
2583 #[test]
2584 fn test_get_all_cursors() {
2585 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2586 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2587 let (tx, _rx) = std::sync::mpsc::channel();
2588 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2589
2590 {
2592 let mut snapshot = state_snapshot.write().unwrap();
2593 snapshot.all_cursors = vec![
2594 CursorInfo {
2595 position: 10,
2596 selection: None,
2597 },
2598 CursorInfo {
2599 position: 20,
2600 selection: Some(15..20),
2601 },
2602 CursorInfo {
2603 position: 30,
2604 selection: Some(25..30),
2605 },
2606 ];
2607 }
2608
2609 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2610
2611 let cursors = api.get_all_cursors();
2612 assert_eq!(cursors.len(), 3);
2613 assert_eq!(cursors[0].position, 10);
2614 assert_eq!(cursors[0].selection, None);
2615 assert_eq!(cursors[1].position, 20);
2616 assert_eq!(cursors[1].selection, Some(15..20));
2617 assert_eq!(cursors[2].position, 30);
2618 assert_eq!(cursors[2].selection, Some(25..30));
2619 }
2620
2621 #[test]
2622 fn test_get_viewport() {
2623 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2624 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2625 let (tx, _rx) = std::sync::mpsc::channel();
2626 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2627
2628 {
2630 let mut snapshot = state_snapshot.write().unwrap();
2631 snapshot.viewport = Some(ViewportInfo {
2632 top_byte: 100,
2633 left_column: 5,
2634 width: 80,
2635 height: 24,
2636 });
2637 }
2638
2639 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2640
2641 let viewport = api.get_viewport();
2642 assert!(viewport.is_some());
2643 let viewport = viewport.unwrap();
2644 assert_eq!(viewport.top_byte, 100);
2645 assert_eq!(viewport.left_column, 5);
2646 assert_eq!(viewport.width, 80);
2647 assert_eq!(viewport.height, 24);
2648 }
2649
2650 #[test]
2651 fn test_composite_buffer_options_rejects_unknown_fields() {
2652 let valid_json = r#"{
2654 "name": "test",
2655 "mode": "diff",
2656 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2657 "sources": [{"bufferId": 1, "label": "old"}]
2658 }"#;
2659 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
2660 assert!(
2661 result.is_ok(),
2662 "Valid JSON should parse: {:?}",
2663 result.err()
2664 );
2665
2666 let invalid_json = r#"{
2668 "name": "test",
2669 "mode": "diff",
2670 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2671 "sources": [{"buffer_id": 1, "label": "old"}]
2672 }"#;
2673 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
2674 assert!(
2675 result.is_err(),
2676 "JSON with unknown field should fail to parse"
2677 );
2678 let err = result.unwrap_err().to_string();
2679 assert!(
2680 err.contains("unknown field") || err.contains("buffer_id"),
2681 "Error should mention unknown field: {}",
2682 err
2683 );
2684 }
2685
2686 #[test]
2687 fn test_composite_hunk_rejects_unknown_fields() {
2688 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
2690 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
2691 assert!(
2692 result.is_ok(),
2693 "Valid JSON should parse: {:?}",
2694 result.err()
2695 );
2696
2697 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
2699 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
2700 assert!(
2701 result.is_err(),
2702 "JSON with unknown field should fail to parse"
2703 );
2704 let err = result.unwrap_err().to_string();
2705 assert!(
2706 err.contains("unknown field") || err.contains("old_start"),
2707 "Error should mention unknown field: {}",
2708 err
2709 );
2710 }
2711}