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 LineEndPosition {
184 request_id: u64,
185 position: Option<usize>,
187 },
188 BufferLineCount {
190 request_id: u64,
191 count: Option<usize>,
193 },
194 CompositeBufferCreated {
196 request_id: u64,
197 buffer_id: BufferId,
198 },
199}
200
201#[derive(Debug, Clone, Serialize, Deserialize, TS)]
203#[ts(export)]
204pub enum PluginAsyncMessage {
205 ProcessOutput {
207 process_id: u64,
209 stdout: String,
211 stderr: String,
213 exit_code: i32,
215 },
216 DelayComplete {
218 callback_id: u64,
220 },
221 ProcessStdout { process_id: u64, data: String },
223 ProcessStderr { process_id: u64, data: String },
225 ProcessExit {
227 process_id: u64,
228 callback_id: u64,
229 exit_code: i32,
230 },
231 LspResponse {
233 language: String,
234 request_id: u64,
235 #[ts(type = "any")]
236 result: Result<JsonValue, String>,
237 },
238 PluginResponse(crate::api::PluginResponse),
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize, TS)]
244#[ts(export)]
245pub struct CursorInfo {
246 pub position: usize,
248 #[cfg_attr(
250 feature = "plugins",
251 ts(type = "{ start: number; end: number } | null")
252 )]
253 pub selection: Option<Range<usize>>,
254}
255
256#[derive(Debug, Clone, Serialize, Deserialize, TS)]
258#[serde(deny_unknown_fields)]
259#[ts(export)]
260pub struct ActionSpec {
261 pub action: String,
263 #[serde(default = "default_action_count")]
265 pub count: u32,
266}
267
268fn default_action_count() -> u32 {
269 1
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize, TS)]
274#[ts(export)]
275pub struct BufferInfo {
276 #[ts(type = "number")]
278 pub id: BufferId,
279 #[serde(serialize_with = "serialize_path")]
281 #[ts(type = "string")]
282 pub path: Option<PathBuf>,
283 pub modified: bool,
285 pub length: usize,
287}
288
289fn serialize_path<S: serde::Serializer>(path: &Option<PathBuf>, s: S) -> Result<S::Ok, S::Error> {
290 s.serialize_str(
291 &path
292 .as_ref()
293 .map(|p| p.to_string_lossy().to_string())
294 .unwrap_or_default(),
295 )
296}
297
298fn serialize_ranges_as_tuples<S>(ranges: &[Range<usize>], serializer: S) -> Result<S::Ok, S::Error>
300where
301 S: serde::Serializer,
302{
303 use serde::ser::SerializeSeq;
304 let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
305 for range in ranges {
306 seq.serialize_element(&(range.start, range.end))?;
307 }
308 seq.end()
309}
310
311fn serialize_opt_ranges_as_tuples<S>(
313 ranges: &Option<Vec<Range<usize>>>,
314 serializer: S,
315) -> Result<S::Ok, S::Error>
316where
317 S: serde::Serializer,
318{
319 match ranges {
320 Some(ranges) => {
321 use serde::ser::SerializeSeq;
322 let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
323 for range in ranges {
324 seq.serialize_element(&(range.start, range.end))?;
325 }
326 seq.end()
327 }
328 None => serializer.serialize_none(),
329 }
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize, TS)]
334#[ts(export)]
335pub struct BufferSavedDiff {
336 pub equal: bool,
337 #[serde(serialize_with = "serialize_ranges_as_tuples")]
338 #[ts(type = "Array<[number, number]>")]
339 pub byte_ranges: Vec<Range<usize>>,
340 #[serde(serialize_with = "serialize_opt_ranges_as_tuples")]
341 #[ts(type = "Array<[number, number]> | null")]
342 pub line_ranges: Option<Vec<Range<usize>>>,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize, TS)]
347#[serde(rename_all = "camelCase")]
348#[ts(export, rename_all = "camelCase")]
349pub struct ViewportInfo {
350 pub top_byte: usize,
352 pub left_column: usize,
354 pub width: u16,
356 pub height: u16,
358}
359
360#[derive(Debug, Clone, Serialize, Deserialize, TS)]
362#[serde(rename_all = "camelCase")]
363#[ts(export, rename_all = "camelCase")]
364pub struct LayoutHints {
365 pub compose_width: Option<u16>,
367 pub column_guides: Option<Vec<u16>>,
369}
370
371#[derive(Debug, Clone, Serialize, Deserialize, TS)]
386#[serde(untagged)]
387#[ts(export)]
388pub enum OverlayColorSpec {
389 #[ts(type = "[number, number, number]")]
391 Rgb(u8, u8, u8),
392 ThemeKey(String),
394}
395
396impl OverlayColorSpec {
397 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
399 Self::Rgb(r, g, b)
400 }
401
402 pub fn theme_key(key: impl Into<String>) -> Self {
404 Self::ThemeKey(key.into())
405 }
406
407 pub fn as_rgb(&self) -> Option<(u8, u8, u8)> {
409 match self {
410 Self::Rgb(r, g, b) => Some((*r, *g, *b)),
411 Self::ThemeKey(_) => None,
412 }
413 }
414
415 pub fn as_theme_key(&self) -> Option<&str> {
417 match self {
418 Self::ThemeKey(key) => Some(key),
419 Self::Rgb(_, _, _) => None,
420 }
421 }
422}
423
424#[derive(Debug, Clone, Serialize, Deserialize, TS)]
429#[serde(deny_unknown_fields, rename_all = "camelCase")]
430#[ts(export, rename_all = "camelCase")]
431pub struct OverlayOptions {
432 #[serde(default, skip_serializing_if = "Option::is_none")]
434 pub fg: Option<OverlayColorSpec>,
435
436 #[serde(default, skip_serializing_if = "Option::is_none")]
438 pub bg: Option<OverlayColorSpec>,
439
440 #[serde(default)]
442 pub underline: bool,
443
444 #[serde(default)]
446 pub bold: bool,
447
448 #[serde(default)]
450 pub italic: bool,
451
452 #[serde(default)]
454 pub extend_to_line_end: bool,
455}
456
457impl Default for OverlayOptions {
458 fn default() -> Self {
459 Self {
460 fg: None,
461 bg: None,
462 underline: false,
463 bold: false,
464 italic: false,
465 extend_to_line_end: false,
466 }
467 }
468}
469
470#[derive(Debug, Clone, Serialize, Deserialize, TS)]
476#[serde(deny_unknown_fields)]
477#[ts(export, rename = "TsCompositeLayoutConfig")]
478pub struct CompositeLayoutConfig {
479 #[serde(rename = "type")]
481 #[ts(rename = "type")]
482 pub layout_type: String,
483 #[serde(default)]
485 pub ratios: Option<Vec<f32>>,
486 #[serde(default = "default_true", rename = "showSeparator")]
488 #[ts(rename = "showSeparator")]
489 pub show_separator: bool,
490 #[serde(default)]
492 pub spacing: Option<u16>,
493}
494
495fn default_true() -> bool {
496 true
497}
498
499#[derive(Debug, Clone, Serialize, Deserialize, TS)]
501#[serde(deny_unknown_fields)]
502#[ts(export, rename = "TsCompositeSourceConfig")]
503pub struct CompositeSourceConfig {
504 #[serde(rename = "bufferId")]
506 #[ts(rename = "bufferId")]
507 pub buffer_id: usize,
508 pub label: String,
510 #[serde(default)]
512 pub editable: bool,
513 #[serde(default)]
515 pub style: Option<CompositePaneStyle>,
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
520#[serde(deny_unknown_fields)]
521#[ts(export, rename = "TsCompositePaneStyle")]
522pub struct CompositePaneStyle {
523 #[serde(default, rename = "addBg")]
526 #[ts(rename = "addBg", type = "[number, number, number] | null")]
527 pub add_bg: Option<[u8; 3]>,
528 #[serde(default, rename = "removeBg")]
530 #[ts(rename = "removeBg", type = "[number, number, number] | null")]
531 pub remove_bg: Option<[u8; 3]>,
532 #[serde(default, rename = "modifyBg")]
534 #[ts(rename = "modifyBg", type = "[number, number, number] | null")]
535 pub modify_bg: Option<[u8; 3]>,
536 #[serde(default, rename = "gutterStyle")]
538 #[ts(rename = "gutterStyle")]
539 pub gutter_style: Option<String>,
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize, TS)]
544#[serde(deny_unknown_fields)]
545#[ts(export, rename = "TsCompositeHunk")]
546pub struct CompositeHunk {
547 #[serde(rename = "oldStart")]
549 #[ts(rename = "oldStart")]
550 pub old_start: usize,
551 #[serde(rename = "oldCount")]
553 #[ts(rename = "oldCount")]
554 pub old_count: usize,
555 #[serde(rename = "newStart")]
557 #[ts(rename = "newStart")]
558 pub new_start: usize,
559 #[serde(rename = "newCount")]
561 #[ts(rename = "newCount")]
562 pub new_count: usize,
563}
564
565#[derive(Debug, Clone, Serialize, Deserialize, TS)]
567#[serde(deny_unknown_fields)]
568#[ts(export, rename = "TsCreateCompositeBufferOptions")]
569pub struct CreateCompositeBufferOptions {
570 #[serde(default)]
572 pub name: String,
573 #[serde(default)]
575 pub mode: String,
576 pub layout: CompositeLayoutConfig,
578 pub sources: Vec<CompositeSourceConfig>,
580 #[serde(default)]
582 pub hunks: Option<Vec<CompositeHunk>>,
583}
584
585#[derive(Debug, Clone, Serialize, Deserialize, TS)]
587#[ts(export)]
588pub enum ViewTokenWireKind {
589 Text(String),
590 Newline,
591 Space,
592 Break,
595 BinaryByte(u8),
599}
600
601#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
607#[serde(deny_unknown_fields)]
608#[ts(export)]
609pub struct ViewTokenStyle {
610 #[serde(default)]
612 #[ts(type = "[number, number, number] | null")]
613 pub fg: Option<(u8, u8, u8)>,
614 #[serde(default)]
616 #[ts(type = "[number, number, number] | null")]
617 pub bg: Option<(u8, u8, u8)>,
618 #[serde(default)]
620 pub bold: bool,
621 #[serde(default)]
623 pub italic: bool,
624}
625
626#[derive(Debug, Clone, Serialize, Deserialize, TS)]
628#[serde(deny_unknown_fields)]
629#[ts(export)]
630pub struct ViewTokenWire {
631 #[ts(type = "number | null")]
633 pub source_offset: Option<usize>,
634 pub kind: ViewTokenWireKind,
636 #[serde(default)]
638 #[ts(optional)]
639 pub style: Option<ViewTokenStyle>,
640}
641
642#[derive(Debug, Clone, Serialize, Deserialize, TS)]
644#[ts(export)]
645pub struct ViewTransformPayload {
646 pub range: Range<usize>,
648 pub tokens: Vec<ViewTokenWire>,
650 pub layout_hints: Option<LayoutHints>,
652}
653
654#[derive(Debug, Clone, Serialize, Deserialize, TS)]
657#[ts(export)]
658pub struct EditorStateSnapshot {
659 pub active_buffer_id: BufferId,
661 pub active_split_id: usize,
663 pub buffers: HashMap<BufferId, BufferInfo>,
665 pub buffer_saved_diffs: HashMap<BufferId, BufferSavedDiff>,
667 pub primary_cursor: Option<CursorInfo>,
669 pub all_cursors: Vec<CursorInfo>,
671 pub viewport: Option<ViewportInfo>,
673 pub buffer_cursor_positions: HashMap<BufferId, usize>,
675 pub buffer_text_properties: HashMap<BufferId, Vec<TextProperty>>,
677 pub selected_text: Option<String>,
680 pub clipboard: String,
682 pub working_dir: PathBuf,
684 #[ts(type = "any")]
687 pub diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
688 #[ts(type = "any")]
691 pub config: serde_json::Value,
692 #[ts(type = "any")]
695 pub user_config: serde_json::Value,
696 pub editor_mode: Option<String>,
699}
700
701impl EditorStateSnapshot {
702 pub fn new() -> Self {
703 Self {
704 active_buffer_id: BufferId(0),
705 active_split_id: 0,
706 buffers: HashMap::new(),
707 buffer_saved_diffs: HashMap::new(),
708 primary_cursor: None,
709 all_cursors: Vec::new(),
710 viewport: None,
711 buffer_cursor_positions: HashMap::new(),
712 buffer_text_properties: HashMap::new(),
713 selected_text: None,
714 clipboard: String::new(),
715 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
716 diagnostics: HashMap::new(),
717 config: serde_json::Value::Null,
718 user_config: serde_json::Value::Null,
719 editor_mode: None,
720 }
721 }
722}
723
724impl Default for EditorStateSnapshot {
725 fn default() -> Self {
726 Self::new()
727 }
728}
729
730#[derive(Debug, Clone, Serialize, Deserialize, TS)]
732#[ts(export)]
733pub enum MenuPosition {
734 Top,
736 Bottom,
738 Before(String),
740 After(String),
742}
743
744#[derive(Debug, Clone, Serialize, Deserialize, TS)]
746#[ts(export)]
747pub enum PluginCommand {
748 InsertText {
750 buffer_id: BufferId,
751 position: usize,
752 text: String,
753 },
754
755 DeleteRange {
757 buffer_id: BufferId,
758 range: Range<usize>,
759 },
760
761 AddOverlay {
766 buffer_id: BufferId,
767 namespace: Option<OverlayNamespace>,
768 range: Range<usize>,
769 options: OverlayOptions,
771 },
772
773 RemoveOverlay {
775 buffer_id: BufferId,
776 handle: OverlayHandle,
777 },
778
779 SetStatus { message: String },
781
782 ApplyTheme { theme_name: String },
784
785 ReloadConfig,
788
789 RegisterCommand { command: Command },
791
792 UnregisterCommand { name: String },
794
795 OpenFileInBackground { path: PathBuf },
797
798 InsertAtCursor { text: String },
800
801 SpawnProcess {
803 command: String,
804 args: Vec<String>,
805 cwd: Option<String>,
806 callback_id: JsCallbackId,
807 },
808
809 Delay {
811 callback_id: JsCallbackId,
812 duration_ms: u64,
813 },
814
815 SpawnBackgroundProcess {
819 process_id: u64,
821 command: String,
823 args: Vec<String>,
825 cwd: Option<String>,
827 callback_id: JsCallbackId,
829 },
830
831 KillBackgroundProcess { process_id: u64 },
833
834 SpawnProcessWait {
837 process_id: u64,
839 callback_id: JsCallbackId,
841 },
842
843 SetLayoutHints {
845 buffer_id: BufferId,
846 split_id: Option<SplitId>,
847 range: Range<usize>,
848 hints: LayoutHints,
849 },
850
851 SetLineNumbers { buffer_id: BufferId, enabled: bool },
853
854 SetLineWrap {
856 buffer_id: BufferId,
857 split_id: Option<SplitId>,
858 enabled: bool,
859 },
860
861 SubmitViewTransform {
863 buffer_id: BufferId,
864 split_id: Option<SplitId>,
865 payload: ViewTransformPayload,
866 },
867
868 ClearViewTransform {
870 buffer_id: BufferId,
871 split_id: Option<SplitId>,
872 },
873
874 ClearAllOverlays { buffer_id: BufferId },
876
877 ClearNamespace {
879 buffer_id: BufferId,
880 namespace: OverlayNamespace,
881 },
882
883 ClearOverlaysInRange {
886 buffer_id: BufferId,
887 start: usize,
888 end: usize,
889 },
890
891 AddVirtualText {
894 buffer_id: BufferId,
895 virtual_text_id: String,
896 position: usize,
897 text: String,
898 color: (u8, u8, u8),
899 use_bg: bool, before: bool, },
902
903 RemoveVirtualText {
905 buffer_id: BufferId,
906 virtual_text_id: String,
907 },
908
909 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
911
912 ClearVirtualTexts { buffer_id: BufferId },
914
915 AddVirtualLine {
919 buffer_id: BufferId,
920 position: usize,
922 text: String,
924 fg_color: (u8, u8, u8),
926 bg_color: Option<(u8, u8, u8)>,
928 above: bool,
930 namespace: String,
932 priority: i32,
934 },
935
936 ClearVirtualTextNamespace {
939 buffer_id: BufferId,
940 namespace: String,
941 },
942
943 RefreshLines { buffer_id: BufferId },
945
946 SetLineIndicator {
949 buffer_id: BufferId,
950 line: usize,
952 namespace: String,
954 symbol: String,
956 color: (u8, u8, u8),
958 priority: i32,
960 },
961
962 ClearLineIndicators {
964 buffer_id: BufferId,
965 namespace: String,
967 },
968
969 SetFileExplorerDecorations {
971 namespace: String,
973 decorations: Vec<FileExplorerDecoration>,
975 },
976
977 ClearFileExplorerDecorations {
979 namespace: String,
981 },
982
983 OpenFileAtLocation {
986 path: PathBuf,
987 line: Option<usize>, column: Option<usize>, },
990
991 OpenFileInSplit {
994 split_id: usize,
995 path: PathBuf,
996 line: Option<usize>, column: Option<usize>, },
999
1000 StartPrompt {
1003 label: String,
1004 prompt_type: String, },
1006
1007 StartPromptWithInitial {
1009 label: String,
1010 prompt_type: String,
1011 initial_value: String,
1012 },
1013
1014 StartPromptAsync {
1017 label: String,
1018 initial_value: String,
1019 callback_id: JsCallbackId,
1020 },
1021
1022 SetPromptSuggestions { suggestions: Vec<Suggestion> },
1025
1026 AddMenuItem {
1029 menu_label: String,
1030 item: MenuItem,
1031 position: MenuPosition,
1032 },
1033
1034 AddMenu { menu: Menu, position: MenuPosition },
1036
1037 RemoveMenuItem {
1039 menu_label: String,
1040 item_label: String,
1041 },
1042
1043 RemoveMenu { menu_label: String },
1045
1046 CreateVirtualBuffer {
1048 name: String,
1050 mode: String,
1052 read_only: bool,
1054 },
1055
1056 CreateVirtualBufferWithContent {
1060 name: String,
1062 mode: String,
1064 read_only: bool,
1066 entries: Vec<TextPropertyEntry>,
1068 show_line_numbers: bool,
1070 show_cursors: bool,
1072 editing_disabled: bool,
1074 hidden_from_tabs: bool,
1076 request_id: Option<u64>,
1078 },
1079
1080 CreateVirtualBufferInSplit {
1083 name: String,
1085 mode: String,
1087 read_only: bool,
1089 entries: Vec<TextPropertyEntry>,
1091 ratio: f32,
1093 direction: Option<String>,
1095 panel_id: Option<String>,
1097 show_line_numbers: bool,
1099 show_cursors: bool,
1101 editing_disabled: bool,
1103 line_wrap: Option<bool>,
1105 request_id: Option<u64>,
1107 },
1108
1109 SetVirtualBufferContent {
1111 buffer_id: BufferId,
1112 entries: Vec<TextPropertyEntry>,
1114 },
1115
1116 GetTextPropertiesAtCursor { buffer_id: BufferId },
1118
1119 DefineMode {
1121 name: String,
1122 parent: Option<String>,
1123 bindings: Vec<(String, String)>, read_only: bool,
1125 },
1126
1127 ShowBuffer { buffer_id: BufferId },
1129
1130 CreateVirtualBufferInExistingSplit {
1132 name: String,
1134 mode: String,
1136 read_only: bool,
1138 entries: Vec<TextPropertyEntry>,
1140 split_id: SplitId,
1142 show_line_numbers: bool,
1144 show_cursors: bool,
1146 editing_disabled: bool,
1148 line_wrap: Option<bool>,
1150 request_id: Option<u64>,
1152 },
1153
1154 CloseBuffer { buffer_id: BufferId },
1156
1157 CreateCompositeBuffer {
1160 name: String,
1162 mode: String,
1164 layout: CompositeLayoutConfig,
1166 sources: Vec<CompositeSourceConfig>,
1168 hunks: Option<Vec<CompositeHunk>>,
1170 request_id: Option<u64>,
1172 },
1173
1174 UpdateCompositeAlignment {
1176 buffer_id: BufferId,
1177 hunks: Vec<CompositeHunk>,
1178 },
1179
1180 CloseCompositeBuffer { buffer_id: BufferId },
1182
1183 FocusSplit { split_id: SplitId },
1185
1186 SetSplitBuffer {
1188 split_id: SplitId,
1189 buffer_id: BufferId,
1190 },
1191
1192 SetSplitScroll { split_id: SplitId, top_byte: usize },
1194
1195 RequestHighlights {
1197 buffer_id: BufferId,
1198 range: Range<usize>,
1199 request_id: u64,
1200 },
1201
1202 CloseSplit { split_id: SplitId },
1204
1205 SetSplitRatio {
1207 split_id: SplitId,
1208 ratio: f32,
1210 },
1211
1212 DistributeSplitsEvenly {
1214 split_ids: Vec<SplitId>,
1216 },
1217
1218 SetBufferCursor {
1220 buffer_id: BufferId,
1221 position: usize,
1223 },
1224
1225 SendLspRequest {
1227 language: String,
1228 method: String,
1229 #[ts(type = "any")]
1230 params: Option<JsonValue>,
1231 request_id: u64,
1232 },
1233
1234 SetClipboard { text: String },
1236
1237 DeleteSelection,
1240
1241 SetContext {
1245 name: String,
1247 active: bool,
1249 },
1250
1251 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1253
1254 ExecuteAction {
1257 action_name: String,
1259 },
1260
1261 ExecuteActions {
1265 actions: Vec<ActionSpec>,
1267 },
1268
1269 GetBufferText {
1271 buffer_id: BufferId,
1273 start: usize,
1275 end: usize,
1277 request_id: u64,
1279 },
1280
1281 GetLineStartPosition {
1284 buffer_id: BufferId,
1286 line: u32,
1288 request_id: u64,
1290 },
1291
1292 GetLineEndPosition {
1296 buffer_id: BufferId,
1298 line: u32,
1300 request_id: u64,
1302 },
1303
1304 GetBufferLineCount {
1306 buffer_id: BufferId,
1308 request_id: u64,
1310 },
1311
1312 ScrollToLineCenter {
1315 split_id: SplitId,
1317 buffer_id: BufferId,
1319 line: usize,
1321 },
1322
1323 SetEditorMode {
1326 mode: Option<String>,
1328 },
1329
1330 ShowActionPopup {
1333 popup_id: String,
1335 title: String,
1337 message: String,
1339 actions: Vec<ActionPopupAction>,
1341 },
1342
1343 DisableLspForLanguage {
1345 language: String,
1347 },
1348
1349 SetLspRootUri {
1353 language: String,
1355 uri: String,
1357 },
1358
1359 CreateScrollSyncGroup {
1363 group_id: u32,
1365 left_split: SplitId,
1367 right_split: SplitId,
1369 },
1370
1371 SetScrollSyncAnchors {
1374 group_id: u32,
1376 anchors: Vec<(usize, usize)>,
1378 },
1379
1380 RemoveScrollSyncGroup {
1382 group_id: u32,
1384 },
1385
1386 SaveBufferToPath {
1389 buffer_id: BufferId,
1391 path: PathBuf,
1393 },
1394
1395 LoadPlugin {
1398 path: PathBuf,
1400 callback_id: JsCallbackId,
1402 },
1403
1404 UnloadPlugin {
1407 name: String,
1409 callback_id: JsCallbackId,
1411 },
1412
1413 ReloadPlugin {
1416 name: String,
1418 callback_id: JsCallbackId,
1420 },
1421
1422 ListPlugins {
1425 callback_id: JsCallbackId,
1427 },
1428
1429 ReloadThemes,
1432
1433 RegisterGrammar {
1436 language: String,
1438 grammar_path: String,
1440 extensions: Vec<String>,
1442 },
1443
1444 RegisterLanguageConfig {
1447 language: String,
1449 config: LanguagePackConfig,
1451 },
1452
1453 RegisterLspServer {
1456 language: String,
1458 config: LspServerPackConfig,
1460 },
1461
1462 ReloadGrammars,
1465}
1466
1467#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
1476#[serde(rename_all = "camelCase")]
1477#[ts(export)]
1478pub struct LanguagePackConfig {
1479 #[serde(default)]
1481 pub comment_prefix: Option<String>,
1482
1483 #[serde(default)]
1485 pub block_comment_start: Option<String>,
1486
1487 #[serde(default)]
1489 pub block_comment_end: Option<String>,
1490
1491 #[serde(default)]
1493 pub use_tabs: Option<bool>,
1494
1495 #[serde(default)]
1497 pub tab_size: Option<usize>,
1498
1499 #[serde(default)]
1501 pub auto_indent: Option<bool>,
1502
1503 #[serde(default)]
1506 pub show_whitespace_tabs: Option<bool>,
1507
1508 #[serde(default)]
1510 pub formatter: Option<FormatterPackConfig>,
1511}
1512
1513#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1515#[serde(rename_all = "camelCase")]
1516#[ts(export)]
1517pub struct FormatterPackConfig {
1518 pub command: String,
1520
1521 #[serde(default)]
1523 pub args: Vec<String>,
1524}
1525
1526#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1528#[serde(rename_all = "camelCase")]
1529#[ts(export)]
1530pub struct LspServerPackConfig {
1531 pub command: String,
1533
1534 #[serde(default)]
1536 pub args: Vec<String>,
1537
1538 #[serde(default)]
1540 pub auto_start: Option<bool>,
1541
1542 #[serde(default)]
1544 #[ts(type = "Record<string, unknown> | null")]
1545 pub initialization_options: Option<JsonValue>,
1546}
1547
1548#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1550#[ts(export)]
1551pub enum HunkStatus {
1552 Pending,
1553 Staged,
1554 Discarded,
1555}
1556
1557#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1559#[ts(export)]
1560pub struct ReviewHunk {
1561 pub id: String,
1562 pub file: String,
1563 pub context_header: String,
1564 pub status: HunkStatus,
1565 pub base_range: Option<(usize, usize)>,
1567 pub modified_range: Option<(usize, usize)>,
1569}
1570
1571#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1573#[serde(deny_unknown_fields)]
1574#[ts(export, rename = "TsActionPopupAction")]
1575pub struct ActionPopupAction {
1576 pub id: String,
1578 pub label: String,
1580}
1581
1582#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1584#[serde(deny_unknown_fields)]
1585#[ts(export)]
1586pub struct ActionPopupOptions {
1587 pub id: String,
1589 pub title: String,
1591 pub message: String,
1593 pub actions: Vec<ActionPopupAction>,
1595}
1596
1597#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1599#[ts(export)]
1600pub struct TsHighlightSpan {
1601 pub start: u32,
1602 pub end: u32,
1603 #[ts(type = "[number, number, number]")]
1604 pub color: (u8, u8, u8),
1605 pub bold: bool,
1606 pub italic: bool,
1607}
1608
1609#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1611#[ts(export)]
1612pub struct SpawnResult {
1613 pub stdout: String,
1615 pub stderr: String,
1617 pub exit_code: i32,
1619}
1620
1621#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1623#[ts(export)]
1624pub struct BackgroundProcessResult {
1625 #[ts(type = "number")]
1627 pub process_id: u64,
1628 pub exit_code: i32,
1631}
1632
1633#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1635#[serde(deny_unknown_fields)]
1636#[ts(export, rename = "TextPropertyEntry")]
1637pub struct JsTextPropertyEntry {
1638 pub text: String,
1640 #[serde(default)]
1642 #[ts(optional, type = "Record<string, unknown>")]
1643 pub properties: Option<HashMap<String, JsonValue>>,
1644}
1645
1646#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1648#[ts(export)]
1649pub struct DirEntry {
1650 pub name: String,
1652 pub is_file: bool,
1654 pub is_dir: bool,
1656}
1657
1658#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1660#[ts(export)]
1661pub struct JsPosition {
1662 pub line: u32,
1664 pub character: u32,
1666}
1667
1668#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1670#[ts(export)]
1671pub struct JsRange {
1672 pub start: JsPosition,
1674 pub end: JsPosition,
1676}
1677
1678#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1680#[ts(export)]
1681pub struct JsDiagnostic {
1682 pub uri: String,
1684 pub message: String,
1686 pub severity: Option<u8>,
1688 pub range: JsRange,
1690 #[ts(optional)]
1692 pub source: Option<String>,
1693}
1694
1695#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1697#[serde(deny_unknown_fields)]
1698#[ts(export)]
1699pub struct CreateVirtualBufferOptions {
1700 pub name: String,
1702 #[serde(default)]
1704 #[ts(optional)]
1705 pub mode: Option<String>,
1706 #[serde(default, rename = "readOnly")]
1708 #[ts(optional, rename = "readOnly")]
1709 pub read_only: Option<bool>,
1710 #[serde(default, rename = "showLineNumbers")]
1712 #[ts(optional, rename = "showLineNumbers")]
1713 pub show_line_numbers: Option<bool>,
1714 #[serde(default, rename = "showCursors")]
1716 #[ts(optional, rename = "showCursors")]
1717 pub show_cursors: Option<bool>,
1718 #[serde(default, rename = "editingDisabled")]
1720 #[ts(optional, rename = "editingDisabled")]
1721 pub editing_disabled: Option<bool>,
1722 #[serde(default, rename = "hiddenFromTabs")]
1724 #[ts(optional, rename = "hiddenFromTabs")]
1725 pub hidden_from_tabs: Option<bool>,
1726 #[serde(default)]
1728 #[ts(optional)]
1729 pub entries: Option<Vec<JsTextPropertyEntry>>,
1730}
1731
1732#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1734#[serde(deny_unknown_fields)]
1735#[ts(export)]
1736pub struct CreateVirtualBufferInSplitOptions {
1737 pub name: String,
1739 #[serde(default)]
1741 #[ts(optional)]
1742 pub mode: Option<String>,
1743 #[serde(default, rename = "readOnly")]
1745 #[ts(optional, rename = "readOnly")]
1746 pub read_only: Option<bool>,
1747 #[serde(default)]
1749 #[ts(optional)]
1750 pub ratio: Option<f32>,
1751 #[serde(default)]
1753 #[ts(optional)]
1754 pub direction: Option<String>,
1755 #[serde(default, rename = "panelId")]
1757 #[ts(optional, rename = "panelId")]
1758 pub panel_id: Option<String>,
1759 #[serde(default, rename = "showLineNumbers")]
1761 #[ts(optional, rename = "showLineNumbers")]
1762 pub show_line_numbers: Option<bool>,
1763 #[serde(default, rename = "showCursors")]
1765 #[ts(optional, rename = "showCursors")]
1766 pub show_cursors: Option<bool>,
1767 #[serde(default, rename = "editingDisabled")]
1769 #[ts(optional, rename = "editingDisabled")]
1770 pub editing_disabled: Option<bool>,
1771 #[serde(default, rename = "lineWrap")]
1773 #[ts(optional, rename = "lineWrap")]
1774 pub line_wrap: Option<bool>,
1775 #[serde(default)]
1777 #[ts(optional)]
1778 pub entries: Option<Vec<JsTextPropertyEntry>>,
1779}
1780
1781#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1783#[serde(deny_unknown_fields)]
1784#[ts(export)]
1785pub struct CreateVirtualBufferInExistingSplitOptions {
1786 pub name: String,
1788 #[serde(rename = "splitId")]
1790 #[ts(rename = "splitId")]
1791 pub split_id: usize,
1792 #[serde(default)]
1794 #[ts(optional)]
1795 pub mode: Option<String>,
1796 #[serde(default, rename = "readOnly")]
1798 #[ts(optional, rename = "readOnly")]
1799 pub read_only: Option<bool>,
1800 #[serde(default, rename = "showLineNumbers")]
1802 #[ts(optional, rename = "showLineNumbers")]
1803 pub show_line_numbers: Option<bool>,
1804 #[serde(default, rename = "showCursors")]
1806 #[ts(optional, rename = "showCursors")]
1807 pub show_cursors: Option<bool>,
1808 #[serde(default, rename = "editingDisabled")]
1810 #[ts(optional, rename = "editingDisabled")]
1811 pub editing_disabled: Option<bool>,
1812 #[serde(default, rename = "lineWrap")]
1814 #[ts(optional, rename = "lineWrap")]
1815 pub line_wrap: Option<bool>,
1816 #[serde(default)]
1818 #[ts(optional)]
1819 pub entries: Option<Vec<JsTextPropertyEntry>>,
1820}
1821
1822#[derive(Debug, Clone, Serialize, TS)]
1827#[ts(export, type = "Array<Record<string, unknown>>")]
1828pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
1829
1830#[cfg(feature = "plugins")]
1832mod fromjs_impls {
1833 use super::*;
1834 use rquickjs::{Ctx, FromJs, Value};
1835
1836 impl<'js> FromJs<'js> for JsTextPropertyEntry {
1837 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1838 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1839 from: "object",
1840 to: "JsTextPropertyEntry",
1841 message: Some(e.to_string()),
1842 })
1843 }
1844 }
1845
1846 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
1847 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1848 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1849 from: "object",
1850 to: "CreateVirtualBufferOptions",
1851 message: Some(e.to_string()),
1852 })
1853 }
1854 }
1855
1856 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
1857 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1858 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1859 from: "object",
1860 to: "CreateVirtualBufferInSplitOptions",
1861 message: Some(e.to_string()),
1862 })
1863 }
1864 }
1865
1866 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
1867 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1868 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1869 from: "object",
1870 to: "CreateVirtualBufferInExistingSplitOptions",
1871 message: Some(e.to_string()),
1872 })
1873 }
1874 }
1875
1876 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
1877 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
1878 rquickjs_serde::to_value(ctx.clone(), &self.0)
1879 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
1880 }
1881 }
1882
1883 impl<'js> FromJs<'js> for ActionSpec {
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: "ActionSpec",
1890 message: Some(e.to_string()),
1891 })
1892 }
1893 }
1894
1895 impl<'js> FromJs<'js> for ActionPopupAction {
1896 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1897 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1898 from: "object",
1899 to: "ActionPopupAction",
1900 message: Some(e.to_string()),
1901 })
1902 }
1903 }
1904
1905 impl<'js> FromJs<'js> for ActionPopupOptions {
1906 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1907 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1908 from: "object",
1909 to: "ActionPopupOptions",
1910 message: Some(e.to_string()),
1911 })
1912 }
1913 }
1914
1915 impl<'js> FromJs<'js> for ViewTokenWire {
1916 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1917 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1918 from: "object",
1919 to: "ViewTokenWire",
1920 message: Some(e.to_string()),
1921 })
1922 }
1923 }
1924
1925 impl<'js> FromJs<'js> for ViewTokenStyle {
1926 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1927 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1928 from: "object",
1929 to: "ViewTokenStyle",
1930 message: Some(e.to_string()),
1931 })
1932 }
1933 }
1934
1935 impl<'js> FromJs<'js> for LayoutHints {
1936 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1937 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1938 from: "object",
1939 to: "LayoutHints",
1940 message: Some(e.to_string()),
1941 })
1942 }
1943 }
1944
1945 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
1946 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1947 let json: serde_json::Value =
1949 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1950 from: "object",
1951 to: "CreateCompositeBufferOptions (json)",
1952 message: Some(e.to_string()),
1953 })?;
1954 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
1955 from: "json",
1956 to: "CreateCompositeBufferOptions",
1957 message: Some(e.to_string()),
1958 })
1959 }
1960 }
1961
1962 impl<'js> FromJs<'js> for CompositeHunk {
1963 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1964 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1965 from: "object",
1966 to: "CompositeHunk",
1967 message: Some(e.to_string()),
1968 })
1969 }
1970 }
1971
1972 impl<'js> FromJs<'js> for LanguagePackConfig {
1973 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1974 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1975 from: "object",
1976 to: "LanguagePackConfig",
1977 message: Some(e.to_string()),
1978 })
1979 }
1980 }
1981
1982 impl<'js> FromJs<'js> for LspServerPackConfig {
1983 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
1984 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
1985 from: "object",
1986 to: "LspServerPackConfig",
1987 message: Some(e.to_string()),
1988 })
1989 }
1990 }
1991}
1992
1993pub struct PluginApi {
1995 hooks: Arc<RwLock<HookRegistry>>,
1997
1998 commands: Arc<RwLock<CommandRegistry>>,
2000
2001 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2003
2004 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2006}
2007
2008impl PluginApi {
2009 pub fn new(
2011 hooks: Arc<RwLock<HookRegistry>>,
2012 commands: Arc<RwLock<CommandRegistry>>,
2013 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2014 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2015 ) -> Self {
2016 Self {
2017 hooks,
2018 commands,
2019 command_sender,
2020 state_snapshot,
2021 }
2022 }
2023
2024 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
2026 let mut hooks = self.hooks.write().unwrap();
2027 hooks.add_hook(hook_name, callback);
2028 }
2029
2030 pub fn unregister_hooks(&self, hook_name: &str) {
2032 let mut hooks = self.hooks.write().unwrap();
2033 hooks.remove_hooks(hook_name);
2034 }
2035
2036 pub fn register_command(&self, command: Command) {
2038 let commands = self.commands.read().unwrap();
2039 commands.register(command);
2040 }
2041
2042 pub fn unregister_command(&self, name: &str) {
2044 let commands = self.commands.read().unwrap();
2045 commands.unregister(name);
2046 }
2047
2048 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
2050 self.command_sender
2051 .send(command)
2052 .map_err(|e| format!("Failed to send command: {}", e))
2053 }
2054
2055 pub fn insert_text(
2057 &self,
2058 buffer_id: BufferId,
2059 position: usize,
2060 text: String,
2061 ) -> Result<(), String> {
2062 self.send_command(PluginCommand::InsertText {
2063 buffer_id,
2064 position,
2065 text,
2066 })
2067 }
2068
2069 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
2071 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
2072 }
2073
2074 pub fn add_overlay(
2082 &self,
2083 buffer_id: BufferId,
2084 namespace: Option<String>,
2085 range: Range<usize>,
2086 options: OverlayOptions,
2087 ) -> Result<(), String> {
2088 self.send_command(PluginCommand::AddOverlay {
2089 buffer_id,
2090 namespace: namespace.map(OverlayNamespace::from_string),
2091 range,
2092 options,
2093 })
2094 }
2095
2096 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
2098 self.send_command(PluginCommand::RemoveOverlay {
2099 buffer_id,
2100 handle: OverlayHandle::from_string(handle),
2101 })
2102 }
2103
2104 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
2106 self.send_command(PluginCommand::ClearNamespace {
2107 buffer_id,
2108 namespace: OverlayNamespace::from_string(namespace),
2109 })
2110 }
2111
2112 pub fn clear_overlays_in_range(
2115 &self,
2116 buffer_id: BufferId,
2117 start: usize,
2118 end: usize,
2119 ) -> Result<(), String> {
2120 self.send_command(PluginCommand::ClearOverlaysInRange {
2121 buffer_id,
2122 start,
2123 end,
2124 })
2125 }
2126
2127 pub fn set_status(&self, message: String) -> Result<(), String> {
2129 self.send_command(PluginCommand::SetStatus { message })
2130 }
2131
2132 pub fn open_file_at_location(
2135 &self,
2136 path: PathBuf,
2137 line: Option<usize>,
2138 column: Option<usize>,
2139 ) -> Result<(), String> {
2140 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
2141 }
2142
2143 pub fn open_file_in_split(
2148 &self,
2149 split_id: usize,
2150 path: PathBuf,
2151 line: Option<usize>,
2152 column: Option<usize>,
2153 ) -> Result<(), String> {
2154 self.send_command(PluginCommand::OpenFileInSplit {
2155 split_id,
2156 path,
2157 line,
2158 column,
2159 })
2160 }
2161
2162 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
2165 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
2166 }
2167
2168 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
2171 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
2172 }
2173
2174 pub fn add_menu_item(
2176 &self,
2177 menu_label: String,
2178 item: MenuItem,
2179 position: MenuPosition,
2180 ) -> Result<(), String> {
2181 self.send_command(PluginCommand::AddMenuItem {
2182 menu_label,
2183 item,
2184 position,
2185 })
2186 }
2187
2188 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
2190 self.send_command(PluginCommand::AddMenu { menu, position })
2191 }
2192
2193 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
2195 self.send_command(PluginCommand::RemoveMenuItem {
2196 menu_label,
2197 item_label,
2198 })
2199 }
2200
2201 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
2203 self.send_command(PluginCommand::RemoveMenu { menu_label })
2204 }
2205
2206 pub fn create_virtual_buffer(
2213 &self,
2214 name: String,
2215 mode: String,
2216 read_only: bool,
2217 ) -> Result<(), String> {
2218 self.send_command(PluginCommand::CreateVirtualBuffer {
2219 name,
2220 mode,
2221 read_only,
2222 })
2223 }
2224
2225 pub fn create_virtual_buffer_with_content(
2231 &self,
2232 name: String,
2233 mode: String,
2234 read_only: bool,
2235 entries: Vec<TextPropertyEntry>,
2236 ) -> Result<(), String> {
2237 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
2238 name,
2239 mode,
2240 read_only,
2241 entries,
2242 show_line_numbers: true,
2243 show_cursors: true,
2244 editing_disabled: false,
2245 hidden_from_tabs: false,
2246 request_id: None,
2247 })
2248 }
2249
2250 pub fn set_virtual_buffer_content(
2254 &self,
2255 buffer_id: BufferId,
2256 entries: Vec<TextPropertyEntry>,
2257 ) -> Result<(), String> {
2258 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
2259 }
2260
2261 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
2265 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
2266 }
2267
2268 pub fn define_mode(
2273 &self,
2274 name: String,
2275 parent: Option<String>,
2276 bindings: Vec<(String, String)>,
2277 read_only: bool,
2278 ) -> Result<(), String> {
2279 self.send_command(PluginCommand::DefineMode {
2280 name,
2281 parent,
2282 bindings,
2283 read_only,
2284 })
2285 }
2286
2287 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
2289 self.send_command(PluginCommand::ShowBuffer { buffer_id })
2290 }
2291
2292 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
2294 self.send_command(PluginCommand::SetSplitScroll {
2295 split_id: SplitId(split_id),
2296 top_byte,
2297 })
2298 }
2299
2300 pub fn get_highlights(
2302 &self,
2303 buffer_id: BufferId,
2304 range: Range<usize>,
2305 request_id: u64,
2306 ) -> Result<(), String> {
2307 self.send_command(PluginCommand::RequestHighlights {
2308 buffer_id,
2309 range,
2310 request_id,
2311 })
2312 }
2313
2314 pub fn get_active_buffer_id(&self) -> BufferId {
2318 let snapshot = self.state_snapshot.read().unwrap();
2319 snapshot.active_buffer_id
2320 }
2321
2322 pub fn get_active_split_id(&self) -> usize {
2324 let snapshot = self.state_snapshot.read().unwrap();
2325 snapshot.active_split_id
2326 }
2327
2328 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
2330 let snapshot = self.state_snapshot.read().unwrap();
2331 snapshot.buffers.get(&buffer_id).cloned()
2332 }
2333
2334 pub fn list_buffers(&self) -> Vec<BufferInfo> {
2336 let snapshot = self.state_snapshot.read().unwrap();
2337 snapshot.buffers.values().cloned().collect()
2338 }
2339
2340 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
2342 let snapshot = self.state_snapshot.read().unwrap();
2343 snapshot.primary_cursor.clone()
2344 }
2345
2346 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
2348 let snapshot = self.state_snapshot.read().unwrap();
2349 snapshot.all_cursors.clone()
2350 }
2351
2352 pub fn get_viewport(&self) -> Option<ViewportInfo> {
2354 let snapshot = self.state_snapshot.read().unwrap();
2355 snapshot.viewport.clone()
2356 }
2357
2358 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
2360 Arc::clone(&self.state_snapshot)
2361 }
2362}
2363
2364impl Clone for PluginApi {
2365 fn clone(&self) -> Self {
2366 Self {
2367 hooks: Arc::clone(&self.hooks),
2368 commands: Arc::clone(&self.commands),
2369 command_sender: self.command_sender.clone(),
2370 state_snapshot: Arc::clone(&self.state_snapshot),
2371 }
2372 }
2373}
2374
2375#[cfg(test)]
2376mod tests {
2377 use super::*;
2378
2379 #[test]
2380 fn test_plugin_api_creation() {
2381 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2382 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2383 let (tx, _rx) = std::sync::mpsc::channel();
2384 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2385
2386 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2387
2388 let _clone = api.clone();
2390 }
2391
2392 #[test]
2393 fn test_register_hook() {
2394 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2395 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2396 let (tx, _rx) = std::sync::mpsc::channel();
2397 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2398
2399 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
2400
2401 api.register_hook("test-hook", Box::new(|_| true));
2402
2403 let hook_registry = hooks.read().unwrap();
2404 assert_eq!(hook_registry.hook_count("test-hook"), 1);
2405 }
2406
2407 #[test]
2408 fn test_send_command() {
2409 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2410 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2411 let (tx, rx) = std::sync::mpsc::channel();
2412 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2413
2414 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2415
2416 let result = api.insert_text(BufferId(1), 0, "test".to_string());
2417 assert!(result.is_ok());
2418
2419 let received = rx.try_recv();
2421 assert!(received.is_ok());
2422
2423 match received.unwrap() {
2424 PluginCommand::InsertText {
2425 buffer_id,
2426 position,
2427 text,
2428 } => {
2429 assert_eq!(buffer_id.0, 1);
2430 assert_eq!(position, 0);
2431 assert_eq!(text, "test");
2432 }
2433 _ => panic!("Wrong command type"),
2434 }
2435 }
2436
2437 #[test]
2438 fn test_add_overlay_command() {
2439 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2440 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2441 let (tx, rx) = std::sync::mpsc::channel();
2442 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2443
2444 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2445
2446 let result = api.add_overlay(
2447 BufferId(1),
2448 Some("test-overlay".to_string()),
2449 0..10,
2450 OverlayOptions {
2451 fg: Some(OverlayColorSpec::ThemeKey("ui.status_bar_fg".to_string())),
2452 bg: None,
2453 underline: true,
2454 bold: false,
2455 italic: false,
2456 extend_to_line_end: false,
2457 },
2458 );
2459 assert!(result.is_ok());
2460
2461 let received = rx.try_recv().unwrap();
2462 match received {
2463 PluginCommand::AddOverlay {
2464 buffer_id,
2465 namespace,
2466 range,
2467 options,
2468 } => {
2469 assert_eq!(buffer_id.0, 1);
2470 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
2471 assert_eq!(range, 0..10);
2472 assert!(matches!(
2473 options.fg,
2474 Some(OverlayColorSpec::ThemeKey(ref k)) if k == "ui.status_bar_fg"
2475 ));
2476 assert!(options.bg.is_none());
2477 assert!(options.underline);
2478 assert!(!options.bold);
2479 assert!(!options.italic);
2480 assert!(!options.extend_to_line_end);
2481 }
2482 _ => panic!("Wrong command type"),
2483 }
2484 }
2485
2486 #[test]
2487 fn test_set_status_command() {
2488 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2489 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2490 let (tx, rx) = std::sync::mpsc::channel();
2491 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2492
2493 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2494
2495 let result = api.set_status("Test status".to_string());
2496 assert!(result.is_ok());
2497
2498 let received = rx.try_recv().unwrap();
2499 match received {
2500 PluginCommand::SetStatus { message } => {
2501 assert_eq!(message, "Test status");
2502 }
2503 _ => panic!("Wrong command type"),
2504 }
2505 }
2506
2507 #[test]
2508 fn test_get_active_buffer_id() {
2509 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2510 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2511 let (tx, _rx) = std::sync::mpsc::channel();
2512 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2513
2514 {
2516 let mut snapshot = state_snapshot.write().unwrap();
2517 snapshot.active_buffer_id = BufferId(5);
2518 }
2519
2520 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2521
2522 let active_id = api.get_active_buffer_id();
2523 assert_eq!(active_id.0, 5);
2524 }
2525
2526 #[test]
2527 fn test_get_buffer_info() {
2528 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2529 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2530 let (tx, _rx) = std::sync::mpsc::channel();
2531 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2532
2533 {
2535 let mut snapshot = state_snapshot.write().unwrap();
2536 let buffer_info = BufferInfo {
2537 id: BufferId(1),
2538 path: Some(std::path::PathBuf::from("/test/file.txt")),
2539 modified: true,
2540 length: 100,
2541 };
2542 snapshot.buffers.insert(BufferId(1), buffer_info);
2543 }
2544
2545 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2546
2547 let info = api.get_buffer_info(BufferId(1));
2548 assert!(info.is_some());
2549 let info = info.unwrap();
2550 assert_eq!(info.id.0, 1);
2551 assert_eq!(
2552 info.path.as_ref().unwrap().to_str().unwrap(),
2553 "/test/file.txt"
2554 );
2555 assert!(info.modified);
2556 assert_eq!(info.length, 100);
2557
2558 let no_info = api.get_buffer_info(BufferId(999));
2560 assert!(no_info.is_none());
2561 }
2562
2563 #[test]
2564 fn test_list_buffers() {
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.buffers.insert(
2574 BufferId(1),
2575 BufferInfo {
2576 id: BufferId(1),
2577 path: Some(std::path::PathBuf::from("/file1.txt")),
2578 modified: false,
2579 length: 50,
2580 },
2581 );
2582 snapshot.buffers.insert(
2583 BufferId(2),
2584 BufferInfo {
2585 id: BufferId(2),
2586 path: Some(std::path::PathBuf::from("/file2.txt")),
2587 modified: true,
2588 length: 100,
2589 },
2590 );
2591 snapshot.buffers.insert(
2592 BufferId(3),
2593 BufferInfo {
2594 id: BufferId(3),
2595 path: None,
2596 modified: false,
2597 length: 0,
2598 },
2599 );
2600 }
2601
2602 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2603
2604 let buffers = api.list_buffers();
2605 assert_eq!(buffers.len(), 3);
2606
2607 assert!(buffers.iter().any(|b| b.id.0 == 1));
2609 assert!(buffers.iter().any(|b| b.id.0 == 2));
2610 assert!(buffers.iter().any(|b| b.id.0 == 3));
2611 }
2612
2613 #[test]
2614 fn test_get_primary_cursor() {
2615 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2616 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2617 let (tx, _rx) = std::sync::mpsc::channel();
2618 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2619
2620 {
2622 let mut snapshot = state_snapshot.write().unwrap();
2623 snapshot.primary_cursor = Some(CursorInfo {
2624 position: 42,
2625 selection: Some(10..42),
2626 });
2627 }
2628
2629 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2630
2631 let cursor = api.get_primary_cursor();
2632 assert!(cursor.is_some());
2633 let cursor = cursor.unwrap();
2634 assert_eq!(cursor.position, 42);
2635 assert_eq!(cursor.selection, Some(10..42));
2636 }
2637
2638 #[test]
2639 fn test_get_all_cursors() {
2640 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2641 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2642 let (tx, _rx) = std::sync::mpsc::channel();
2643 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2644
2645 {
2647 let mut snapshot = state_snapshot.write().unwrap();
2648 snapshot.all_cursors = vec![
2649 CursorInfo {
2650 position: 10,
2651 selection: None,
2652 },
2653 CursorInfo {
2654 position: 20,
2655 selection: Some(15..20),
2656 },
2657 CursorInfo {
2658 position: 30,
2659 selection: Some(25..30),
2660 },
2661 ];
2662 }
2663
2664 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2665
2666 let cursors = api.get_all_cursors();
2667 assert_eq!(cursors.len(), 3);
2668 assert_eq!(cursors[0].position, 10);
2669 assert_eq!(cursors[0].selection, None);
2670 assert_eq!(cursors[1].position, 20);
2671 assert_eq!(cursors[1].selection, Some(15..20));
2672 assert_eq!(cursors[2].position, 30);
2673 assert_eq!(cursors[2].selection, Some(25..30));
2674 }
2675
2676 #[test]
2677 fn test_get_viewport() {
2678 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2679 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2680 let (tx, _rx) = std::sync::mpsc::channel();
2681 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2682
2683 {
2685 let mut snapshot = state_snapshot.write().unwrap();
2686 snapshot.viewport = Some(ViewportInfo {
2687 top_byte: 100,
2688 left_column: 5,
2689 width: 80,
2690 height: 24,
2691 });
2692 }
2693
2694 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2695
2696 let viewport = api.get_viewport();
2697 assert!(viewport.is_some());
2698 let viewport = viewport.unwrap();
2699 assert_eq!(viewport.top_byte, 100);
2700 assert_eq!(viewport.left_column, 5);
2701 assert_eq!(viewport.width, 80);
2702 assert_eq!(viewport.height, 24);
2703 }
2704
2705 #[test]
2706 fn test_composite_buffer_options_rejects_unknown_fields() {
2707 let valid_json = r#"{
2709 "name": "test",
2710 "mode": "diff",
2711 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2712 "sources": [{"bufferId": 1, "label": "old"}]
2713 }"#;
2714 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
2715 assert!(
2716 result.is_ok(),
2717 "Valid JSON should parse: {:?}",
2718 result.err()
2719 );
2720
2721 let invalid_json = r#"{
2723 "name": "test",
2724 "mode": "diff",
2725 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
2726 "sources": [{"buffer_id": 1, "label": "old"}]
2727 }"#;
2728 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
2729 assert!(
2730 result.is_err(),
2731 "JSON with unknown field should fail to parse"
2732 );
2733 let err = result.unwrap_err().to_string();
2734 assert!(
2735 err.contains("unknown field") || err.contains("buffer_id"),
2736 "Error should mention unknown field: {}",
2737 err
2738 );
2739 }
2740
2741 #[test]
2742 fn test_composite_hunk_rejects_unknown_fields() {
2743 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
2745 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
2746 assert!(
2747 result.is_ok(),
2748 "Valid JSON should parse: {:?}",
2749 result.err()
2750 );
2751
2752 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
2754 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
2755 assert!(
2756 result.is_err(),
2757 "JSON with unknown field should fail to parse"
2758 );
2759 let err = result.unwrap_err().to_string();
2760 assert!(
2761 err.contains("unknown field") || err.contains("old_start"),
2762 "Error should mention unknown field: {}",
2763 err
2764 );
2765 }
2766
2767 #[test]
2768 fn test_plugin_response_line_end_position() {
2769 let response = PluginResponse::LineEndPosition {
2770 request_id: 42,
2771 position: Some(100),
2772 };
2773 let json = serde_json::to_string(&response).unwrap();
2774 assert!(json.contains("LineEndPosition"));
2775 assert!(json.contains("42"));
2776 assert!(json.contains("100"));
2777
2778 let response_none = PluginResponse::LineEndPosition {
2780 request_id: 1,
2781 position: None,
2782 };
2783 let json_none = serde_json::to_string(&response_none).unwrap();
2784 assert!(json_none.contains("null"));
2785 }
2786
2787 #[test]
2788 fn test_plugin_response_buffer_line_count() {
2789 let response = PluginResponse::BufferLineCount {
2790 request_id: 99,
2791 count: Some(500),
2792 };
2793 let json = serde_json::to_string(&response).unwrap();
2794 assert!(json.contains("BufferLineCount"));
2795 assert!(json.contains("99"));
2796 assert!(json.contains("500"));
2797 }
2798
2799 #[test]
2800 fn test_plugin_command_get_line_end_position() {
2801 let command = PluginCommand::GetLineEndPosition {
2802 buffer_id: BufferId(1),
2803 line: 10,
2804 request_id: 123,
2805 };
2806 let json = serde_json::to_string(&command).unwrap();
2807 assert!(json.contains("GetLineEndPosition"));
2808 assert!(json.contains("10"));
2809 }
2810
2811 #[test]
2812 fn test_plugin_command_get_buffer_line_count() {
2813 let command = PluginCommand::GetBufferLineCount {
2814 buffer_id: BufferId(0),
2815 request_id: 456,
2816 };
2817 let json = serde_json::to_string(&command).unwrap();
2818 assert!(json.contains("GetBufferLineCount"));
2819 assert!(json.contains("456"));
2820 }
2821
2822 #[test]
2823 fn test_plugin_command_scroll_to_line_center() {
2824 let command = PluginCommand::ScrollToLineCenter {
2825 split_id: SplitId(1),
2826 buffer_id: BufferId(2),
2827 line: 50,
2828 };
2829 let json = serde_json::to_string(&command).unwrap();
2830 assert!(json.contains("ScrollToLineCenter"));
2831 assert!(json.contains("50"));
2832 }
2833}