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 crate::TerminalId;
55use lsp_types;
56use serde::{Deserialize, Serialize};
57use serde_json::Value as JsonValue;
58use std::collections::HashMap;
59use std::ops::Range;
60use std::path::PathBuf;
61use std::sync::{Arc, RwLock};
62use ts_rs::TS;
63
64pub struct CommandRegistry {
68 commands: std::sync::RwLock<Vec<Command>>,
69}
70
71impl CommandRegistry {
72 pub fn new() -> Self {
74 Self {
75 commands: std::sync::RwLock::new(Vec::new()),
76 }
77 }
78
79 pub fn register(&self, command: Command) {
81 let mut commands = self.commands.write().unwrap();
82 commands.retain(|c| c.name != command.name);
83 commands.push(command);
84 }
85
86 pub fn unregister(&self, name: &str) {
88 let mut commands = self.commands.write().unwrap();
89 commands.retain(|c| c.name != name);
90 }
91}
92
93impl Default for CommandRegistry {
94 fn default() -> Self {
95 Self::new()
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
105#[ts(export)]
106pub struct JsCallbackId(pub u64);
107
108impl JsCallbackId {
109 pub fn new(id: u64) -> Self {
111 Self(id)
112 }
113
114 pub fn as_u64(self) -> u64 {
116 self.0
117 }
118}
119
120impl From<u64> for JsCallbackId {
121 fn from(id: u64) -> Self {
122 Self(id)
123 }
124}
125
126impl From<JsCallbackId> for u64 {
127 fn from(id: JsCallbackId) -> u64 {
128 id.0
129 }
130}
131
132impl std::fmt::Display for JsCallbackId {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 write!(f, "{}", self.0)
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize, TS)]
140#[serde(rename_all = "camelCase")]
141#[ts(export, rename_all = "camelCase")]
142pub struct TerminalResult {
143 #[ts(type = "number")]
145 pub buffer_id: u64,
146 #[ts(type = "number")]
148 pub terminal_id: u64,
149 #[ts(type = "number | null")]
151 pub split_id: Option<u64>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, TS)]
156#[serde(rename_all = "camelCase")]
157#[ts(export, rename_all = "camelCase")]
158pub struct VirtualBufferResult {
159 #[ts(type = "number")]
161 pub buffer_id: u64,
162 #[ts(type = "number | null")]
164 pub split_id: Option<u64>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, TS)]
169#[serde(rename_all = "camelCase")]
170#[ts(export, rename_all = "camelCase")]
171pub struct BufferGroupResult {
172 #[ts(type = "number")]
174 pub group_id: u64,
175 #[ts(type = "Record<string, number>")]
177 pub panels: HashMap<String, u64>,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, TS)]
182#[ts(export)]
183pub enum PluginResponse {
184 VirtualBufferCreated {
186 request_id: u64,
187 buffer_id: BufferId,
188 split_id: Option<SplitId>,
189 },
190 TerminalCreated {
192 request_id: u64,
193 buffer_id: BufferId,
194 terminal_id: TerminalId,
195 split_id: Option<SplitId>,
196 },
197 LspRequest {
199 request_id: u64,
200 #[ts(type = "any")]
201 result: Result<JsonValue, String>,
202 },
203 HighlightsComputed {
205 request_id: u64,
206 spans: Vec<TsHighlightSpan>,
207 },
208 BufferText {
210 request_id: u64,
211 text: Result<String, String>,
212 },
213 LineStartPosition {
215 request_id: u64,
216 position: Option<usize>,
218 },
219 LineEndPosition {
221 request_id: u64,
222 position: Option<usize>,
224 },
225 BufferLineCount {
227 request_id: u64,
228 count: Option<usize>,
230 },
231 CompositeBufferCreated {
233 request_id: u64,
234 buffer_id: BufferId,
235 },
236 SplitByLabel {
238 request_id: u64,
239 split_id: Option<SplitId>,
240 },
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize, TS)]
245#[ts(export)]
246pub enum PluginAsyncMessage {
247 ProcessOutput {
249 process_id: u64,
251 stdout: String,
253 stderr: String,
255 exit_code: i32,
257 },
258 DelayComplete {
260 callback_id: u64,
262 },
263 ProcessStdout { process_id: u64, data: String },
265 ProcessStderr { process_id: u64, data: String },
267 ProcessExit {
269 process_id: u64,
270 callback_id: u64,
271 exit_code: i32,
272 },
273 LspResponse {
275 language: String,
276 request_id: u64,
277 #[ts(type = "any")]
278 result: Result<JsonValue, String>,
279 },
280 PluginResponse(crate::api::PluginResponse),
282
283 GrepStreamingProgress {
285 search_id: u64,
287 matches_json: String,
289 },
290
291 GrepStreamingComplete {
293 search_id: u64,
295 callback_id: u64,
297 total_matches: usize,
299 truncated: bool,
301 },
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize, TS)]
306#[ts(export)]
307pub struct CursorInfo {
308 pub position: usize,
310 #[cfg_attr(
312 feature = "plugins",
313 ts(type = "{ start: number; end: number } | null")
314 )]
315 pub selection: Option<Range<usize>>,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize, TS)]
320#[serde(deny_unknown_fields)]
321#[ts(export)]
322pub struct ActionSpec {
323 pub action: String,
325 #[serde(default = "default_action_count")]
327 pub count: u32,
328}
329
330fn default_action_count() -> u32 {
331 1
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize, TS)]
336#[ts(export)]
337pub struct BufferInfo {
338 #[ts(type = "number")]
340 pub id: BufferId,
341 #[serde(serialize_with = "serialize_path")]
343 #[ts(type = "string")]
344 pub path: Option<PathBuf>,
345 pub modified: bool,
347 pub length: usize,
349 pub is_virtual: bool,
351 pub view_mode: String,
353 pub is_composing_in_any_split: bool,
358 pub compose_width: Option<u16>,
360 pub language: String,
362 #[serde(default)]
369 pub is_preview: bool,
370 #[serde(default)]
377 #[ts(type = "number[]")]
378 pub splits: Vec<SplitId>,
379}
380
381fn serialize_path<S: serde::Serializer>(path: &Option<PathBuf>, s: S) -> Result<S::Ok, S::Error> {
382 s.serialize_str(
383 &path
384 .as_ref()
385 .map(|p| p.to_string_lossy().to_string())
386 .unwrap_or_default(),
387 )
388}
389
390fn serialize_ranges_as_tuples<S>(ranges: &[Range<usize>], serializer: S) -> Result<S::Ok, S::Error>
392where
393 S: serde::Serializer,
394{
395 use serde::ser::SerializeSeq;
396 let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
397 for range in ranges {
398 seq.serialize_element(&(range.start, range.end))?;
399 }
400 seq.end()
401}
402
403#[derive(Debug, Clone, Serialize, Deserialize, TS)]
405#[ts(export)]
406pub struct BufferSavedDiff {
407 pub equal: bool,
408 #[serde(serialize_with = "serialize_ranges_as_tuples")]
409 #[ts(type = "Array<[number, number]>")]
410 pub byte_ranges: Vec<Range<usize>>,
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize, TS)]
415#[serde(rename_all = "camelCase")]
416#[ts(export, rename_all = "camelCase")]
417pub struct ViewportInfo {
418 pub top_byte: usize,
420 pub top_line: Option<usize>,
422 pub left_column: usize,
424 pub width: u16,
426 pub height: u16,
428}
429
430#[derive(Debug, Clone, Serialize, Deserialize, TS)]
432#[serde(rename_all = "camelCase")]
433#[ts(export, rename_all = "camelCase")]
434pub struct LayoutHints {
435 #[ts(optional)]
437 pub compose_width: Option<u16>,
438 #[ts(optional)]
440 pub column_guides: Option<Vec<u16>>,
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize, TS)]
458#[serde(untagged)]
459#[ts(export)]
460pub enum OverlayColorSpec {
461 #[ts(type = "[number, number, number]")]
463 Rgb(u8, u8, u8),
464 ThemeKey(String),
466}
467
468impl OverlayColorSpec {
469 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
471 Self::Rgb(r, g, b)
472 }
473
474 pub fn theme_key(key: impl Into<String>) -> Self {
476 Self::ThemeKey(key.into())
477 }
478
479 pub fn as_rgb(&self) -> Option<(u8, u8, u8)> {
481 match self {
482 Self::Rgb(r, g, b) => Some((*r, *g, *b)),
483 Self::ThemeKey(_) => None,
484 }
485 }
486
487 pub fn as_theme_key(&self) -> Option<&str> {
489 match self {
490 Self::ThemeKey(key) => Some(key),
491 Self::Rgb(_, _, _) => None,
492 }
493 }
494}
495
496#[derive(Debug, Clone, Serialize, Deserialize, TS)]
501#[serde(deny_unknown_fields, rename_all = "camelCase")]
502#[ts(export, rename_all = "camelCase")]
503#[derive(Default)]
504pub struct OverlayOptions {
505 #[serde(default, skip_serializing_if = "Option::is_none")]
507 pub fg: Option<OverlayColorSpec>,
508
509 #[serde(default, skip_serializing_if = "Option::is_none")]
511 pub bg: Option<OverlayColorSpec>,
512
513 #[serde(default)]
515 pub underline: bool,
516
517 #[serde(default)]
519 pub bold: bool,
520
521 #[serde(default)]
523 pub italic: bool,
524
525 #[serde(default)]
527 pub strikethrough: bool,
528
529 #[serde(default)]
531 pub extend_to_line_end: bool,
532
533 #[serde(default, skip_serializing_if = "Option::is_none")]
537 pub url: Option<String>,
538}
539
540#[derive(Debug, Clone, Serialize, Deserialize, TS)]
546#[serde(deny_unknown_fields)]
547#[ts(export, rename = "TsCompositeLayoutConfig")]
548pub struct CompositeLayoutConfig {
549 #[serde(rename = "type")]
551 #[ts(rename = "type")]
552 pub layout_type: String,
553 #[serde(default)]
555 #[ts(optional)]
556 pub ratios: Option<Vec<f32>>,
557 #[serde(default = "default_true", rename = "showSeparator")]
559 #[ts(rename = "showSeparator")]
560 pub show_separator: bool,
561 #[serde(default)]
563 #[ts(optional)]
564 pub spacing: Option<u16>,
565}
566
567fn default_true() -> bool {
568 true
569}
570
571#[derive(Debug, Clone, Serialize, Deserialize, TS)]
573#[serde(deny_unknown_fields)]
574#[ts(export, rename = "TsCompositeSourceConfig")]
575pub struct CompositeSourceConfig {
576 #[serde(rename = "bufferId")]
578 #[ts(rename = "bufferId")]
579 pub buffer_id: usize,
580 pub label: String,
582 #[serde(default)]
584 pub editable: bool,
585 #[serde(default)]
587 pub style: Option<CompositePaneStyle>,
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
592#[serde(deny_unknown_fields)]
593#[ts(export, rename = "TsCompositePaneStyle")]
594pub struct CompositePaneStyle {
595 #[serde(default, rename = "addBg")]
598 #[ts(optional, rename = "addBg", type = "[number, number, number]")]
599 pub add_bg: Option<[u8; 3]>,
600 #[serde(default, rename = "removeBg")]
602 #[ts(optional, rename = "removeBg", type = "[number, number, number]")]
603 pub remove_bg: Option<[u8; 3]>,
604 #[serde(default, rename = "modifyBg")]
606 #[ts(optional, rename = "modifyBg", type = "[number, number, number]")]
607 pub modify_bg: Option<[u8; 3]>,
608 #[serde(default, rename = "gutterStyle")]
610 #[ts(optional, rename = "gutterStyle")]
611 pub gutter_style: Option<String>,
612}
613
614#[derive(Debug, Clone, Serialize, Deserialize, TS)]
616#[serde(deny_unknown_fields)]
617#[ts(export, rename = "TsCompositeHunk")]
618pub struct CompositeHunk {
619 #[serde(rename = "oldStart")]
621 #[ts(rename = "oldStart")]
622 pub old_start: usize,
623 #[serde(rename = "oldCount")]
625 #[ts(rename = "oldCount")]
626 pub old_count: usize,
627 #[serde(rename = "newStart")]
629 #[ts(rename = "newStart")]
630 pub new_start: usize,
631 #[serde(rename = "newCount")]
633 #[ts(rename = "newCount")]
634 pub new_count: usize,
635}
636
637#[derive(Debug, Clone, Serialize, Deserialize, TS)]
639#[serde(deny_unknown_fields)]
640#[ts(export, rename = "TsCreateCompositeBufferOptions")]
641pub struct CreateCompositeBufferOptions {
642 #[serde(default)]
644 pub name: String,
645 #[serde(default)]
647 pub mode: String,
648 pub layout: CompositeLayoutConfig,
650 pub sources: Vec<CompositeSourceConfig>,
652 #[serde(default)]
654 pub hunks: Option<Vec<CompositeHunk>>,
655 #[serde(default, rename = "initialFocusHunk")]
659 #[ts(optional, rename = "initialFocusHunk")]
660 pub initial_focus_hunk: Option<usize>,
661}
662
663#[derive(Debug, Clone, Serialize, Deserialize, TS)]
665#[ts(export)]
666pub enum ViewTokenWireKind {
667 Text(String),
668 Newline,
669 Space,
670 Break,
673 BinaryByte(u8),
677}
678
679#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
685#[serde(deny_unknown_fields)]
686#[ts(export)]
687pub struct ViewTokenStyle {
688 #[serde(default)]
690 #[ts(type = "[number, number, number] | null")]
691 pub fg: Option<(u8, u8, u8)>,
692 #[serde(default)]
694 #[ts(type = "[number, number, number] | null")]
695 pub bg: Option<(u8, u8, u8)>,
696 #[serde(default)]
698 pub bold: bool,
699 #[serde(default)]
701 pub italic: bool,
702}
703
704#[derive(Debug, Clone, Serialize, Deserialize, TS)]
706#[serde(deny_unknown_fields)]
707#[ts(export)]
708pub struct ViewTokenWire {
709 #[ts(type = "number | null")]
711 pub source_offset: Option<usize>,
712 pub kind: ViewTokenWireKind,
714 #[serde(default)]
716 #[ts(optional)]
717 pub style: Option<ViewTokenStyle>,
718}
719
720#[derive(Debug, Clone, Serialize, Deserialize, TS)]
722#[ts(export)]
723pub struct ViewTransformPayload {
724 pub range: Range<usize>,
726 pub tokens: Vec<ViewTokenWire>,
728 pub layout_hints: Option<LayoutHints>,
730}
731
732#[derive(Debug, Clone, Serialize, Deserialize, TS)]
735#[ts(export)]
736pub struct EditorStateSnapshot {
737 pub active_buffer_id: BufferId,
739 pub active_split_id: usize,
741 pub buffers: HashMap<BufferId, BufferInfo>,
743 pub buffer_saved_diffs: HashMap<BufferId, BufferSavedDiff>,
745 pub primary_cursor: Option<CursorInfo>,
747 pub all_cursors: Vec<CursorInfo>,
749 pub viewport: Option<ViewportInfo>,
751 pub buffer_cursor_positions: HashMap<BufferId, usize>,
753 pub buffer_text_properties: HashMap<BufferId, Vec<TextProperty>>,
755 pub selected_text: Option<String>,
758 pub clipboard: String,
760 pub working_dir: PathBuf,
762 #[serde(default)]
770 pub authority_label: String,
771 #[serde(skip)]
784 #[ts(type = "any")]
785 pub diagnostics: Arc<HashMap<String, Vec<lsp_types::Diagnostic>>>,
786 #[serde(skip)]
791 #[ts(type = "any")]
792 pub folding_ranges: Arc<HashMap<String, Vec<lsp_types::FoldingRange>>>,
793 #[serde(skip)]
803 #[ts(type = "any")]
804 pub config: Arc<serde_json::Value>,
805 #[serde(skip)]
810 #[ts(type = "any")]
811 pub user_config: Arc<serde_json::Value>,
812 #[ts(type = "GrammarInfo[]")]
814 pub available_grammars: Vec<GrammarInfoSnapshot>,
815 pub editor_mode: Option<String>,
818
819 #[ts(type = "any")]
823 pub plugin_view_states: HashMap<BufferId, HashMap<String, serde_json::Value>>,
824
825 #[serde(skip)]
828 #[ts(skip)]
829 pub plugin_view_states_split: usize,
830
831 #[serde(skip)]
834 #[ts(skip)]
835 pub keybinding_labels: HashMap<String, String>,
836
837 #[ts(type = "any")]
844 pub plugin_global_states: HashMap<String, HashMap<String, serde_json::Value>>,
845}
846
847impl EditorStateSnapshot {
848 pub fn new() -> Self {
849 Self {
850 active_buffer_id: BufferId(0),
851 active_split_id: 0,
852 buffers: HashMap::new(),
853 buffer_saved_diffs: HashMap::new(),
854 primary_cursor: None,
855 all_cursors: Vec::new(),
856 viewport: None,
857 buffer_cursor_positions: HashMap::new(),
858 buffer_text_properties: HashMap::new(),
859 selected_text: None,
860 clipboard: String::new(),
861 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
862 authority_label: String::new(),
863 diagnostics: Arc::new(HashMap::new()),
864 folding_ranges: Arc::new(HashMap::new()),
865 config: Arc::new(serde_json::Value::Null),
866 user_config: Arc::new(serde_json::Value::Null),
867 available_grammars: Vec::new(),
868 editor_mode: None,
869 plugin_view_states: HashMap::new(),
870 plugin_view_states_split: 0,
871 keybinding_labels: HashMap::new(),
872 plugin_global_states: HashMap::new(),
873 }
874 }
875}
876
877impl Default for EditorStateSnapshot {
878 fn default() -> Self {
879 Self::new()
880 }
881}
882
883#[derive(Debug, Clone, Serialize, Deserialize, TS)]
885#[ts(export)]
886pub struct GrammarInfoSnapshot {
887 pub name: String,
889 pub source: String,
891 pub file_extensions: Vec<String>,
893 pub short_name: Option<String>,
895}
896
897#[derive(Debug, Clone, Serialize, Deserialize, TS)]
899#[ts(export)]
900pub enum MenuPosition {
901 Top,
903 Bottom,
905 Before(String),
907 After(String),
909}
910
911#[derive(Debug, Clone, Serialize, Deserialize, TS)]
913#[ts(export)]
914pub enum PluginCommand {
915 InsertText {
917 buffer_id: BufferId,
918 position: usize,
919 text: String,
920 },
921
922 DeleteRange {
924 buffer_id: BufferId,
925 range: Range<usize>,
926 },
927
928 AddOverlay {
933 buffer_id: BufferId,
934 namespace: Option<OverlayNamespace>,
935 range: Range<usize>,
936 options: OverlayOptions,
938 },
939
940 RemoveOverlay {
942 buffer_id: BufferId,
943 handle: OverlayHandle,
944 },
945
946 SetStatus { message: String },
948
949 ApplyTheme { theme_name: String },
951
952 OverrideThemeColors { overrides: HashMap<String, [u8; 3]> },
963
964 ReloadConfig,
967
968 SetSetting {
971 plugin_name: String,
972 path: String,
973 #[ts(type = "unknown")]
974 value: JsonValue,
975 },
976
977 RegisterCommand { command: Command },
979
980 UnregisterCommand { name: String },
982
983 OpenFileInBackground { path: PathBuf },
985
986 InsertAtCursor { text: String },
988
989 SpawnProcess {
991 command: String,
992 args: Vec<String>,
993 cwd: Option<String>,
994 callback_id: JsCallbackId,
995 },
996
997 Delay {
999 callback_id: JsCallbackId,
1000 duration_ms: u64,
1001 },
1002
1003 SpawnBackgroundProcess {
1007 process_id: u64,
1009 command: String,
1011 args: Vec<String>,
1013 cwd: Option<String>,
1015 callback_id: JsCallbackId,
1017 },
1018
1019 KillBackgroundProcess { process_id: u64 },
1021
1022 SpawnProcessWait {
1025 process_id: u64,
1027 callback_id: JsCallbackId,
1029 },
1030
1031 SetLayoutHints {
1033 buffer_id: BufferId,
1034 split_id: Option<SplitId>,
1035 range: Range<usize>,
1036 hints: LayoutHints,
1037 },
1038
1039 SetLineNumbers { buffer_id: BufferId, enabled: bool },
1041
1042 SetViewMode { buffer_id: BufferId, mode: String },
1044
1045 SetLineWrap {
1047 buffer_id: BufferId,
1048 split_id: Option<SplitId>,
1049 enabled: bool,
1050 },
1051
1052 SubmitViewTransform {
1054 buffer_id: BufferId,
1055 split_id: Option<SplitId>,
1056 payload: ViewTransformPayload,
1057 },
1058
1059 ClearViewTransform {
1061 buffer_id: BufferId,
1062 split_id: Option<SplitId>,
1063 },
1064
1065 SetViewState {
1068 buffer_id: BufferId,
1069 key: String,
1070 #[ts(type = "any")]
1071 value: Option<serde_json::Value>,
1072 },
1073
1074 SetGlobalState {
1078 plugin_name: String,
1079 key: String,
1080 #[ts(type = "any")]
1081 value: Option<serde_json::Value>,
1082 },
1083
1084 ClearAllOverlays { buffer_id: BufferId },
1086
1087 ClearNamespace {
1089 buffer_id: BufferId,
1090 namespace: OverlayNamespace,
1091 },
1092
1093 ClearOverlaysInRange {
1096 buffer_id: BufferId,
1097 start: usize,
1098 end: usize,
1099 },
1100
1101 AddVirtualText {
1104 buffer_id: BufferId,
1105 virtual_text_id: String,
1106 position: usize,
1107 text: String,
1108 color: (u8, u8, u8),
1109 use_bg: bool, before: bool, },
1112
1113 RemoveVirtualText {
1115 buffer_id: BufferId,
1116 virtual_text_id: String,
1117 },
1118
1119 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
1121
1122 ClearVirtualTexts { buffer_id: BufferId },
1124
1125 AddVirtualLine {
1129 buffer_id: BufferId,
1130 position: usize,
1132 text: String,
1134 fg_color: Option<OverlayColorSpec>,
1138 bg_color: Option<OverlayColorSpec>,
1141 above: bool,
1143 namespace: String,
1145 priority: i32,
1147 },
1148
1149 ClearVirtualTextNamespace {
1152 buffer_id: BufferId,
1153 namespace: String,
1154 },
1155
1156 AddConceal {
1159 buffer_id: BufferId,
1160 namespace: OverlayNamespace,
1162 start: usize,
1164 end: usize,
1165 replacement: Option<String>,
1167 },
1168
1169 ClearConcealNamespace {
1171 buffer_id: BufferId,
1172 namespace: OverlayNamespace,
1173 },
1174
1175 ClearConcealsInRange {
1178 buffer_id: BufferId,
1179 start: usize,
1180 end: usize,
1181 },
1182
1183 AddFold {
1189 buffer_id: BufferId,
1190 start: usize,
1191 end: usize,
1192 placeholder: Option<String>,
1195 },
1196
1197 ClearFolds { buffer_id: BufferId },
1199
1200 AddSoftBreak {
1204 buffer_id: BufferId,
1205 namespace: OverlayNamespace,
1207 position: usize,
1209 indent: u16,
1211 },
1212
1213 ClearSoftBreakNamespace {
1215 buffer_id: BufferId,
1216 namespace: OverlayNamespace,
1217 },
1218
1219 ClearSoftBreaksInRange {
1221 buffer_id: BufferId,
1222 start: usize,
1223 end: usize,
1224 },
1225
1226 RefreshLines { buffer_id: BufferId },
1228
1229 RefreshAllLines,
1233
1234 HookCompleted { hook_name: String },
1238
1239 SetLineIndicator {
1242 buffer_id: BufferId,
1243 line: usize,
1245 namespace: String,
1247 symbol: String,
1249 color: (u8, u8, u8),
1251 priority: i32,
1253 },
1254
1255 SetLineIndicators {
1258 buffer_id: BufferId,
1259 lines: Vec<usize>,
1261 namespace: String,
1263 symbol: String,
1265 color: (u8, u8, u8),
1267 priority: i32,
1269 },
1270
1271 ClearLineIndicators {
1273 buffer_id: BufferId,
1274 namespace: String,
1276 },
1277
1278 SetFileExplorerDecorations {
1280 namespace: String,
1282 decorations: Vec<FileExplorerDecoration>,
1284 },
1285
1286 ClearFileExplorerDecorations {
1288 namespace: String,
1290 },
1291
1292 OpenFileAtLocation {
1295 path: PathBuf,
1296 line: Option<usize>, column: Option<usize>, },
1299
1300 OpenFileInSplit {
1303 split_id: usize,
1304 path: PathBuf,
1305 line: Option<usize>, column: Option<usize>, },
1308
1309 StartPrompt {
1312 label: String,
1313 prompt_type: String, },
1315
1316 StartPromptWithInitial {
1318 label: String,
1319 prompt_type: String,
1320 initial_value: String,
1321 },
1322
1323 StartPromptAsync {
1326 label: String,
1327 initial_value: String,
1328 callback_id: JsCallbackId,
1329 },
1330
1331 SetPromptSuggestions { suggestions: Vec<Suggestion> },
1334
1335 SetPromptInputSync { sync: bool },
1337
1338 AddMenuItem {
1341 menu_label: String,
1342 item: MenuItem,
1343 position: MenuPosition,
1344 },
1345
1346 AddMenu { menu: Menu, position: MenuPosition },
1348
1349 RemoveMenuItem {
1351 menu_label: String,
1352 item_label: String,
1353 },
1354
1355 RemoveMenu { menu_label: String },
1357
1358 CreateVirtualBuffer {
1360 name: String,
1362 mode: String,
1364 read_only: bool,
1366 },
1367
1368 CreateVirtualBufferWithContent {
1372 name: String,
1374 mode: String,
1376 read_only: bool,
1378 entries: Vec<TextPropertyEntry>,
1380 show_line_numbers: bool,
1382 show_cursors: bool,
1384 editing_disabled: bool,
1386 hidden_from_tabs: bool,
1388 request_id: Option<u64>,
1390 },
1391
1392 CreateVirtualBufferInSplit {
1395 name: String,
1397 mode: String,
1399 read_only: bool,
1401 entries: Vec<TextPropertyEntry>,
1403 ratio: f32,
1405 direction: Option<String>,
1407 panel_id: Option<String>,
1409 show_line_numbers: bool,
1411 show_cursors: bool,
1413 editing_disabled: bool,
1415 line_wrap: Option<bool>,
1417 before: bool,
1419 request_id: Option<u64>,
1421 },
1422
1423 SetVirtualBufferContent {
1425 buffer_id: BufferId,
1426 entries: Vec<TextPropertyEntry>,
1428 },
1429
1430 GetTextPropertiesAtCursor { buffer_id: BufferId },
1432
1433 CreateBufferGroup {
1436 name: String,
1438 mode: String,
1440 layout_json: String,
1442 request_id: Option<u64>,
1444 },
1445
1446 SetPanelContent {
1448 group_id: usize,
1450 panel_name: String,
1452 entries: Vec<TextPropertyEntry>,
1454 },
1455
1456 CloseBufferGroup { group_id: usize },
1458
1459 FocusPanel { group_id: usize, panel_name: String },
1461
1462 DefineMode {
1464 name: String,
1465 bindings: Vec<(String, String)>, read_only: bool,
1467 allow_text_input: bool,
1469 inherit_normal_bindings: bool,
1472 plugin_name: Option<String>,
1474 },
1475
1476 ShowBuffer { buffer_id: BufferId },
1478
1479 CreateVirtualBufferInExistingSplit {
1481 name: String,
1483 mode: String,
1485 read_only: bool,
1487 entries: Vec<TextPropertyEntry>,
1489 split_id: SplitId,
1491 show_line_numbers: bool,
1493 show_cursors: bool,
1495 editing_disabled: bool,
1497 line_wrap: Option<bool>,
1499 request_id: Option<u64>,
1501 },
1502
1503 CloseBuffer { buffer_id: BufferId },
1505
1506 CreateCompositeBuffer {
1509 name: String,
1511 mode: String,
1513 layout: CompositeLayoutConfig,
1515 sources: Vec<CompositeSourceConfig>,
1517 hunks: Option<Vec<CompositeHunk>>,
1519 initial_focus_hunk: Option<usize>,
1521 request_id: Option<u64>,
1523 },
1524
1525 UpdateCompositeAlignment {
1527 buffer_id: BufferId,
1528 hunks: Vec<CompositeHunk>,
1529 },
1530
1531 CloseCompositeBuffer { buffer_id: BufferId },
1533
1534 FlushLayout,
1541
1542 CompositeNextHunk { buffer_id: BufferId },
1544
1545 CompositePrevHunk { buffer_id: BufferId },
1547
1548 FocusSplit { split_id: SplitId },
1550
1551 SetSplitBuffer {
1553 split_id: SplitId,
1554 buffer_id: BufferId,
1555 },
1556
1557 SetSplitScroll { split_id: SplitId, top_byte: usize },
1559
1560 RequestHighlights {
1562 buffer_id: BufferId,
1563 range: Range<usize>,
1564 request_id: u64,
1565 },
1566
1567 CloseSplit { split_id: SplitId },
1569
1570 SetSplitRatio {
1572 split_id: SplitId,
1573 ratio: f32,
1575 },
1576
1577 SetSplitLabel { split_id: SplitId, label: String },
1579
1580 ClearSplitLabel { split_id: SplitId },
1582
1583 GetSplitByLabel { label: String, request_id: u64 },
1585
1586 DistributeSplitsEvenly {
1588 split_ids: Vec<SplitId>,
1590 },
1591
1592 SetBufferCursor {
1594 buffer_id: BufferId,
1595 position: usize,
1597 },
1598
1599 SetBufferShowCursors { buffer_id: BufferId, show: bool },
1607
1608 SendLspRequest {
1610 language: String,
1611 method: String,
1612 #[ts(type = "any")]
1613 params: Option<JsonValue>,
1614 request_id: u64,
1615 },
1616
1617 SetClipboard { text: String },
1619
1620 DeleteSelection,
1623
1624 SetContext {
1628 name: String,
1630 active: bool,
1632 },
1633
1634 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1636
1637 ExecuteAction {
1640 action_name: String,
1642 },
1643
1644 ExecuteActions {
1648 actions: Vec<ActionSpec>,
1650 },
1651
1652 GetBufferText {
1654 buffer_id: BufferId,
1656 start: usize,
1658 end: usize,
1660 request_id: u64,
1662 },
1663
1664 GetLineStartPosition {
1667 buffer_id: BufferId,
1669 line: u32,
1671 request_id: u64,
1673 },
1674
1675 GetLineEndPosition {
1679 buffer_id: BufferId,
1681 line: u32,
1683 request_id: u64,
1685 },
1686
1687 GetBufferLineCount {
1689 buffer_id: BufferId,
1691 request_id: u64,
1693 },
1694
1695 ScrollToLineCenter {
1698 split_id: SplitId,
1700 buffer_id: BufferId,
1702 line: usize,
1704 },
1705
1706 ScrollBufferToLine {
1712 buffer_id: BufferId,
1714 line: usize,
1716 },
1717
1718 SetEditorMode {
1721 mode: Option<String>,
1723 },
1724
1725 ShowActionPopup {
1728 popup_id: String,
1730 title: String,
1732 message: String,
1734 actions: Vec<ActionPopupAction>,
1736 },
1737
1738 DisableLspForLanguage {
1740 language: String,
1742 },
1743
1744 RestartLspForLanguage {
1746 language: String,
1748 },
1749
1750 SetLspRootUri {
1754 language: String,
1756 uri: String,
1758 },
1759
1760 CreateScrollSyncGroup {
1764 group_id: u32,
1766 left_split: SplitId,
1768 right_split: SplitId,
1770 },
1771
1772 SetScrollSyncAnchors {
1775 group_id: u32,
1777 anchors: Vec<(usize, usize)>,
1779 },
1780
1781 RemoveScrollSyncGroup {
1783 group_id: u32,
1785 },
1786
1787 SaveBufferToPath {
1790 buffer_id: BufferId,
1792 path: PathBuf,
1794 },
1795
1796 LoadPlugin {
1799 path: PathBuf,
1801 callback_id: JsCallbackId,
1803 },
1804
1805 UnloadPlugin {
1808 name: String,
1810 callback_id: JsCallbackId,
1812 },
1813
1814 ReloadPlugin {
1817 name: String,
1819 callback_id: JsCallbackId,
1821 },
1822
1823 ListPlugins {
1826 callback_id: JsCallbackId,
1828 },
1829
1830 ReloadThemes { apply_theme: Option<String> },
1834
1835 RegisterGrammar {
1838 language: String,
1840 grammar_path: String,
1842 extensions: Vec<String>,
1844 },
1845
1846 RegisterLanguageConfig {
1849 language: String,
1851 config: LanguagePackConfig,
1853 },
1854
1855 RegisterLspServer {
1858 language: String,
1860 config: LspServerPackConfig,
1862 },
1863
1864 ReloadGrammars { callback_id: JsCallbackId },
1868
1869 CreateTerminal {
1873 cwd: Option<String>,
1875 direction: Option<String>,
1877 ratio: Option<f32>,
1879 focus: Option<bool>,
1881 persistent: bool,
1887 request_id: u64,
1889 },
1890
1891 SendTerminalInput {
1893 terminal_id: TerminalId,
1895 data: String,
1897 },
1898
1899 CloseTerminal {
1901 terminal_id: TerminalId,
1903 },
1904
1905 GrepProject {
1909 pattern: String,
1911 fixed_string: bool,
1913 case_sensitive: bool,
1915 max_results: usize,
1917 whole_words: bool,
1919 callback_id: JsCallbackId,
1921 },
1922
1923 GrepProjectStreaming {
1928 pattern: String,
1930 fixed_string: bool,
1932 case_sensitive: bool,
1934 max_results: usize,
1936 whole_words: bool,
1938 search_id: u64,
1940 callback_id: JsCallbackId,
1942 },
1943
1944 ReplaceInBuffer {
1948 file_path: PathBuf,
1950 matches: Vec<(usize, usize)>,
1952 replacement: String,
1954 callback_id: JsCallbackId,
1956 },
1957
1958 SetAuthority {
1974 #[ts(type = "unknown")]
1975 payload: JsonValue,
1976 },
1977
1978 ClearAuthority,
1982
1983 SetRemoteIndicatorState {
2006 #[ts(type = "unknown")]
2007 state: JsonValue,
2008 },
2009
2010 ClearRemoteIndicatorState,
2014
2015 SpawnHostProcess {
2028 command: String,
2029 args: Vec<String>,
2030 cwd: Option<String>,
2031 callback_id: JsCallbackId,
2032 },
2033
2034 KillHostProcess { process_id: u64 },
2045}
2046
2047impl PluginCommand {
2048 pub fn debug_variant_name(&self) -> String {
2050 let dbg = format!("{:?}", self);
2051 dbg.split([' ', '{', '(']).next().unwrap_or("?").to_string()
2052 }
2053}
2054
2055#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
2064#[serde(rename_all = "camelCase")]
2065#[ts(export)]
2066pub struct LanguagePackConfig {
2067 #[serde(default)]
2069 pub comment_prefix: Option<String>,
2070
2071 #[serde(default)]
2073 pub block_comment_start: Option<String>,
2074
2075 #[serde(default)]
2077 pub block_comment_end: Option<String>,
2078
2079 #[serde(default)]
2081 pub use_tabs: Option<bool>,
2082
2083 #[serde(default)]
2085 pub tab_size: Option<usize>,
2086
2087 #[serde(default)]
2089 pub auto_indent: Option<bool>,
2090
2091 #[serde(default)]
2094 pub show_whitespace_tabs: Option<bool>,
2095
2096 #[serde(default)]
2098 pub formatter: Option<FormatterPackConfig>,
2099}
2100
2101#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2103#[serde(rename_all = "camelCase")]
2104#[ts(export)]
2105pub struct FormatterPackConfig {
2106 pub command: String,
2108
2109 #[serde(default)]
2111 pub args: Vec<String>,
2112}
2113
2114#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2116#[serde(rename_all = "camelCase")]
2117#[ts(export)]
2118pub struct ProcessLimitsPackConfig {
2119 #[serde(default)]
2121 pub max_memory_percent: Option<u32>,
2122
2123 #[serde(default)]
2125 pub max_cpu_percent: Option<u32>,
2126
2127 #[serde(default)]
2129 pub enabled: Option<bool>,
2130}
2131
2132#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2134#[serde(rename_all = "camelCase")]
2135#[ts(export)]
2136pub struct LspServerPackConfig {
2137 pub command: String,
2139
2140 #[serde(default)]
2142 pub args: Vec<String>,
2143
2144 #[serde(default)]
2146 pub auto_start: Option<bool>,
2147
2148 #[serde(default)]
2150 #[ts(type = "Record<string, unknown> | null")]
2151 pub initialization_options: Option<JsonValue>,
2152
2153 #[serde(default)]
2155 pub process_limits: Option<ProcessLimitsPackConfig>,
2156}
2157
2158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
2160#[ts(export)]
2161pub enum HunkStatus {
2162 Pending,
2163 Staged,
2164 Discarded,
2165}
2166
2167#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2169#[ts(export)]
2170pub struct ReviewHunk {
2171 pub id: String,
2172 pub file: String,
2173 pub context_header: String,
2174 pub status: HunkStatus,
2175 pub base_range: Option<(usize, usize)>,
2177 pub modified_range: Option<(usize, usize)>,
2179}
2180
2181#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2183#[serde(deny_unknown_fields)]
2184#[ts(export, rename = "TsActionPopupAction")]
2185pub struct ActionPopupAction {
2186 pub id: String,
2188 pub label: String,
2190}
2191
2192#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2194#[serde(deny_unknown_fields)]
2195#[ts(export)]
2196pub struct ActionPopupOptions {
2197 pub id: String,
2199 pub title: String,
2201 pub message: String,
2203 pub actions: Vec<ActionPopupAction>,
2205}
2206
2207#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2209#[ts(export)]
2210pub struct TsHighlightSpan {
2211 pub start: u32,
2212 pub end: u32,
2213 #[ts(type = "[number, number, number]")]
2214 pub color: (u8, u8, u8),
2215 pub bold: bool,
2216 pub italic: bool,
2217}
2218
2219#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2221#[ts(export)]
2222pub struct SpawnResult {
2223 pub stdout: String,
2225 pub stderr: String,
2227 pub exit_code: i32,
2229}
2230
2231#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2233#[ts(export)]
2234pub struct BackgroundProcessResult {
2235 #[ts(type = "number")]
2237 pub process_id: u64,
2238 pub exit_code: i32,
2241}
2242
2243#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2245#[serde(rename_all = "camelCase")]
2246#[ts(export, rename_all = "camelCase")]
2247pub struct GrepMatch {
2248 pub file: String,
2250 #[ts(type = "number")]
2252 pub buffer_id: usize,
2253 #[ts(type = "number")]
2255 pub byte_offset: usize,
2256 #[ts(type = "number")]
2258 pub length: usize,
2259 #[ts(type = "number")]
2261 pub line: usize,
2262 #[ts(type = "number")]
2264 pub column: usize,
2265 pub context: String,
2267}
2268
2269#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2271#[serde(rename_all = "camelCase")]
2272#[ts(export, rename_all = "camelCase")]
2273pub struct ReplaceResult {
2274 #[ts(type = "number")]
2276 pub replacements: usize,
2277 #[ts(type = "number")]
2279 pub buffer_id: usize,
2280}
2281
2282#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2284#[serde(deny_unknown_fields, rename_all = "camelCase")]
2285#[ts(export, rename = "TextPropertyEntry", rename_all = "camelCase")]
2286pub struct JsTextPropertyEntry {
2287 pub text: String,
2289 #[serde(default)]
2291 #[ts(optional, type = "Record<string, unknown>")]
2292 pub properties: Option<HashMap<String, JsonValue>>,
2293 #[serde(default)]
2295 #[ts(optional, type = "Partial<OverlayOptions>")]
2296 pub style: Option<OverlayOptions>,
2297 #[serde(default)]
2299 #[ts(optional)]
2300 pub inline_overlays: Option<Vec<crate::text_property::InlineOverlay>>,
2301}
2302
2303#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2305#[ts(export)]
2306pub struct DirEntry {
2307 pub name: String,
2309 pub is_file: bool,
2311 pub is_dir: bool,
2313}
2314
2315#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2317#[ts(export)]
2318pub struct JsPosition {
2319 pub line: u32,
2321 pub character: u32,
2323}
2324
2325#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2327#[ts(export)]
2328pub struct JsRange {
2329 pub start: JsPosition,
2331 pub end: JsPosition,
2333}
2334
2335#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2337#[ts(export)]
2338pub struct JsDiagnostic {
2339 pub uri: String,
2341 pub message: String,
2343 pub severity: Option<u8>,
2345 pub range: JsRange,
2347 #[ts(optional)]
2349 pub source: Option<String>,
2350}
2351
2352#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2354#[serde(deny_unknown_fields)]
2355#[ts(export)]
2356pub struct CreateVirtualBufferOptions {
2357 pub name: String,
2359 #[serde(default)]
2361 #[ts(optional)]
2362 pub mode: Option<String>,
2363 #[serde(default, rename = "readOnly")]
2365 #[ts(optional, rename = "readOnly")]
2366 pub read_only: Option<bool>,
2367 #[serde(default, rename = "showLineNumbers")]
2369 #[ts(optional, rename = "showLineNumbers")]
2370 pub show_line_numbers: Option<bool>,
2371 #[serde(default, rename = "showCursors")]
2373 #[ts(optional, rename = "showCursors")]
2374 pub show_cursors: Option<bool>,
2375 #[serde(default, rename = "editingDisabled")]
2377 #[ts(optional, rename = "editingDisabled")]
2378 pub editing_disabled: Option<bool>,
2379 #[serde(default, rename = "hiddenFromTabs")]
2381 #[ts(optional, rename = "hiddenFromTabs")]
2382 pub hidden_from_tabs: Option<bool>,
2383 #[serde(default)]
2385 #[ts(optional)]
2386 pub entries: Option<Vec<JsTextPropertyEntry>>,
2387}
2388
2389#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2391#[serde(deny_unknown_fields)]
2392#[ts(export)]
2393pub struct CreateVirtualBufferInSplitOptions {
2394 pub name: String,
2396 #[serde(default)]
2398 #[ts(optional)]
2399 pub mode: Option<String>,
2400 #[serde(default, rename = "readOnly")]
2402 #[ts(optional, rename = "readOnly")]
2403 pub read_only: Option<bool>,
2404 #[serde(default)]
2406 #[ts(optional)]
2407 pub ratio: Option<f32>,
2408 #[serde(default)]
2410 #[ts(optional)]
2411 pub direction: Option<String>,
2412 #[serde(default, rename = "panelId")]
2414 #[ts(optional, rename = "panelId")]
2415 pub panel_id: Option<String>,
2416 #[serde(default, rename = "showLineNumbers")]
2418 #[ts(optional, rename = "showLineNumbers")]
2419 pub show_line_numbers: Option<bool>,
2420 #[serde(default, rename = "showCursors")]
2422 #[ts(optional, rename = "showCursors")]
2423 pub show_cursors: Option<bool>,
2424 #[serde(default, rename = "editingDisabled")]
2426 #[ts(optional, rename = "editingDisabled")]
2427 pub editing_disabled: Option<bool>,
2428 #[serde(default, rename = "lineWrap")]
2430 #[ts(optional, rename = "lineWrap")]
2431 pub line_wrap: Option<bool>,
2432 #[serde(default)]
2434 #[ts(optional)]
2435 pub before: Option<bool>,
2436 #[serde(default)]
2438 #[ts(optional)]
2439 pub entries: Option<Vec<JsTextPropertyEntry>>,
2440}
2441
2442#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2444#[serde(deny_unknown_fields)]
2445#[ts(export)]
2446pub struct CreateVirtualBufferInExistingSplitOptions {
2447 pub name: String,
2449 #[serde(rename = "splitId")]
2451 #[ts(rename = "splitId")]
2452 pub split_id: usize,
2453 #[serde(default)]
2455 #[ts(optional)]
2456 pub mode: Option<String>,
2457 #[serde(default, rename = "readOnly")]
2459 #[ts(optional, rename = "readOnly")]
2460 pub read_only: Option<bool>,
2461 #[serde(default, rename = "showLineNumbers")]
2463 #[ts(optional, rename = "showLineNumbers")]
2464 pub show_line_numbers: Option<bool>,
2465 #[serde(default, rename = "showCursors")]
2467 #[ts(optional, rename = "showCursors")]
2468 pub show_cursors: Option<bool>,
2469 #[serde(default, rename = "editingDisabled")]
2471 #[ts(optional, rename = "editingDisabled")]
2472 pub editing_disabled: Option<bool>,
2473 #[serde(default, rename = "lineWrap")]
2475 #[ts(optional, rename = "lineWrap")]
2476 pub line_wrap: Option<bool>,
2477 #[serde(default)]
2479 #[ts(optional)]
2480 pub entries: Option<Vec<JsTextPropertyEntry>>,
2481}
2482
2483#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2485#[serde(deny_unknown_fields)]
2486#[ts(export)]
2487pub struct CreateTerminalOptions {
2488 #[serde(default)]
2490 #[ts(optional)]
2491 pub cwd: Option<String>,
2492 #[serde(default)]
2494 #[ts(optional)]
2495 pub direction: Option<String>,
2496 #[serde(default)]
2498 #[ts(optional)]
2499 pub ratio: Option<f32>,
2500 #[serde(default)]
2502 #[ts(optional)]
2503 pub focus: Option<bool>,
2504 #[serde(default)]
2511 #[ts(optional)]
2512 pub persistent: Option<bool>,
2513}
2514
2515#[derive(Debug, Clone, Serialize, TS)]
2520#[ts(export, type = "Array<Record<string, unknown>>")]
2521pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
2522
2523#[cfg(feature = "plugins")]
2525mod fromjs_impls {
2526 use super::*;
2527 use rquickjs::{Ctx, FromJs, Value};
2528
2529 impl<'js> FromJs<'js> for JsTextPropertyEntry {
2530 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2531 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2532 from: "object",
2533 to: "JsTextPropertyEntry",
2534 message: Some(e.to_string()),
2535 })
2536 }
2537 }
2538
2539 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
2540 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2541 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2542 from: "object",
2543 to: "CreateVirtualBufferOptions",
2544 message: Some(e.to_string()),
2545 })
2546 }
2547 }
2548
2549 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
2550 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2551 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2552 from: "object",
2553 to: "CreateVirtualBufferInSplitOptions",
2554 message: Some(e.to_string()),
2555 })
2556 }
2557 }
2558
2559 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
2560 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2561 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2562 from: "object",
2563 to: "CreateVirtualBufferInExistingSplitOptions",
2564 message: Some(e.to_string()),
2565 })
2566 }
2567 }
2568
2569 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
2570 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
2571 rquickjs_serde::to_value(ctx.clone(), &self.0)
2572 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
2573 }
2574 }
2575
2576 impl<'js> FromJs<'js> for ActionSpec {
2579 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2580 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2581 from: "object",
2582 to: "ActionSpec",
2583 message: Some(e.to_string()),
2584 })
2585 }
2586 }
2587
2588 impl<'js> FromJs<'js> for ActionPopupAction {
2589 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2590 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2591 from: "object",
2592 to: "ActionPopupAction",
2593 message: Some(e.to_string()),
2594 })
2595 }
2596 }
2597
2598 impl<'js> FromJs<'js> for ActionPopupOptions {
2599 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2600 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2601 from: "object",
2602 to: "ActionPopupOptions",
2603 message: Some(e.to_string()),
2604 })
2605 }
2606 }
2607
2608 impl<'js> FromJs<'js> for ViewTokenWire {
2609 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2610 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2611 from: "object",
2612 to: "ViewTokenWire",
2613 message: Some(e.to_string()),
2614 })
2615 }
2616 }
2617
2618 impl<'js> FromJs<'js> for ViewTokenStyle {
2619 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2620 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2621 from: "object",
2622 to: "ViewTokenStyle",
2623 message: Some(e.to_string()),
2624 })
2625 }
2626 }
2627
2628 impl<'js> FromJs<'js> for LayoutHints {
2629 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2630 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2631 from: "object",
2632 to: "LayoutHints",
2633 message: Some(e.to_string()),
2634 })
2635 }
2636 }
2637
2638 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
2639 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2640 let json: serde_json::Value =
2642 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2643 from: "object",
2644 to: "CreateCompositeBufferOptions (json)",
2645 message: Some(e.to_string()),
2646 })?;
2647 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
2648 from: "json",
2649 to: "CreateCompositeBufferOptions",
2650 message: Some(e.to_string()),
2651 })
2652 }
2653 }
2654
2655 impl<'js> FromJs<'js> for CompositeHunk {
2656 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2657 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2658 from: "object",
2659 to: "CompositeHunk",
2660 message: Some(e.to_string()),
2661 })
2662 }
2663 }
2664
2665 impl<'js> FromJs<'js> for LanguagePackConfig {
2666 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2667 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2668 from: "object",
2669 to: "LanguagePackConfig",
2670 message: Some(e.to_string()),
2671 })
2672 }
2673 }
2674
2675 impl<'js> FromJs<'js> for LspServerPackConfig {
2676 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2677 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2678 from: "object",
2679 to: "LspServerPackConfig",
2680 message: Some(e.to_string()),
2681 })
2682 }
2683 }
2684
2685 impl<'js> FromJs<'js> for ProcessLimitsPackConfig {
2686 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2687 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2688 from: "object",
2689 to: "ProcessLimitsPackConfig",
2690 message: Some(e.to_string()),
2691 })
2692 }
2693 }
2694
2695 impl<'js> FromJs<'js> for CreateTerminalOptions {
2696 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2697 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2698 from: "object",
2699 to: "CreateTerminalOptions",
2700 message: Some(e.to_string()),
2701 })
2702 }
2703 }
2704
2705 #[cfg(test)]
2717 mod tests {
2718 use super::*;
2719 use rquickjs::{Context, Runtime};
2720
2721 fn with_js<R>(f: impl for<'js> FnOnce(Ctx<'js>) -> R) -> R {
2724 let rt = Runtime::new().expect("create rquickjs runtime");
2725 let ctx = Context::full(&rt).expect("create rquickjs context");
2726 ctx.with(f)
2727 }
2728
2729 fn eval_as<T>(src: &str) -> T
2731 where
2732 for<'js> T: rquickjs::FromJs<'js>,
2733 {
2734 with_js(|ctx| {
2735 let value: Value = ctx
2736 .eval::<Value, _>(src.as_bytes())
2737 .expect("eval JS source");
2738 T::from_js(&ctx, value).expect("from_js decode")
2739 })
2740 }
2741
2742 #[test]
2743 fn js_text_property_entry_decodes_text_and_properties() {
2744 let got: JsTextPropertyEntry =
2745 eval_as("({text: 'hello', properties: {file: '/x.rs'}})");
2746 assert_eq!(got.text, "hello");
2747 let props = got.properties.expect("properties present");
2748 assert_eq!(props.get("file").and_then(|v| v.as_str()), Some("/x.rs"));
2749 }
2750
2751 #[test]
2752 fn create_virtual_buffer_options_decodes_name() {
2753 let got: CreateVirtualBufferOptions = eval_as("({name: 'logs', readOnly: true})");
2754 assert_eq!(got.name, "logs");
2755 assert_eq!(got.read_only, Some(true));
2756 }
2757
2758 #[test]
2759 fn create_virtual_buffer_in_split_options_decodes_ratio() {
2760 let got: CreateVirtualBufferInSplitOptions =
2761 eval_as("({name: 'diag', ratio: 0.25, direction: 'horizontal'})");
2762 assert_eq!(got.name, "diag");
2763 assert!(matches!(got.ratio, Some(r) if (r - 0.25).abs() < 1e-6));
2764 assert_eq!(got.direction.as_deref(), Some("horizontal"));
2765 }
2766
2767 #[test]
2768 fn create_virtual_buffer_in_existing_split_options_decodes_splitid() {
2769 let got: CreateVirtualBufferInExistingSplitOptions =
2770 eval_as("({name: 'n', splitId: 7})");
2771 assert_eq!(got.name, "n");
2772 assert_eq!(got.split_id, 7);
2773 }
2774
2775 #[test]
2776 fn create_terminal_options_decodes_cwd_and_focus() {
2777 let got: CreateTerminalOptions =
2778 eval_as("({cwd: '/tmp', direction: 'vertical', focus: false})");
2779 assert_eq!(got.cwd.as_deref(), Some("/tmp"));
2780 assert_eq!(got.direction.as_deref(), Some("vertical"));
2781 assert_eq!(got.focus, Some(false));
2782 }
2783
2784 #[test]
2785 fn action_spec_decodes_action_and_count() {
2786 let got: ActionSpec = eval_as("({action: 'move_word_right', count: 5})");
2787 assert_eq!(got.action, "move_word_right");
2788 assert_eq!(got.count, 5);
2789 }
2790
2791 #[test]
2792 fn action_popup_action_decodes_id_and_label() {
2793 let got: ActionPopupAction = eval_as("({id: 'ok', label: 'OK'})");
2794 assert_eq!(got.id, "ok");
2795 assert_eq!(got.label, "OK");
2796 }
2797
2798 #[test]
2799 fn action_popup_options_decodes_actions_list() {
2800 let got: ActionPopupOptions = eval_as(
2801 "({id: 'p', title: 't', message: 'm', \
2802 actions: [{id: 'ok', label: 'OK'}]})",
2803 );
2804 assert_eq!(got.id, "p");
2805 assert_eq!(got.title, "t");
2806 assert_eq!(got.message, "m");
2807 assert_eq!(got.actions.len(), 1);
2808 assert_eq!(got.actions[0].id, "ok");
2809 }
2810
2811 #[test]
2812 fn view_token_wire_decodes_offset_and_kind() {
2813 let got: ViewTokenWire = eval_as("({source_offset: 42, kind: 'Newline'})");
2817 assert_eq!(got.source_offset, Some(42));
2818 assert!(matches!(got.kind, ViewTokenWireKind::Newline));
2819 }
2820
2821 #[test]
2822 fn view_token_style_decodes_boolean_flags() {
2823 let got: ViewTokenStyle = eval_as("({bold: true, italic: true})");
2827 assert!(got.bold);
2828 assert!(got.italic);
2829 assert!(got.fg.is_none());
2830 }
2831
2832 #[test]
2833 fn layout_hints_decodes_compose_width() {
2834 let got: LayoutHints = eval_as("({composeWidth: 120})");
2835 assert_eq!(got.compose_width, Some(120));
2836 assert!(got.column_guides.is_none());
2837 }
2838
2839 #[test]
2840 fn create_composite_buffer_options_decodes_name_and_sources() {
2841 let got: CreateCompositeBufferOptions = eval_as(
2842 "({name: 'diff', mode: 'm', \
2843 layout: {type: 'side-by-side', ratios: [0.5, 0.5], showSeparator: true}, \
2844 sources: [{bufferId: 3, label: 'OLD'}]})",
2845 );
2846 assert_eq!(got.name, "diff");
2847 assert_eq!(got.layout.layout_type, "side-by-side");
2848 assert_eq!(got.sources.len(), 1);
2849 assert_eq!(got.sources[0].buffer_id, 3);
2850 assert_eq!(got.sources[0].label, "OLD");
2851 }
2852
2853 #[test]
2854 fn composite_hunk_decodes_all_fields() {
2855 let got: CompositeHunk =
2856 eval_as("({oldStart: 1, oldCount: 2, newStart: 3, newCount: 4})");
2857 assert_eq!(got.old_start, 1);
2858 assert_eq!(got.old_count, 2);
2859 assert_eq!(got.new_start, 3);
2860 assert_eq!(got.new_count, 4);
2861 }
2862
2863 #[test]
2864 fn language_pack_config_decodes_comment_prefix_and_tab_size() {
2865 let got: LanguagePackConfig =
2866 eval_as("({commentPrefix: '//', tabSize: 7, useTabs: true})");
2867 assert_eq!(got.comment_prefix.as_deref(), Some("//"));
2868 assert_eq!(got.tab_size, Some(7));
2869 assert_eq!(got.use_tabs, Some(true));
2870 }
2871
2872 #[test]
2873 fn lsp_server_pack_config_decodes_command_and_args() {
2874 let got: LspServerPackConfig =
2875 eval_as("({command: 'rust-analyzer', args: ['--log'], autoStart: true})");
2876 assert_eq!(got.command, "rust-analyzer");
2877 assert_eq!(got.args, vec!["--log".to_string()]);
2878 assert_eq!(got.auto_start, Some(true));
2879 }
2880
2881 #[test]
2882 fn process_limits_pack_config_decodes_percentages() {
2883 let got: ProcessLimitsPackConfig =
2884 eval_as("({maxMemoryPercent: 75, maxCpuPercent: 50, enabled: true})");
2885 assert_eq!(got.max_memory_percent, Some(75));
2886 assert_eq!(got.max_cpu_percent, Some(50));
2887 assert_eq!(got.enabled, Some(true));
2888 }
2889
2890 #[test]
2895 fn text_properties_at_cursor_into_js_preserves_length() {
2896 use rquickjs::IntoJs;
2897 with_js(|ctx| {
2898 let mut entry = std::collections::HashMap::new();
2899 entry.insert("k".to_string(), serde_json::json!("v"));
2900 let payload = TextPropertiesAtCursor(vec![entry.clone(), entry]);
2901
2902 let v = payload.into_js(&ctx).expect("into_js");
2903 let arr = v.as_array().expect("expected JS array");
2904 assert_eq!(arr.len(), 2);
2905 });
2906 }
2907 }
2908}
2909
2910pub struct PluginApi {
2912 hooks: Arc<RwLock<HookRegistry>>,
2914
2915 commands: Arc<RwLock<CommandRegistry>>,
2917
2918 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2920
2921 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2923}
2924
2925impl PluginApi {
2926 pub fn new(
2928 hooks: Arc<RwLock<HookRegistry>>,
2929 commands: Arc<RwLock<CommandRegistry>>,
2930 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2931 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2932 ) -> Self {
2933 Self {
2934 hooks,
2935 commands,
2936 command_sender,
2937 state_snapshot,
2938 }
2939 }
2940
2941 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
2943 let mut hooks = self.hooks.write().unwrap();
2944 hooks.add_hook(hook_name, callback);
2945 }
2946
2947 pub fn unregister_hooks(&self, hook_name: &str) {
2949 let mut hooks = self.hooks.write().unwrap();
2950 hooks.remove_hooks(hook_name);
2951 }
2952
2953 pub fn register_command(&self, command: Command) {
2955 let commands = self.commands.read().unwrap();
2956 commands.register(command);
2957 }
2958
2959 pub fn unregister_command(&self, name: &str) {
2961 let commands = self.commands.read().unwrap();
2962 commands.unregister(name);
2963 }
2964
2965 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
2967 self.command_sender
2968 .send(command)
2969 .map_err(|e| format!("Failed to send command: {}", e))
2970 }
2971
2972 pub fn insert_text(
2974 &self,
2975 buffer_id: BufferId,
2976 position: usize,
2977 text: String,
2978 ) -> Result<(), String> {
2979 self.send_command(PluginCommand::InsertText {
2980 buffer_id,
2981 position,
2982 text,
2983 })
2984 }
2985
2986 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
2988 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
2989 }
2990
2991 pub fn add_overlay(
2999 &self,
3000 buffer_id: BufferId,
3001 namespace: Option<String>,
3002 range: Range<usize>,
3003 options: OverlayOptions,
3004 ) -> Result<(), String> {
3005 self.send_command(PluginCommand::AddOverlay {
3006 buffer_id,
3007 namespace: namespace.map(OverlayNamespace::from_string),
3008 range,
3009 options,
3010 })
3011 }
3012
3013 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
3015 self.send_command(PluginCommand::RemoveOverlay {
3016 buffer_id,
3017 handle: OverlayHandle::from_string(handle),
3018 })
3019 }
3020
3021 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
3023 self.send_command(PluginCommand::ClearNamespace {
3024 buffer_id,
3025 namespace: OverlayNamespace::from_string(namespace),
3026 })
3027 }
3028
3029 pub fn clear_overlays_in_range(
3032 &self,
3033 buffer_id: BufferId,
3034 start: usize,
3035 end: usize,
3036 ) -> Result<(), String> {
3037 self.send_command(PluginCommand::ClearOverlaysInRange {
3038 buffer_id,
3039 start,
3040 end,
3041 })
3042 }
3043
3044 pub fn set_status(&self, message: String) -> Result<(), String> {
3046 self.send_command(PluginCommand::SetStatus { message })
3047 }
3048
3049 pub fn open_file_at_location(
3052 &self,
3053 path: PathBuf,
3054 line: Option<usize>,
3055 column: Option<usize>,
3056 ) -> Result<(), String> {
3057 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
3058 }
3059
3060 pub fn open_file_in_split(
3065 &self,
3066 split_id: usize,
3067 path: PathBuf,
3068 line: Option<usize>,
3069 column: Option<usize>,
3070 ) -> Result<(), String> {
3071 self.send_command(PluginCommand::OpenFileInSplit {
3072 split_id,
3073 path,
3074 line,
3075 column,
3076 })
3077 }
3078
3079 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
3082 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
3083 }
3084
3085 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
3088 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
3089 }
3090
3091 pub fn set_prompt_input_sync(&self, sync: bool) -> Result<(), String> {
3093 self.send_command(PluginCommand::SetPromptInputSync { sync })
3094 }
3095
3096 pub fn add_menu_item(
3098 &self,
3099 menu_label: String,
3100 item: MenuItem,
3101 position: MenuPosition,
3102 ) -> Result<(), String> {
3103 self.send_command(PluginCommand::AddMenuItem {
3104 menu_label,
3105 item,
3106 position,
3107 })
3108 }
3109
3110 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
3112 self.send_command(PluginCommand::AddMenu { menu, position })
3113 }
3114
3115 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
3117 self.send_command(PluginCommand::RemoveMenuItem {
3118 menu_label,
3119 item_label,
3120 })
3121 }
3122
3123 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
3125 self.send_command(PluginCommand::RemoveMenu { menu_label })
3126 }
3127
3128 pub fn create_virtual_buffer(
3135 &self,
3136 name: String,
3137 mode: String,
3138 read_only: bool,
3139 ) -> Result<(), String> {
3140 self.send_command(PluginCommand::CreateVirtualBuffer {
3141 name,
3142 mode,
3143 read_only,
3144 })
3145 }
3146
3147 pub fn create_virtual_buffer_with_content(
3153 &self,
3154 name: String,
3155 mode: String,
3156 read_only: bool,
3157 entries: Vec<TextPropertyEntry>,
3158 ) -> Result<(), String> {
3159 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
3160 name,
3161 mode,
3162 read_only,
3163 entries,
3164 show_line_numbers: true,
3165 show_cursors: true,
3166 editing_disabled: false,
3167 hidden_from_tabs: false,
3168 request_id: None,
3169 })
3170 }
3171
3172 pub fn set_virtual_buffer_content(
3176 &self,
3177 buffer_id: BufferId,
3178 entries: Vec<TextPropertyEntry>,
3179 ) -> Result<(), String> {
3180 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
3181 }
3182
3183 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
3187 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
3188 }
3189
3190 pub fn define_mode(
3194 &self,
3195 name: String,
3196 bindings: Vec<(String, String)>,
3197 read_only: bool,
3198 allow_text_input: bool,
3199 ) -> Result<(), String> {
3200 self.send_command(PluginCommand::DefineMode {
3201 name,
3202 bindings,
3203 read_only,
3204 allow_text_input,
3205 inherit_normal_bindings: false,
3206 plugin_name: None,
3207 })
3208 }
3209
3210 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
3212 self.send_command(PluginCommand::ShowBuffer { buffer_id })
3213 }
3214
3215 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
3217 self.send_command(PluginCommand::SetSplitScroll {
3218 split_id: SplitId(split_id),
3219 top_byte,
3220 })
3221 }
3222
3223 pub fn get_highlights(
3225 &self,
3226 buffer_id: BufferId,
3227 range: Range<usize>,
3228 request_id: u64,
3229 ) -> Result<(), String> {
3230 self.send_command(PluginCommand::RequestHighlights {
3231 buffer_id,
3232 range,
3233 request_id,
3234 })
3235 }
3236
3237 pub fn get_active_buffer_id(&self) -> BufferId {
3241 let snapshot = self.state_snapshot.read().unwrap();
3242 snapshot.active_buffer_id
3243 }
3244
3245 pub fn get_active_split_id(&self) -> usize {
3247 let snapshot = self.state_snapshot.read().unwrap();
3248 snapshot.active_split_id
3249 }
3250
3251 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
3253 let snapshot = self.state_snapshot.read().unwrap();
3254 snapshot.buffers.get(&buffer_id).cloned()
3255 }
3256
3257 pub fn list_buffers(&self) -> Vec<BufferInfo> {
3259 let snapshot = self.state_snapshot.read().unwrap();
3260 snapshot.buffers.values().cloned().collect()
3261 }
3262
3263 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
3265 let snapshot = self.state_snapshot.read().unwrap();
3266 snapshot.primary_cursor.clone()
3267 }
3268
3269 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
3271 let snapshot = self.state_snapshot.read().unwrap();
3272 snapshot.all_cursors.clone()
3273 }
3274
3275 pub fn get_viewport(&self) -> Option<ViewportInfo> {
3277 let snapshot = self.state_snapshot.read().unwrap();
3278 snapshot.viewport.clone()
3279 }
3280
3281 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
3283 Arc::clone(&self.state_snapshot)
3284 }
3285}
3286
3287impl Clone for PluginApi {
3288 fn clone(&self) -> Self {
3289 Self {
3290 hooks: Arc::clone(&self.hooks),
3291 commands: Arc::clone(&self.commands),
3292 command_sender: self.command_sender.clone(),
3293 state_snapshot: Arc::clone(&self.state_snapshot),
3294 }
3295 }
3296}
3297
3298#[derive(Debug, Clone, Serialize, Deserialize, TS)]
3312#[serde(rename_all = "camelCase", deny_unknown_fields)]
3313#[ts(export, rename_all = "camelCase")]
3314pub struct TsCompletionCandidate {
3315 pub label: String,
3317
3318 #[serde(skip_serializing_if = "Option::is_none")]
3320 pub insert_text: Option<String>,
3321
3322 #[serde(skip_serializing_if = "Option::is_none")]
3324 pub detail: Option<String>,
3325
3326 #[serde(skip_serializing_if = "Option::is_none")]
3328 pub icon: Option<String>,
3329
3330 #[serde(default)]
3332 pub score: i64,
3333
3334 #[serde(default)]
3336 pub is_snippet: bool,
3337
3338 #[serde(skip_serializing_if = "Option::is_none")]
3340 pub provider_data: Option<String>,
3341}
3342
3343#[derive(Debug, Clone, Serialize, Deserialize, TS)]
3348#[serde(rename_all = "camelCase")]
3349#[ts(export, rename_all = "camelCase")]
3350pub struct TsCompletionContext {
3351 pub prefix: String,
3353
3354 pub cursor_byte: usize,
3356
3357 pub word_start_byte: usize,
3359
3360 pub buffer_len: usize,
3362
3363 pub is_large_file: bool,
3365
3366 pub text_around_cursor: String,
3369
3370 pub cursor_offset_in_text: usize,
3372
3373 #[serde(skip_serializing_if = "Option::is_none")]
3375 pub language_id: Option<String>,
3376}
3377
3378#[derive(Debug, Clone, Serialize, Deserialize, TS)]
3380#[serde(rename_all = "camelCase", deny_unknown_fields)]
3381#[ts(export, rename_all = "camelCase")]
3382pub struct TsCompletionProviderRegistration {
3383 pub id: String,
3385
3386 pub display_name: String,
3388
3389 #[serde(default = "default_plugin_provider_priority")]
3392 pub priority: u32,
3393
3394 #[serde(default)]
3397 pub language_ids: Vec<String>,
3398}
3399
3400fn default_plugin_provider_priority() -> u32 {
3401 50
3402}
3403
3404#[cfg(test)]
3405mod tests {
3406 use super::*;
3407
3408 #[test]
3409 fn test_plugin_api_creation() {
3410 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3411 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3412 let (tx, _rx) = std::sync::mpsc::channel();
3413 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3414
3415 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3416
3417 let _clone = api.clone();
3419 }
3420
3421 #[test]
3422 fn test_register_hook() {
3423 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3424 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3425 let (tx, _rx) = std::sync::mpsc::channel();
3426 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3427
3428 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
3429
3430 api.register_hook("test-hook", Box::new(|_| true));
3431
3432 let hook_registry = hooks.read().unwrap();
3433 assert_eq!(hook_registry.hook_count("test-hook"), 1);
3434 }
3435
3436 #[test]
3437 fn test_send_command() {
3438 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3439 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3440 let (tx, rx) = std::sync::mpsc::channel();
3441 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3442
3443 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3444
3445 let result = api.insert_text(BufferId(1), 0, "test".to_string());
3446 assert!(result.is_ok());
3447
3448 let received = rx.try_recv();
3450 assert!(received.is_ok());
3451
3452 match received.unwrap() {
3453 PluginCommand::InsertText {
3454 buffer_id,
3455 position,
3456 text,
3457 } => {
3458 assert_eq!(buffer_id.0, 1);
3459 assert_eq!(position, 0);
3460 assert_eq!(text, "test");
3461 }
3462 _ => panic!("Wrong command type"),
3463 }
3464 }
3465
3466 #[test]
3467 fn test_add_overlay_command() {
3468 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3469 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3470 let (tx, rx) = std::sync::mpsc::channel();
3471 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3472
3473 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3474
3475 let result = api.add_overlay(
3476 BufferId(1),
3477 Some("test-overlay".to_string()),
3478 0..10,
3479 OverlayOptions {
3480 fg: Some(OverlayColorSpec::ThemeKey("ui.status_bar_fg".to_string())),
3481 bg: None,
3482 underline: true,
3483 bold: false,
3484 italic: false,
3485 strikethrough: false,
3486 extend_to_line_end: false,
3487 url: None,
3488 },
3489 );
3490 assert!(result.is_ok());
3491
3492 let received = rx.try_recv().unwrap();
3493 match received {
3494 PluginCommand::AddOverlay {
3495 buffer_id,
3496 namespace,
3497 range,
3498 options,
3499 } => {
3500 assert_eq!(buffer_id.0, 1);
3501 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
3502 assert_eq!(range, 0..10);
3503 assert!(matches!(
3504 options.fg,
3505 Some(OverlayColorSpec::ThemeKey(ref k)) if k == "ui.status_bar_fg"
3506 ));
3507 assert!(options.bg.is_none());
3508 assert!(options.underline);
3509 assert!(!options.bold);
3510 assert!(!options.italic);
3511 assert!(!options.extend_to_line_end);
3512 }
3513 _ => panic!("Wrong command type"),
3514 }
3515 }
3516
3517 #[test]
3518 fn test_set_status_command() {
3519 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3520 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3521 let (tx, rx) = std::sync::mpsc::channel();
3522 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3523
3524 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3525
3526 let result = api.set_status("Test status".to_string());
3527 assert!(result.is_ok());
3528
3529 let received = rx.try_recv().unwrap();
3530 match received {
3531 PluginCommand::SetStatus { message } => {
3532 assert_eq!(message, "Test status");
3533 }
3534 _ => panic!("Wrong command type"),
3535 }
3536 }
3537
3538 #[test]
3539 fn test_get_active_buffer_id() {
3540 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3541 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3542 let (tx, _rx) = std::sync::mpsc::channel();
3543 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3544
3545 {
3547 let mut snapshot = state_snapshot.write().unwrap();
3548 snapshot.active_buffer_id = BufferId(5);
3549 }
3550
3551 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3552
3553 let active_id = api.get_active_buffer_id();
3554 assert_eq!(active_id.0, 5);
3555 }
3556
3557 #[test]
3558 fn test_get_buffer_info() {
3559 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3560 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3561 let (tx, _rx) = std::sync::mpsc::channel();
3562 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3563
3564 {
3566 let mut snapshot = state_snapshot.write().unwrap();
3567 let buffer_info = BufferInfo {
3568 id: BufferId(1),
3569 path: Some(std::path::PathBuf::from("/test/file.txt")),
3570 modified: true,
3571 length: 100,
3572 is_virtual: false,
3573 view_mode: "source".to_string(),
3574 is_composing_in_any_split: false,
3575 compose_width: None,
3576 language: "text".to_string(),
3577 is_preview: false,
3578 splits: Vec::new(),
3579 };
3580 snapshot.buffers.insert(BufferId(1), buffer_info);
3581 }
3582
3583 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3584
3585 let info = api.get_buffer_info(BufferId(1));
3586 assert!(info.is_some());
3587 let info = info.unwrap();
3588 assert_eq!(info.id.0, 1);
3589 assert_eq!(
3590 info.path.as_ref().unwrap().to_str().unwrap(),
3591 "/test/file.txt"
3592 );
3593 assert!(info.modified);
3594 assert_eq!(info.length, 100);
3595
3596 let no_info = api.get_buffer_info(BufferId(999));
3598 assert!(no_info.is_none());
3599 }
3600
3601 #[test]
3602 fn test_list_buffers() {
3603 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3604 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3605 let (tx, _rx) = std::sync::mpsc::channel();
3606 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3607
3608 {
3610 let mut snapshot = state_snapshot.write().unwrap();
3611 snapshot.buffers.insert(
3612 BufferId(1),
3613 BufferInfo {
3614 id: BufferId(1),
3615 path: Some(std::path::PathBuf::from("/file1.txt")),
3616 modified: false,
3617 length: 50,
3618 is_virtual: false,
3619 view_mode: "source".to_string(),
3620 is_composing_in_any_split: false,
3621 compose_width: None,
3622 language: "text".to_string(),
3623 is_preview: false,
3624 splits: Vec::new(),
3625 },
3626 );
3627 snapshot.buffers.insert(
3628 BufferId(2),
3629 BufferInfo {
3630 id: BufferId(2),
3631 path: Some(std::path::PathBuf::from("/file2.txt")),
3632 modified: true,
3633 length: 100,
3634 is_virtual: false,
3635 view_mode: "source".to_string(),
3636 is_composing_in_any_split: false,
3637 compose_width: None,
3638 language: "text".to_string(),
3639 is_preview: false,
3640 splits: Vec::new(),
3641 },
3642 );
3643 snapshot.buffers.insert(
3644 BufferId(3),
3645 BufferInfo {
3646 id: BufferId(3),
3647 path: None,
3648 modified: false,
3649 length: 0,
3650 is_virtual: true,
3651 view_mode: "source".to_string(),
3652 is_composing_in_any_split: false,
3653 compose_width: None,
3654 language: "text".to_string(),
3655 is_preview: false,
3656 splits: Vec::new(),
3657 },
3658 );
3659 }
3660
3661 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3662
3663 let buffers = api.list_buffers();
3664 assert_eq!(buffers.len(), 3);
3665
3666 assert!(buffers.iter().any(|b| b.id.0 == 1));
3668 assert!(buffers.iter().any(|b| b.id.0 == 2));
3669 assert!(buffers.iter().any(|b| b.id.0 == 3));
3670 }
3671
3672 #[test]
3673 fn test_get_primary_cursor() {
3674 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3675 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3676 let (tx, _rx) = std::sync::mpsc::channel();
3677 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3678
3679 {
3681 let mut snapshot = state_snapshot.write().unwrap();
3682 snapshot.primary_cursor = Some(CursorInfo {
3683 position: 42,
3684 selection: Some(10..42),
3685 });
3686 }
3687
3688 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3689
3690 let cursor = api.get_primary_cursor();
3691 assert!(cursor.is_some());
3692 let cursor = cursor.unwrap();
3693 assert_eq!(cursor.position, 42);
3694 assert_eq!(cursor.selection, Some(10..42));
3695 }
3696
3697 #[test]
3698 fn test_get_all_cursors() {
3699 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3700 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3701 let (tx, _rx) = std::sync::mpsc::channel();
3702 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3703
3704 {
3706 let mut snapshot = state_snapshot.write().unwrap();
3707 snapshot.all_cursors = vec![
3708 CursorInfo {
3709 position: 10,
3710 selection: None,
3711 },
3712 CursorInfo {
3713 position: 20,
3714 selection: Some(15..20),
3715 },
3716 CursorInfo {
3717 position: 30,
3718 selection: Some(25..30),
3719 },
3720 ];
3721 }
3722
3723 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3724
3725 let cursors = api.get_all_cursors();
3726 assert_eq!(cursors.len(), 3);
3727 assert_eq!(cursors[0].position, 10);
3728 assert_eq!(cursors[0].selection, None);
3729 assert_eq!(cursors[1].position, 20);
3730 assert_eq!(cursors[1].selection, Some(15..20));
3731 assert_eq!(cursors[2].position, 30);
3732 assert_eq!(cursors[2].selection, Some(25..30));
3733 }
3734
3735 #[test]
3736 fn test_get_viewport() {
3737 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3738 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3739 let (tx, _rx) = std::sync::mpsc::channel();
3740 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3741
3742 {
3744 let mut snapshot = state_snapshot.write().unwrap();
3745 snapshot.viewport = Some(ViewportInfo {
3746 top_byte: 100,
3747 top_line: Some(5),
3748 left_column: 5,
3749 width: 80,
3750 height: 24,
3751 });
3752 }
3753
3754 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3755
3756 let viewport = api.get_viewport();
3757 assert!(viewport.is_some());
3758 let viewport = viewport.unwrap();
3759 assert_eq!(viewport.top_byte, 100);
3760 assert_eq!(viewport.left_column, 5);
3761 assert_eq!(viewport.width, 80);
3762 assert_eq!(viewport.height, 24);
3763 }
3764
3765 #[test]
3766 fn test_composite_buffer_options_rejects_unknown_fields() {
3767 let valid_json = r#"{
3769 "name": "test",
3770 "mode": "diff",
3771 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3772 "sources": [{"bufferId": 1, "label": "old"}]
3773 }"#;
3774 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
3775 assert!(
3776 result.is_ok(),
3777 "Valid JSON should parse: {:?}",
3778 result.err()
3779 );
3780
3781 let invalid_json = r#"{
3783 "name": "test",
3784 "mode": "diff",
3785 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3786 "sources": [{"buffer_id": 1, "label": "old"}]
3787 }"#;
3788 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
3789 assert!(
3790 result.is_err(),
3791 "JSON with unknown field should fail to parse"
3792 );
3793 let err = result.unwrap_err().to_string();
3794 assert!(
3795 err.contains("unknown field") || err.contains("buffer_id"),
3796 "Error should mention unknown field: {}",
3797 err
3798 );
3799 }
3800
3801 #[test]
3802 fn test_composite_hunk_rejects_unknown_fields() {
3803 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3805 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
3806 assert!(
3807 result.is_ok(),
3808 "Valid JSON should parse: {:?}",
3809 result.err()
3810 );
3811
3812 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3814 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
3815 assert!(
3816 result.is_err(),
3817 "JSON with unknown field should fail to parse"
3818 );
3819 let err = result.unwrap_err().to_string();
3820 assert!(
3821 err.contains("unknown field") || err.contains("old_start"),
3822 "Error should mention unknown field: {}",
3823 err
3824 );
3825 }
3826
3827 #[test]
3828 fn test_plugin_response_line_end_position() {
3829 let response = PluginResponse::LineEndPosition {
3830 request_id: 42,
3831 position: Some(100),
3832 };
3833 let json = serde_json::to_string(&response).unwrap();
3834 assert!(json.contains("LineEndPosition"));
3835 assert!(json.contains("42"));
3836 assert!(json.contains("100"));
3837
3838 let response_none = PluginResponse::LineEndPosition {
3840 request_id: 1,
3841 position: None,
3842 };
3843 let json_none = serde_json::to_string(&response_none).unwrap();
3844 assert!(json_none.contains("null"));
3845 }
3846
3847 #[test]
3848 fn test_plugin_response_buffer_line_count() {
3849 let response = PluginResponse::BufferLineCount {
3850 request_id: 99,
3851 count: Some(500),
3852 };
3853 let json = serde_json::to_string(&response).unwrap();
3854 assert!(json.contains("BufferLineCount"));
3855 assert!(json.contains("99"));
3856 assert!(json.contains("500"));
3857 }
3858
3859 #[test]
3860 fn test_plugin_command_get_line_end_position() {
3861 let command = PluginCommand::GetLineEndPosition {
3862 buffer_id: BufferId(1),
3863 line: 10,
3864 request_id: 123,
3865 };
3866 let json = serde_json::to_string(&command).unwrap();
3867 assert!(json.contains("GetLineEndPosition"));
3868 assert!(json.contains("10"));
3869 }
3870
3871 #[test]
3872 fn test_plugin_command_get_buffer_line_count() {
3873 let command = PluginCommand::GetBufferLineCount {
3874 buffer_id: BufferId(0),
3875 request_id: 456,
3876 };
3877 let json = serde_json::to_string(&command).unwrap();
3878 assert!(json.contains("GetBufferLineCount"));
3879 assert!(json.contains("456"));
3880 }
3881
3882 #[test]
3883 fn test_plugin_command_scroll_to_line_center() {
3884 let command = PluginCommand::ScrollToLineCenter {
3885 split_id: SplitId(1),
3886 buffer_id: BufferId(2),
3887 line: 50,
3888 };
3889 let json = serde_json::to_string(&command).unwrap();
3890 assert!(json.contains("ScrollToLineCenter"));
3891 assert!(json.contains("50"));
3892 }
3893
3894 #[test]
3897 fn js_callback_id_conversions_and_display() {
3898 for raw in [0u64, 1, 42, u64::MAX] {
3899 let id = JsCallbackId::new(raw);
3900 assert_eq!(id.as_u64(), raw);
3901 assert_eq!(u64::from(id), raw);
3902 assert_eq!(JsCallbackId::from(raw), id);
3903 assert_eq!(id.to_string(), raw.to_string());
3904 }
3905 }
3906
3907 #[test]
3911 fn serde_defaults_fire_when_fields_are_omitted() {
3912 let spec: ActionSpec = serde_json::from_str(r#"{"action": "move_left"}"#).unwrap();
3914 assert_eq!(spec.count, 1);
3915 let spec: ActionSpec =
3916 serde_json::from_str(r#"{"action": "move_left", "count": 5}"#).unwrap();
3917 assert_eq!(spec.count, 5);
3918
3919 let layout: CompositeLayoutConfig =
3921 serde_json::from_str(r#"{"type": "side-by-side"}"#).unwrap();
3922 assert!(layout.show_separator);
3923 let layout: CompositeLayoutConfig =
3924 serde_json::from_str(r#"{"type": "side-by-side", "showSeparator": false}"#).unwrap();
3925 assert!(!layout.show_separator);
3926
3927 let reg: TsCompletionProviderRegistration =
3929 serde_json::from_str(r#"{"id": "p", "displayName": "P"}"#).unwrap();
3930 assert_eq!(reg.priority, 50);
3931 let reg: TsCompletionProviderRegistration =
3932 serde_json::from_str(r#"{"id": "p", "displayName": "P", "priority": 3}"#).unwrap();
3933 assert_eq!(reg.priority, 3);
3934 }
3935
3936 fn mk_cmd(name: &str) -> Command {
3944 Command {
3945 name: name.to_string(),
3946 description: String::new(),
3947 action_name: String::new(),
3948 plugin_name: String::new(),
3949 custom_contexts: Vec::new(),
3950 }
3951 }
3952
3953 #[test]
3960 fn command_registry_register_and_unregister_semantics() {
3961 let r = CommandRegistry::new();
3962
3963 r.register(mk_cmd("a"));
3964 r.register(mk_cmd("b"));
3965 assert_eq!(r.commands.read().unwrap().len(), 2);
3966
3967 r.register(mk_cmd("a"));
3970 let names: Vec<String> = r
3971 .commands
3972 .read()
3973 .unwrap()
3974 .iter()
3975 .map(|c| c.name.clone())
3976 .collect();
3977 assert_eq!(names, vec!["b".to_string(), "a".to_string()]);
3978
3979 r.unregister("a");
3982 let names: Vec<String> = r
3983 .commands
3984 .read()
3985 .unwrap()
3986 .iter()
3987 .map(|c| c.name.clone())
3988 .collect();
3989 assert_eq!(names, vec!["b".to_string()]);
3990
3991 r.unregister("nope");
3993 assert_eq!(r.commands.read().unwrap().len(), 1);
3994 }
3995
3996 #[test]
4002 fn overlay_color_spec_accessors_are_variant_specific() {
4003 let rgb = OverlayColorSpec::rgb(12, 34, 56);
4004 assert_eq!(rgb.as_rgb(), Some((12, 34, 56)));
4005 assert_eq!(rgb.as_theme_key(), None);
4006
4007 let tk = OverlayColorSpec::theme_key("ui.status_bar_bg");
4008 assert_eq!(tk.as_rgb(), None);
4009 assert_eq!(tk.as_theme_key(), Some("ui.status_bar_bg"));
4010 }
4011
4012 #[test]
4015 fn plugin_command_debug_variant_name_returns_real_variant() {
4016 let c = PluginCommand::SetStatus {
4017 message: "hi".into(),
4018 };
4019 assert_eq!(c.debug_variant_name(), "SetStatus");
4020
4021 let c2 = PluginCommand::InsertText {
4022 buffer_id: BufferId(1),
4023 position: 0,
4024 text: String::new(),
4025 };
4026 assert_eq!(c2.debug_variant_name(), "InsertText");
4027 }
4028
4029 fn mk_api() -> (
4037 PluginApi,
4038 std::sync::mpsc::Receiver<PluginCommand>,
4039 Arc<RwLock<HookRegistry>>,
4040 Arc<RwLock<CommandRegistry>>,
4041 Arc<RwLock<EditorStateSnapshot>>,
4042 ) {
4043 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
4044 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
4045 let (tx, rx) = std::sync::mpsc::channel();
4046 let snap = Arc::new(RwLock::new(EditorStateSnapshot::new()));
4047 let api = PluginApi::new(hooks.clone(), commands.clone(), tx, snap.clone());
4048 (api, rx, hooks, commands, snap)
4049 }
4050
4051 #[test]
4054 fn plugin_api_unregister_hooks_clears_registry() {
4055 let (api, _rx, hooks, _cmds, _snap) = mk_api();
4056 api.register_hook("h", Box::new(|_| true));
4057 assert_eq!(hooks.read().unwrap().hook_count("h"), 1);
4058 api.unregister_hooks("h");
4059 assert_eq!(hooks.read().unwrap().hook_count("h"), 0);
4060 }
4061
4062 #[test]
4065 fn plugin_api_register_and_unregister_command_write_through() {
4066 let (api, _rx, _hooks, cmds, _snap) = mk_api();
4067
4068 api.register_command(mk_cmd("x"));
4069 assert_eq!(cmds.read().unwrap().commands.read().unwrap().len(), 1);
4070
4071 api.unregister_command("x");
4072 assert_eq!(cmds.read().unwrap().commands.read().unwrap().len(), 0);
4073 }
4074
4075 macro_rules! assert_dispatches {
4079 ($call:expr, $pattern:pat $(if $guard:expr)?) => {{
4080 let (api, rx, _h, _c, _s) = mk_api();
4081 let _ = $call(&api);
4082 match rx.try_recv().expect("no command sent") {
4083 $pattern $(if $guard)? => {}
4084 other => panic!("unexpected command variant: {:?}", other),
4085 }
4086 }};
4087 }
4088
4089 #[test]
4093 fn plugin_api_send_command_methods_dispatch_correctly() {
4094 assert_dispatches!(
4096 |a: &PluginApi| a.delete_range(BufferId(7), 3..9),
4097 PluginCommand::DeleteRange { buffer_id, range }
4098 if buffer_id == BufferId(7) && range == (3..9)
4099 );
4100
4101 assert_dispatches!(
4103 |a: &PluginApi| a.remove_overlay(BufferId(2), "h-1".into()),
4104 PluginCommand::RemoveOverlay { buffer_id, handle }
4105 if buffer_id == BufferId(2) && handle.as_str() == "h-1"
4106 );
4107
4108 assert_dispatches!(
4110 |a: &PluginApi| a.clear_namespace(BufferId(3), "diag".into()),
4111 PluginCommand::ClearNamespace { buffer_id, namespace }
4112 if buffer_id == BufferId(3) && namespace.as_str() == "diag"
4113 );
4114
4115 assert_dispatches!(
4117 |a: &PluginApi| a.clear_overlays_in_range(BufferId(4), 10, 20),
4118 PluginCommand::ClearOverlaysInRange { buffer_id, start, end }
4119 if buffer_id == BufferId(4) && start == 10 && end == 20
4120 );
4121
4122 assert_dispatches!(
4124 |a: &PluginApi| a.open_file_at_location(
4125 PathBuf::from("/tmp/x.rs"), Some(4), Some(8)
4126 ),
4127 PluginCommand::OpenFileAtLocation { path, line, column }
4128 if path == PathBuf::from("/tmp/x.rs")
4129 && line == Some(4)
4130 && column == Some(8)
4131 );
4132
4133 assert_dispatches!(
4135 |a: &PluginApi| a.open_file_in_split(
4136 2, PathBuf::from("/tmp/y.rs"), Some(5), None
4137 ),
4138 PluginCommand::OpenFileInSplit { split_id, path, line, column }
4139 if split_id == 2
4140 && path == PathBuf::from("/tmp/y.rs")
4141 && line == Some(5)
4142 && column.is_none()
4143 );
4144
4145 assert_dispatches!(
4147 |a: &PluginApi| a.start_prompt("label".into(), "cmd".into()),
4148 PluginCommand::StartPrompt { label, prompt_type }
4149 if label == "label" && prompt_type == "cmd"
4150 );
4151
4152 assert_dispatches!(
4154 |a: &PluginApi| a.set_prompt_suggestions(vec![
4155 Suggestion::new("one".into()),
4156 Suggestion::new("two".into()),
4157 ]),
4158 PluginCommand::SetPromptSuggestions { suggestions }
4159 if suggestions.len() == 2
4160 && suggestions[0].text == "one"
4161 && suggestions[1].text == "two"
4162 );
4163
4164 assert_dispatches!(
4166 |a: &PluginApi| a.set_prompt_input_sync(true),
4167 PluginCommand::SetPromptInputSync { sync } if sync
4168 );
4169 assert_dispatches!(
4170 |a: &PluginApi| a.set_prompt_input_sync(false),
4171 PluginCommand::SetPromptInputSync { sync } if !sync
4172 );
4173
4174 assert_dispatches!(
4176 |a: &PluginApi| a.add_menu_item(
4177 "File".into(),
4178 MenuItem::Label { info: "info".into() },
4179 MenuPosition::Bottom,
4180 ),
4181 PluginCommand::AddMenuItem { menu_label, item, position }
4182 if menu_label == "File"
4183 && matches!(item, MenuItem::Label { ref info } if info == "info")
4184 && matches!(position, MenuPosition::Bottom)
4185 );
4186
4187 assert_dispatches!(
4189 |a: &PluginApi| a.add_menu(
4190 Menu {
4191 id: None,
4192 label: "Help".into(),
4193 items: vec![],
4194 when: None,
4195 },
4196 MenuPosition::After("Edit".into()),
4197 ),
4198 PluginCommand::AddMenu { menu, position }
4199 if menu.label == "Help"
4200 && matches!(position, MenuPosition::After(ref s) if s == "Edit")
4201 );
4202
4203 assert_dispatches!(
4205 |a: &PluginApi| a.remove_menu_item("File".into(), "Open".into()),
4206 PluginCommand::RemoveMenuItem { menu_label, item_label }
4207 if menu_label == "File" && item_label == "Open"
4208 );
4209
4210 assert_dispatches!(
4212 |a: &PluginApi| a.remove_menu("File".into()),
4213 PluginCommand::RemoveMenu { menu_label } if menu_label == "File"
4214 );
4215
4216 assert_dispatches!(
4218 |a: &PluginApi| a.create_virtual_buffer("buf".into(), "mode".into(), true),
4219 PluginCommand::CreateVirtualBuffer { name, mode, read_only }
4220 if name == "buf" && mode == "mode" && read_only
4221 );
4222
4223 assert_dispatches!(
4225 |a: &PluginApi| a.create_virtual_buffer_with_content(
4226 "n".into(), "m".into(), false, vec![]
4227 ),
4228 PluginCommand::CreateVirtualBufferWithContent {
4229 name, mode, read_only, show_line_numbers, show_cursors,
4230 editing_disabled, hidden_from_tabs, request_id, ..
4231 }
4232 if name == "n" && mode == "m" && !read_only
4233 && show_line_numbers && show_cursors
4234 && !editing_disabled && !hidden_from_tabs
4235 && request_id.is_none()
4236 );
4237
4238 assert_dispatches!(
4240 |a: &PluginApi| a.set_virtual_buffer_content(BufferId(9), vec![]),
4241 PluginCommand::SetVirtualBufferContent { buffer_id, entries }
4242 if buffer_id == BufferId(9) && entries.is_empty()
4243 );
4244
4245 assert_dispatches!(
4247 |a: &PluginApi| a.get_text_properties_at_cursor(BufferId(11)),
4248 PluginCommand::GetTextPropertiesAtCursor { buffer_id }
4249 if buffer_id == BufferId(11)
4250 );
4251
4252 assert_dispatches!(
4254 |a: &PluginApi| a.define_mode(
4255 "m".into(),
4256 vec![("j".into(), "move_down".into())],
4257 true,
4258 false,
4259 ),
4260 PluginCommand::DefineMode {
4261 name, bindings, read_only, allow_text_input, inherit_normal_bindings, plugin_name
4262 }
4263 if name == "m"
4264 && bindings.len() == 1
4265 && bindings[0].0 == "j"
4266 && bindings[0].1 == "move_down"
4267 && read_only
4268 && !allow_text_input
4269 && !inherit_normal_bindings
4270 && plugin_name.is_none()
4271 );
4272
4273 assert_dispatches!(
4275 |a: &PluginApi| a.show_buffer(BufferId(77)),
4276 PluginCommand::ShowBuffer { buffer_id } if buffer_id == BufferId(77)
4277 );
4278
4279 assert_dispatches!(
4281 |a: &PluginApi| a.set_split_scroll(5, 128),
4282 PluginCommand::SetSplitScroll { split_id, top_byte }
4283 if split_id == SplitId(5) && top_byte == 128
4284 );
4285
4286 assert_dispatches!(
4288 |a: &PluginApi| a.get_highlights(BufferId(1), 0..10, 7),
4289 PluginCommand::RequestHighlights { buffer_id, range, request_id }
4290 if buffer_id == BufferId(1) && range == (0..10) && request_id == 7
4291 );
4292 }
4293
4294 #[test]
4297 fn plugin_api_get_active_split_id_reads_snapshot() {
4298 let (api, _rx, _h, _c, snap) = mk_api();
4299 snap.write().unwrap().active_split_id = 42;
4300 assert_eq!(api.get_active_split_id(), 42);
4301 }
4302
4303 #[test]
4307 fn plugin_api_state_snapshot_handle_shares_underlying_arc() {
4308 let (api, _rx, _h, _c, snap) = mk_api();
4309 snap.write().unwrap().active_buffer_id = BufferId(42);
4310
4311 let h = api.state_snapshot_handle();
4312 assert_eq!(h.read().unwrap().active_buffer_id, BufferId(42));
4313 assert!(Arc::ptr_eq(&h, &snap));
4314 }
4315
4316 #[test]
4323 fn plugin_command_kill_host_process_serde_round_trip() {
4324 let cmd = PluginCommand::KillHostProcess { process_id: 1234 };
4325 let json = serde_json::to_value(&cmd).unwrap();
4326 assert_eq!(json["KillHostProcess"]["process_id"], 1234);
4327 let decoded: PluginCommand = serde_json::from_value(json).unwrap();
4328 match decoded {
4329 PluginCommand::KillHostProcess { process_id } => assert_eq!(process_id, 1234),
4330 other => panic!("expected KillHostProcess, got {:?}", other),
4331 }
4332 }
4333}