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}
371
372fn serialize_path<S: serde::Serializer>(path: &Option<PathBuf>, s: S) -> Result<S::Ok, S::Error> {
373 s.serialize_str(
374 &path
375 .as_ref()
376 .map(|p| p.to_string_lossy().to_string())
377 .unwrap_or_default(),
378 )
379}
380
381fn serialize_ranges_as_tuples<S>(ranges: &[Range<usize>], serializer: S) -> Result<S::Ok, S::Error>
383where
384 S: serde::Serializer,
385{
386 use serde::ser::SerializeSeq;
387 let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
388 for range in ranges {
389 seq.serialize_element(&(range.start, range.end))?;
390 }
391 seq.end()
392}
393
394#[derive(Debug, Clone, Serialize, Deserialize, TS)]
396#[ts(export)]
397pub struct BufferSavedDiff {
398 pub equal: bool,
399 #[serde(serialize_with = "serialize_ranges_as_tuples")]
400 #[ts(type = "Array<[number, number]>")]
401 pub byte_ranges: Vec<Range<usize>>,
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize, TS)]
406#[serde(rename_all = "camelCase")]
407#[ts(export, rename_all = "camelCase")]
408pub struct ViewportInfo {
409 pub top_byte: usize,
411 pub top_line: Option<usize>,
413 pub left_column: usize,
415 pub width: u16,
417 pub height: u16,
419}
420
421#[derive(Debug, Clone, Serialize, Deserialize, TS)]
423#[serde(rename_all = "camelCase")]
424#[ts(export, rename_all = "camelCase")]
425pub struct LayoutHints {
426 #[ts(optional)]
428 pub compose_width: Option<u16>,
429 #[ts(optional)]
431 pub column_guides: Option<Vec<u16>>,
432}
433
434#[derive(Debug, Clone, Serialize, Deserialize, TS)]
449#[serde(untagged)]
450#[ts(export)]
451pub enum OverlayColorSpec {
452 #[ts(type = "[number, number, number]")]
454 Rgb(u8, u8, u8),
455 ThemeKey(String),
457}
458
459impl OverlayColorSpec {
460 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
462 Self::Rgb(r, g, b)
463 }
464
465 pub fn theme_key(key: impl Into<String>) -> Self {
467 Self::ThemeKey(key.into())
468 }
469
470 pub fn as_rgb(&self) -> Option<(u8, u8, u8)> {
472 match self {
473 Self::Rgb(r, g, b) => Some((*r, *g, *b)),
474 Self::ThemeKey(_) => None,
475 }
476 }
477
478 pub fn as_theme_key(&self) -> Option<&str> {
480 match self {
481 Self::ThemeKey(key) => Some(key),
482 Self::Rgb(_, _, _) => None,
483 }
484 }
485}
486
487#[derive(Debug, Clone, Serialize, Deserialize, TS)]
492#[serde(deny_unknown_fields, rename_all = "camelCase")]
493#[ts(export, rename_all = "camelCase")]
494#[derive(Default)]
495pub struct OverlayOptions {
496 #[serde(default, skip_serializing_if = "Option::is_none")]
498 pub fg: Option<OverlayColorSpec>,
499
500 #[serde(default, skip_serializing_if = "Option::is_none")]
502 pub bg: Option<OverlayColorSpec>,
503
504 #[serde(default)]
506 pub underline: bool,
507
508 #[serde(default)]
510 pub bold: bool,
511
512 #[serde(default)]
514 pub italic: bool,
515
516 #[serde(default)]
518 pub strikethrough: bool,
519
520 #[serde(default)]
522 pub extend_to_line_end: bool,
523
524 #[serde(default, skip_serializing_if = "Option::is_none")]
528 pub url: Option<String>,
529}
530
531#[derive(Debug, Clone, Serialize, Deserialize, TS)]
537#[serde(deny_unknown_fields)]
538#[ts(export, rename = "TsCompositeLayoutConfig")]
539pub struct CompositeLayoutConfig {
540 #[serde(rename = "type")]
542 #[ts(rename = "type")]
543 pub layout_type: String,
544 #[serde(default)]
546 #[ts(optional)]
547 pub ratios: Option<Vec<f32>>,
548 #[serde(default = "default_true", rename = "showSeparator")]
550 #[ts(rename = "showSeparator")]
551 pub show_separator: bool,
552 #[serde(default)]
554 #[ts(optional)]
555 pub spacing: Option<u16>,
556}
557
558fn default_true() -> bool {
559 true
560}
561
562#[derive(Debug, Clone, Serialize, Deserialize, TS)]
564#[serde(deny_unknown_fields)]
565#[ts(export, rename = "TsCompositeSourceConfig")]
566pub struct CompositeSourceConfig {
567 #[serde(rename = "bufferId")]
569 #[ts(rename = "bufferId")]
570 pub buffer_id: usize,
571 pub label: String,
573 #[serde(default)]
575 pub editable: bool,
576 #[serde(default)]
578 pub style: Option<CompositePaneStyle>,
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
583#[serde(deny_unknown_fields)]
584#[ts(export, rename = "TsCompositePaneStyle")]
585pub struct CompositePaneStyle {
586 #[serde(default, rename = "addBg")]
589 #[ts(optional, rename = "addBg", type = "[number, number, number]")]
590 pub add_bg: Option<[u8; 3]>,
591 #[serde(default, rename = "removeBg")]
593 #[ts(optional, rename = "removeBg", type = "[number, number, number]")]
594 pub remove_bg: Option<[u8; 3]>,
595 #[serde(default, rename = "modifyBg")]
597 #[ts(optional, rename = "modifyBg", type = "[number, number, number]")]
598 pub modify_bg: Option<[u8; 3]>,
599 #[serde(default, rename = "gutterStyle")]
601 #[ts(optional, rename = "gutterStyle")]
602 pub gutter_style: Option<String>,
603}
604
605#[derive(Debug, Clone, Serialize, Deserialize, TS)]
607#[serde(deny_unknown_fields)]
608#[ts(export, rename = "TsCompositeHunk")]
609pub struct CompositeHunk {
610 #[serde(rename = "oldStart")]
612 #[ts(rename = "oldStart")]
613 pub old_start: usize,
614 #[serde(rename = "oldCount")]
616 #[ts(rename = "oldCount")]
617 pub old_count: usize,
618 #[serde(rename = "newStart")]
620 #[ts(rename = "newStart")]
621 pub new_start: usize,
622 #[serde(rename = "newCount")]
624 #[ts(rename = "newCount")]
625 pub new_count: usize,
626}
627
628#[derive(Debug, Clone, Serialize, Deserialize, TS)]
630#[serde(deny_unknown_fields)]
631#[ts(export, rename = "TsCreateCompositeBufferOptions")]
632pub struct CreateCompositeBufferOptions {
633 #[serde(default)]
635 pub name: String,
636 #[serde(default)]
638 pub mode: String,
639 pub layout: CompositeLayoutConfig,
641 pub sources: Vec<CompositeSourceConfig>,
643 #[serde(default)]
645 pub hunks: Option<Vec<CompositeHunk>>,
646 #[serde(default, rename = "initialFocusHunk")]
650 #[ts(optional, rename = "initialFocusHunk")]
651 pub initial_focus_hunk: Option<usize>,
652}
653
654#[derive(Debug, Clone, Serialize, Deserialize, TS)]
656#[ts(export)]
657pub enum ViewTokenWireKind {
658 Text(String),
659 Newline,
660 Space,
661 Break,
664 BinaryByte(u8),
668}
669
670#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
676#[serde(deny_unknown_fields)]
677#[ts(export)]
678pub struct ViewTokenStyle {
679 #[serde(default)]
681 #[ts(type = "[number, number, number] | null")]
682 pub fg: Option<(u8, u8, u8)>,
683 #[serde(default)]
685 #[ts(type = "[number, number, number] | null")]
686 pub bg: Option<(u8, u8, u8)>,
687 #[serde(default)]
689 pub bold: bool,
690 #[serde(default)]
692 pub italic: bool,
693}
694
695#[derive(Debug, Clone, Serialize, Deserialize, TS)]
697#[serde(deny_unknown_fields)]
698#[ts(export)]
699pub struct ViewTokenWire {
700 #[ts(type = "number | null")]
702 pub source_offset: Option<usize>,
703 pub kind: ViewTokenWireKind,
705 #[serde(default)]
707 #[ts(optional)]
708 pub style: Option<ViewTokenStyle>,
709}
710
711#[derive(Debug, Clone, Serialize, Deserialize, TS)]
713#[ts(export)]
714pub struct ViewTransformPayload {
715 pub range: Range<usize>,
717 pub tokens: Vec<ViewTokenWire>,
719 pub layout_hints: Option<LayoutHints>,
721}
722
723#[derive(Debug, Clone, Serialize, Deserialize, TS)]
726#[ts(export)]
727pub struct EditorStateSnapshot {
728 pub active_buffer_id: BufferId,
730 pub active_split_id: usize,
732 pub buffers: HashMap<BufferId, BufferInfo>,
734 pub buffer_saved_diffs: HashMap<BufferId, BufferSavedDiff>,
736 pub primary_cursor: Option<CursorInfo>,
738 pub all_cursors: Vec<CursorInfo>,
740 pub viewport: Option<ViewportInfo>,
742 pub buffer_cursor_positions: HashMap<BufferId, usize>,
744 pub buffer_text_properties: HashMap<BufferId, Vec<TextProperty>>,
746 pub selected_text: Option<String>,
749 pub clipboard: String,
751 pub working_dir: PathBuf,
753 #[serde(skip)]
766 #[ts(type = "any")]
767 pub diagnostics: Arc<HashMap<String, Vec<lsp_types::Diagnostic>>>,
768 #[serde(skip)]
773 #[ts(type = "any")]
774 pub folding_ranges: Arc<HashMap<String, Vec<lsp_types::FoldingRange>>>,
775 #[serde(skip)]
785 #[ts(type = "any")]
786 pub config: Arc<serde_json::Value>,
787 #[serde(skip)]
792 #[ts(type = "any")]
793 pub user_config: Arc<serde_json::Value>,
794 #[ts(type = "GrammarInfo[]")]
796 pub available_grammars: Vec<GrammarInfoSnapshot>,
797 pub editor_mode: Option<String>,
800
801 #[ts(type = "any")]
805 pub plugin_view_states: HashMap<BufferId, HashMap<String, serde_json::Value>>,
806
807 #[serde(skip)]
810 #[ts(skip)]
811 pub plugin_view_states_split: usize,
812
813 #[serde(skip)]
816 #[ts(skip)]
817 pub keybinding_labels: HashMap<String, String>,
818
819 #[ts(type = "any")]
826 pub plugin_global_states: HashMap<String, HashMap<String, serde_json::Value>>,
827}
828
829impl EditorStateSnapshot {
830 pub fn new() -> Self {
831 Self {
832 active_buffer_id: BufferId(0),
833 active_split_id: 0,
834 buffers: HashMap::new(),
835 buffer_saved_diffs: HashMap::new(),
836 primary_cursor: None,
837 all_cursors: Vec::new(),
838 viewport: None,
839 buffer_cursor_positions: HashMap::new(),
840 buffer_text_properties: HashMap::new(),
841 selected_text: None,
842 clipboard: String::new(),
843 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
844 diagnostics: Arc::new(HashMap::new()),
845 folding_ranges: Arc::new(HashMap::new()),
846 config: Arc::new(serde_json::Value::Null),
847 user_config: Arc::new(serde_json::Value::Null),
848 available_grammars: Vec::new(),
849 editor_mode: None,
850 plugin_view_states: HashMap::new(),
851 plugin_view_states_split: 0,
852 keybinding_labels: HashMap::new(),
853 plugin_global_states: HashMap::new(),
854 }
855 }
856}
857
858impl Default for EditorStateSnapshot {
859 fn default() -> Self {
860 Self::new()
861 }
862}
863
864#[derive(Debug, Clone, Serialize, Deserialize, TS)]
866#[ts(export)]
867pub struct GrammarInfoSnapshot {
868 pub name: String,
870 pub source: String,
872 pub file_extensions: Vec<String>,
874 pub short_name: Option<String>,
876}
877
878#[derive(Debug, Clone, Serialize, Deserialize, TS)]
880#[ts(export)]
881pub enum MenuPosition {
882 Top,
884 Bottom,
886 Before(String),
888 After(String),
890}
891
892#[derive(Debug, Clone, Serialize, Deserialize, TS)]
894#[ts(export)]
895pub enum PluginCommand {
896 InsertText {
898 buffer_id: BufferId,
899 position: usize,
900 text: String,
901 },
902
903 DeleteRange {
905 buffer_id: BufferId,
906 range: Range<usize>,
907 },
908
909 AddOverlay {
914 buffer_id: BufferId,
915 namespace: Option<OverlayNamespace>,
916 range: Range<usize>,
917 options: OverlayOptions,
919 },
920
921 RemoveOverlay {
923 buffer_id: BufferId,
924 handle: OverlayHandle,
925 },
926
927 SetStatus { message: String },
929
930 ApplyTheme { theme_name: String },
932
933 ReloadConfig,
936
937 RegisterCommand { command: Command },
939
940 UnregisterCommand { name: String },
942
943 OpenFileInBackground { path: PathBuf },
945
946 InsertAtCursor { text: String },
948
949 SpawnProcess {
951 command: String,
952 args: Vec<String>,
953 cwd: Option<String>,
954 callback_id: JsCallbackId,
955 },
956
957 Delay {
959 callback_id: JsCallbackId,
960 duration_ms: u64,
961 },
962
963 SpawnBackgroundProcess {
967 process_id: u64,
969 command: String,
971 args: Vec<String>,
973 cwd: Option<String>,
975 callback_id: JsCallbackId,
977 },
978
979 KillBackgroundProcess { process_id: u64 },
981
982 SpawnProcessWait {
985 process_id: u64,
987 callback_id: JsCallbackId,
989 },
990
991 SetLayoutHints {
993 buffer_id: BufferId,
994 split_id: Option<SplitId>,
995 range: Range<usize>,
996 hints: LayoutHints,
997 },
998
999 SetLineNumbers { buffer_id: BufferId, enabled: bool },
1001
1002 SetViewMode { buffer_id: BufferId, mode: String },
1004
1005 SetLineWrap {
1007 buffer_id: BufferId,
1008 split_id: Option<SplitId>,
1009 enabled: bool,
1010 },
1011
1012 SubmitViewTransform {
1014 buffer_id: BufferId,
1015 split_id: Option<SplitId>,
1016 payload: ViewTransformPayload,
1017 },
1018
1019 ClearViewTransform {
1021 buffer_id: BufferId,
1022 split_id: Option<SplitId>,
1023 },
1024
1025 SetViewState {
1028 buffer_id: BufferId,
1029 key: String,
1030 #[ts(type = "any")]
1031 value: Option<serde_json::Value>,
1032 },
1033
1034 SetGlobalState {
1038 plugin_name: String,
1039 key: String,
1040 #[ts(type = "any")]
1041 value: Option<serde_json::Value>,
1042 },
1043
1044 ClearAllOverlays { buffer_id: BufferId },
1046
1047 ClearNamespace {
1049 buffer_id: BufferId,
1050 namespace: OverlayNamespace,
1051 },
1052
1053 ClearOverlaysInRange {
1056 buffer_id: BufferId,
1057 start: usize,
1058 end: usize,
1059 },
1060
1061 AddVirtualText {
1064 buffer_id: BufferId,
1065 virtual_text_id: String,
1066 position: usize,
1067 text: String,
1068 color: (u8, u8, u8),
1069 use_bg: bool, before: bool, },
1072
1073 RemoveVirtualText {
1075 buffer_id: BufferId,
1076 virtual_text_id: String,
1077 },
1078
1079 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
1081
1082 ClearVirtualTexts { buffer_id: BufferId },
1084
1085 AddVirtualLine {
1089 buffer_id: BufferId,
1090 position: usize,
1092 text: String,
1094 fg_color: Option<OverlayColorSpec>,
1098 bg_color: Option<OverlayColorSpec>,
1101 above: bool,
1103 namespace: String,
1105 priority: i32,
1107 },
1108
1109 ClearVirtualTextNamespace {
1112 buffer_id: BufferId,
1113 namespace: String,
1114 },
1115
1116 AddConceal {
1119 buffer_id: BufferId,
1120 namespace: OverlayNamespace,
1122 start: usize,
1124 end: usize,
1125 replacement: Option<String>,
1127 },
1128
1129 ClearConcealNamespace {
1131 buffer_id: BufferId,
1132 namespace: OverlayNamespace,
1133 },
1134
1135 ClearConcealsInRange {
1138 buffer_id: BufferId,
1139 start: usize,
1140 end: usize,
1141 },
1142
1143 AddFold {
1149 buffer_id: BufferId,
1150 start: usize,
1151 end: usize,
1152 placeholder: Option<String>,
1155 },
1156
1157 ClearFolds { buffer_id: BufferId },
1159
1160 AddSoftBreak {
1164 buffer_id: BufferId,
1165 namespace: OverlayNamespace,
1167 position: usize,
1169 indent: u16,
1171 },
1172
1173 ClearSoftBreakNamespace {
1175 buffer_id: BufferId,
1176 namespace: OverlayNamespace,
1177 },
1178
1179 ClearSoftBreaksInRange {
1181 buffer_id: BufferId,
1182 start: usize,
1183 end: usize,
1184 },
1185
1186 RefreshLines { buffer_id: BufferId },
1188
1189 RefreshAllLines,
1193
1194 HookCompleted { hook_name: String },
1198
1199 SetLineIndicator {
1202 buffer_id: BufferId,
1203 line: usize,
1205 namespace: String,
1207 symbol: String,
1209 color: (u8, u8, u8),
1211 priority: i32,
1213 },
1214
1215 SetLineIndicators {
1218 buffer_id: BufferId,
1219 lines: Vec<usize>,
1221 namespace: String,
1223 symbol: String,
1225 color: (u8, u8, u8),
1227 priority: i32,
1229 },
1230
1231 ClearLineIndicators {
1233 buffer_id: BufferId,
1234 namespace: String,
1236 },
1237
1238 SetFileExplorerDecorations {
1240 namespace: String,
1242 decorations: Vec<FileExplorerDecoration>,
1244 },
1245
1246 ClearFileExplorerDecorations {
1248 namespace: String,
1250 },
1251
1252 OpenFileAtLocation {
1255 path: PathBuf,
1256 line: Option<usize>, column: Option<usize>, },
1259
1260 OpenFileInSplit {
1263 split_id: usize,
1264 path: PathBuf,
1265 line: Option<usize>, column: Option<usize>, },
1268
1269 StartPrompt {
1272 label: String,
1273 prompt_type: String, },
1275
1276 StartPromptWithInitial {
1278 label: String,
1279 prompt_type: String,
1280 initial_value: String,
1281 },
1282
1283 StartPromptAsync {
1286 label: String,
1287 initial_value: String,
1288 callback_id: JsCallbackId,
1289 },
1290
1291 SetPromptSuggestions { suggestions: Vec<Suggestion> },
1294
1295 SetPromptInputSync { sync: bool },
1297
1298 AddMenuItem {
1301 menu_label: String,
1302 item: MenuItem,
1303 position: MenuPosition,
1304 },
1305
1306 AddMenu { menu: Menu, position: MenuPosition },
1308
1309 RemoveMenuItem {
1311 menu_label: String,
1312 item_label: String,
1313 },
1314
1315 RemoveMenu { menu_label: String },
1317
1318 CreateVirtualBuffer {
1320 name: String,
1322 mode: String,
1324 read_only: bool,
1326 },
1327
1328 CreateVirtualBufferWithContent {
1332 name: String,
1334 mode: String,
1336 read_only: bool,
1338 entries: Vec<TextPropertyEntry>,
1340 show_line_numbers: bool,
1342 show_cursors: bool,
1344 editing_disabled: bool,
1346 hidden_from_tabs: bool,
1348 request_id: Option<u64>,
1350 },
1351
1352 CreateVirtualBufferInSplit {
1355 name: String,
1357 mode: String,
1359 read_only: bool,
1361 entries: Vec<TextPropertyEntry>,
1363 ratio: f32,
1365 direction: Option<String>,
1367 panel_id: Option<String>,
1369 show_line_numbers: bool,
1371 show_cursors: bool,
1373 editing_disabled: bool,
1375 line_wrap: Option<bool>,
1377 before: bool,
1379 request_id: Option<u64>,
1381 },
1382
1383 SetVirtualBufferContent {
1385 buffer_id: BufferId,
1386 entries: Vec<TextPropertyEntry>,
1388 },
1389
1390 GetTextPropertiesAtCursor { buffer_id: BufferId },
1392
1393 CreateBufferGroup {
1396 name: String,
1398 mode: String,
1400 layout_json: String,
1402 request_id: Option<u64>,
1404 },
1405
1406 SetPanelContent {
1408 group_id: usize,
1410 panel_name: String,
1412 entries: Vec<TextPropertyEntry>,
1414 },
1415
1416 CloseBufferGroup { group_id: usize },
1418
1419 FocusPanel { group_id: usize, panel_name: String },
1421
1422 DefineMode {
1424 name: String,
1425 bindings: Vec<(String, String)>, read_only: bool,
1427 allow_text_input: bool,
1429 inherit_normal_bindings: bool,
1432 plugin_name: Option<String>,
1434 },
1435
1436 ShowBuffer { buffer_id: BufferId },
1438
1439 CreateVirtualBufferInExistingSplit {
1441 name: String,
1443 mode: String,
1445 read_only: bool,
1447 entries: Vec<TextPropertyEntry>,
1449 split_id: SplitId,
1451 show_line_numbers: bool,
1453 show_cursors: bool,
1455 editing_disabled: bool,
1457 line_wrap: Option<bool>,
1459 request_id: Option<u64>,
1461 },
1462
1463 CloseBuffer { buffer_id: BufferId },
1465
1466 CreateCompositeBuffer {
1469 name: String,
1471 mode: String,
1473 layout: CompositeLayoutConfig,
1475 sources: Vec<CompositeSourceConfig>,
1477 hunks: Option<Vec<CompositeHunk>>,
1479 initial_focus_hunk: Option<usize>,
1481 request_id: Option<u64>,
1483 },
1484
1485 UpdateCompositeAlignment {
1487 buffer_id: BufferId,
1488 hunks: Vec<CompositeHunk>,
1489 },
1490
1491 CloseCompositeBuffer { buffer_id: BufferId },
1493
1494 FlushLayout,
1501
1502 CompositeNextHunk { buffer_id: BufferId },
1504
1505 CompositePrevHunk { buffer_id: BufferId },
1507
1508 FocusSplit { split_id: SplitId },
1510
1511 SetSplitBuffer {
1513 split_id: SplitId,
1514 buffer_id: BufferId,
1515 },
1516
1517 SetSplitScroll { split_id: SplitId, top_byte: usize },
1519
1520 RequestHighlights {
1522 buffer_id: BufferId,
1523 range: Range<usize>,
1524 request_id: u64,
1525 },
1526
1527 CloseSplit { split_id: SplitId },
1529
1530 SetSplitRatio {
1532 split_id: SplitId,
1533 ratio: f32,
1535 },
1536
1537 SetSplitLabel { split_id: SplitId, label: String },
1539
1540 ClearSplitLabel { split_id: SplitId },
1542
1543 GetSplitByLabel { label: String, request_id: u64 },
1545
1546 DistributeSplitsEvenly {
1548 split_ids: Vec<SplitId>,
1550 },
1551
1552 SetBufferCursor {
1554 buffer_id: BufferId,
1555 position: usize,
1557 },
1558
1559 SetBufferShowCursors { buffer_id: BufferId, show: bool },
1567
1568 SendLspRequest {
1570 language: String,
1571 method: String,
1572 #[ts(type = "any")]
1573 params: Option<JsonValue>,
1574 request_id: u64,
1575 },
1576
1577 SetClipboard { text: String },
1579
1580 DeleteSelection,
1583
1584 SetContext {
1588 name: String,
1590 active: bool,
1592 },
1593
1594 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1596
1597 ExecuteAction {
1600 action_name: String,
1602 },
1603
1604 ExecuteActions {
1608 actions: Vec<ActionSpec>,
1610 },
1611
1612 GetBufferText {
1614 buffer_id: BufferId,
1616 start: usize,
1618 end: usize,
1620 request_id: u64,
1622 },
1623
1624 GetLineStartPosition {
1627 buffer_id: BufferId,
1629 line: u32,
1631 request_id: u64,
1633 },
1634
1635 GetLineEndPosition {
1639 buffer_id: BufferId,
1641 line: u32,
1643 request_id: u64,
1645 },
1646
1647 GetBufferLineCount {
1649 buffer_id: BufferId,
1651 request_id: u64,
1653 },
1654
1655 ScrollToLineCenter {
1658 split_id: SplitId,
1660 buffer_id: BufferId,
1662 line: usize,
1664 },
1665
1666 ScrollBufferToLine {
1672 buffer_id: BufferId,
1674 line: usize,
1676 },
1677
1678 SetEditorMode {
1681 mode: Option<String>,
1683 },
1684
1685 ShowActionPopup {
1688 popup_id: String,
1690 title: String,
1692 message: String,
1694 actions: Vec<ActionPopupAction>,
1696 },
1697
1698 DisableLspForLanguage {
1700 language: String,
1702 },
1703
1704 RestartLspForLanguage {
1706 language: String,
1708 },
1709
1710 SetLspRootUri {
1714 language: String,
1716 uri: String,
1718 },
1719
1720 CreateScrollSyncGroup {
1724 group_id: u32,
1726 left_split: SplitId,
1728 right_split: SplitId,
1730 },
1731
1732 SetScrollSyncAnchors {
1735 group_id: u32,
1737 anchors: Vec<(usize, usize)>,
1739 },
1740
1741 RemoveScrollSyncGroup {
1743 group_id: u32,
1745 },
1746
1747 SaveBufferToPath {
1750 buffer_id: BufferId,
1752 path: PathBuf,
1754 },
1755
1756 LoadPlugin {
1759 path: PathBuf,
1761 callback_id: JsCallbackId,
1763 },
1764
1765 UnloadPlugin {
1768 name: String,
1770 callback_id: JsCallbackId,
1772 },
1773
1774 ReloadPlugin {
1777 name: String,
1779 callback_id: JsCallbackId,
1781 },
1782
1783 ListPlugins {
1786 callback_id: JsCallbackId,
1788 },
1789
1790 ReloadThemes { apply_theme: Option<String> },
1794
1795 RegisterGrammar {
1798 language: String,
1800 grammar_path: String,
1802 extensions: Vec<String>,
1804 },
1805
1806 RegisterLanguageConfig {
1809 language: String,
1811 config: LanguagePackConfig,
1813 },
1814
1815 RegisterLspServer {
1818 language: String,
1820 config: LspServerPackConfig,
1822 },
1823
1824 ReloadGrammars { callback_id: JsCallbackId },
1828
1829 CreateTerminal {
1833 cwd: Option<String>,
1835 direction: Option<String>,
1837 ratio: Option<f32>,
1839 focus: Option<bool>,
1841 request_id: u64,
1843 },
1844
1845 SendTerminalInput {
1847 terminal_id: TerminalId,
1849 data: String,
1851 },
1852
1853 CloseTerminal {
1855 terminal_id: TerminalId,
1857 },
1858
1859 GrepProject {
1863 pattern: String,
1865 fixed_string: bool,
1867 case_sensitive: bool,
1869 max_results: usize,
1871 whole_words: bool,
1873 callback_id: JsCallbackId,
1875 },
1876
1877 GrepProjectStreaming {
1882 pattern: String,
1884 fixed_string: bool,
1886 case_sensitive: bool,
1888 max_results: usize,
1890 whole_words: bool,
1892 search_id: u64,
1894 callback_id: JsCallbackId,
1896 },
1897
1898 ReplaceInBuffer {
1902 file_path: PathBuf,
1904 matches: Vec<(usize, usize)>,
1906 replacement: String,
1908 callback_id: JsCallbackId,
1910 },
1911}
1912
1913impl PluginCommand {
1914 pub fn debug_variant_name(&self) -> String {
1916 let dbg = format!("{:?}", self);
1917 dbg.split([' ', '{', '(']).next().unwrap_or("?").to_string()
1918 }
1919}
1920
1921#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
1930#[serde(rename_all = "camelCase")]
1931#[ts(export)]
1932pub struct LanguagePackConfig {
1933 #[serde(default)]
1935 pub comment_prefix: Option<String>,
1936
1937 #[serde(default)]
1939 pub block_comment_start: Option<String>,
1940
1941 #[serde(default)]
1943 pub block_comment_end: Option<String>,
1944
1945 #[serde(default)]
1947 pub use_tabs: Option<bool>,
1948
1949 #[serde(default)]
1951 pub tab_size: Option<usize>,
1952
1953 #[serde(default)]
1955 pub auto_indent: Option<bool>,
1956
1957 #[serde(default)]
1960 pub show_whitespace_tabs: Option<bool>,
1961
1962 #[serde(default)]
1964 pub formatter: Option<FormatterPackConfig>,
1965}
1966
1967#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1969#[serde(rename_all = "camelCase")]
1970#[ts(export)]
1971pub struct FormatterPackConfig {
1972 pub command: String,
1974
1975 #[serde(default)]
1977 pub args: Vec<String>,
1978}
1979
1980#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1982#[serde(rename_all = "camelCase")]
1983#[ts(export)]
1984pub struct ProcessLimitsPackConfig {
1985 #[serde(default)]
1987 pub max_memory_percent: Option<u32>,
1988
1989 #[serde(default)]
1991 pub max_cpu_percent: Option<u32>,
1992
1993 #[serde(default)]
1995 pub enabled: Option<bool>,
1996}
1997
1998#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2000#[serde(rename_all = "camelCase")]
2001#[ts(export)]
2002pub struct LspServerPackConfig {
2003 pub command: String,
2005
2006 #[serde(default)]
2008 pub args: Vec<String>,
2009
2010 #[serde(default)]
2012 pub auto_start: Option<bool>,
2013
2014 #[serde(default)]
2016 #[ts(type = "Record<string, unknown> | null")]
2017 pub initialization_options: Option<JsonValue>,
2018
2019 #[serde(default)]
2021 pub process_limits: Option<ProcessLimitsPackConfig>,
2022}
2023
2024#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
2026#[ts(export)]
2027pub enum HunkStatus {
2028 Pending,
2029 Staged,
2030 Discarded,
2031}
2032
2033#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2035#[ts(export)]
2036pub struct ReviewHunk {
2037 pub id: String,
2038 pub file: String,
2039 pub context_header: String,
2040 pub status: HunkStatus,
2041 pub base_range: Option<(usize, usize)>,
2043 pub modified_range: Option<(usize, usize)>,
2045}
2046
2047#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2049#[serde(deny_unknown_fields)]
2050#[ts(export, rename = "TsActionPopupAction")]
2051pub struct ActionPopupAction {
2052 pub id: String,
2054 pub label: String,
2056}
2057
2058#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2060#[serde(deny_unknown_fields)]
2061#[ts(export)]
2062pub struct ActionPopupOptions {
2063 pub id: String,
2065 pub title: String,
2067 pub message: String,
2069 pub actions: Vec<ActionPopupAction>,
2071}
2072
2073#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2075#[ts(export)]
2076pub struct TsHighlightSpan {
2077 pub start: u32,
2078 pub end: u32,
2079 #[ts(type = "[number, number, number]")]
2080 pub color: (u8, u8, u8),
2081 pub bold: bool,
2082 pub italic: bool,
2083}
2084
2085#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2087#[ts(export)]
2088pub struct SpawnResult {
2089 pub stdout: String,
2091 pub stderr: String,
2093 pub exit_code: i32,
2095}
2096
2097#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2099#[ts(export)]
2100pub struct BackgroundProcessResult {
2101 #[ts(type = "number")]
2103 pub process_id: u64,
2104 pub exit_code: i32,
2107}
2108
2109#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2111#[serde(rename_all = "camelCase")]
2112#[ts(export, rename_all = "camelCase")]
2113pub struct GrepMatch {
2114 pub file: String,
2116 #[ts(type = "number")]
2118 pub buffer_id: usize,
2119 #[ts(type = "number")]
2121 pub byte_offset: usize,
2122 #[ts(type = "number")]
2124 pub length: usize,
2125 #[ts(type = "number")]
2127 pub line: usize,
2128 #[ts(type = "number")]
2130 pub column: usize,
2131 pub context: String,
2133}
2134
2135#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2137#[serde(rename_all = "camelCase")]
2138#[ts(export, rename_all = "camelCase")]
2139pub struct ReplaceResult {
2140 #[ts(type = "number")]
2142 pub replacements: usize,
2143 #[ts(type = "number")]
2145 pub buffer_id: usize,
2146}
2147
2148#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2150#[serde(deny_unknown_fields, rename_all = "camelCase")]
2151#[ts(export, rename = "TextPropertyEntry", rename_all = "camelCase")]
2152pub struct JsTextPropertyEntry {
2153 pub text: String,
2155 #[serde(default)]
2157 #[ts(optional, type = "Record<string, unknown>")]
2158 pub properties: Option<HashMap<String, JsonValue>>,
2159 #[serde(default)]
2161 #[ts(optional, type = "Partial<OverlayOptions>")]
2162 pub style: Option<OverlayOptions>,
2163 #[serde(default)]
2165 #[ts(optional)]
2166 pub inline_overlays: Option<Vec<crate::text_property::InlineOverlay>>,
2167}
2168
2169#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2171#[ts(export)]
2172pub struct DirEntry {
2173 pub name: String,
2175 pub is_file: bool,
2177 pub is_dir: bool,
2179}
2180
2181#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2183#[ts(export)]
2184pub struct JsPosition {
2185 pub line: u32,
2187 pub character: u32,
2189}
2190
2191#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2193#[ts(export)]
2194pub struct JsRange {
2195 pub start: JsPosition,
2197 pub end: JsPosition,
2199}
2200
2201#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2203#[ts(export)]
2204pub struct JsDiagnostic {
2205 pub uri: String,
2207 pub message: String,
2209 pub severity: Option<u8>,
2211 pub range: JsRange,
2213 #[ts(optional)]
2215 pub source: Option<String>,
2216}
2217
2218#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2220#[serde(deny_unknown_fields)]
2221#[ts(export)]
2222pub struct CreateVirtualBufferOptions {
2223 pub name: String,
2225 #[serde(default)]
2227 #[ts(optional)]
2228 pub mode: Option<String>,
2229 #[serde(default, rename = "readOnly")]
2231 #[ts(optional, rename = "readOnly")]
2232 pub read_only: Option<bool>,
2233 #[serde(default, rename = "showLineNumbers")]
2235 #[ts(optional, rename = "showLineNumbers")]
2236 pub show_line_numbers: Option<bool>,
2237 #[serde(default, rename = "showCursors")]
2239 #[ts(optional, rename = "showCursors")]
2240 pub show_cursors: Option<bool>,
2241 #[serde(default, rename = "editingDisabled")]
2243 #[ts(optional, rename = "editingDisabled")]
2244 pub editing_disabled: Option<bool>,
2245 #[serde(default, rename = "hiddenFromTabs")]
2247 #[ts(optional, rename = "hiddenFromTabs")]
2248 pub hidden_from_tabs: Option<bool>,
2249 #[serde(default)]
2251 #[ts(optional)]
2252 pub entries: Option<Vec<JsTextPropertyEntry>>,
2253}
2254
2255#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2257#[serde(deny_unknown_fields)]
2258#[ts(export)]
2259pub struct CreateVirtualBufferInSplitOptions {
2260 pub name: String,
2262 #[serde(default)]
2264 #[ts(optional)]
2265 pub mode: Option<String>,
2266 #[serde(default, rename = "readOnly")]
2268 #[ts(optional, rename = "readOnly")]
2269 pub read_only: Option<bool>,
2270 #[serde(default)]
2272 #[ts(optional)]
2273 pub ratio: Option<f32>,
2274 #[serde(default)]
2276 #[ts(optional)]
2277 pub direction: Option<String>,
2278 #[serde(default, rename = "panelId")]
2280 #[ts(optional, rename = "panelId")]
2281 pub panel_id: Option<String>,
2282 #[serde(default, rename = "showLineNumbers")]
2284 #[ts(optional, rename = "showLineNumbers")]
2285 pub show_line_numbers: Option<bool>,
2286 #[serde(default, rename = "showCursors")]
2288 #[ts(optional, rename = "showCursors")]
2289 pub show_cursors: Option<bool>,
2290 #[serde(default, rename = "editingDisabled")]
2292 #[ts(optional, rename = "editingDisabled")]
2293 pub editing_disabled: Option<bool>,
2294 #[serde(default, rename = "lineWrap")]
2296 #[ts(optional, rename = "lineWrap")]
2297 pub line_wrap: Option<bool>,
2298 #[serde(default)]
2300 #[ts(optional)]
2301 pub before: Option<bool>,
2302 #[serde(default)]
2304 #[ts(optional)]
2305 pub entries: Option<Vec<JsTextPropertyEntry>>,
2306}
2307
2308#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2310#[serde(deny_unknown_fields)]
2311#[ts(export)]
2312pub struct CreateVirtualBufferInExistingSplitOptions {
2313 pub name: String,
2315 #[serde(rename = "splitId")]
2317 #[ts(rename = "splitId")]
2318 pub split_id: usize,
2319 #[serde(default)]
2321 #[ts(optional)]
2322 pub mode: Option<String>,
2323 #[serde(default, rename = "readOnly")]
2325 #[ts(optional, rename = "readOnly")]
2326 pub read_only: Option<bool>,
2327 #[serde(default, rename = "showLineNumbers")]
2329 #[ts(optional, rename = "showLineNumbers")]
2330 pub show_line_numbers: Option<bool>,
2331 #[serde(default, rename = "showCursors")]
2333 #[ts(optional, rename = "showCursors")]
2334 pub show_cursors: Option<bool>,
2335 #[serde(default, rename = "editingDisabled")]
2337 #[ts(optional, rename = "editingDisabled")]
2338 pub editing_disabled: Option<bool>,
2339 #[serde(default, rename = "lineWrap")]
2341 #[ts(optional, rename = "lineWrap")]
2342 pub line_wrap: Option<bool>,
2343 #[serde(default)]
2345 #[ts(optional)]
2346 pub entries: Option<Vec<JsTextPropertyEntry>>,
2347}
2348
2349#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2351#[serde(deny_unknown_fields)]
2352#[ts(export)]
2353pub struct CreateTerminalOptions {
2354 #[serde(default)]
2356 #[ts(optional)]
2357 pub cwd: Option<String>,
2358 #[serde(default)]
2360 #[ts(optional)]
2361 pub direction: Option<String>,
2362 #[serde(default)]
2364 #[ts(optional)]
2365 pub ratio: Option<f32>,
2366 #[serde(default)]
2368 #[ts(optional)]
2369 pub focus: Option<bool>,
2370}
2371
2372#[derive(Debug, Clone, Serialize, TS)]
2377#[ts(export, type = "Array<Record<string, unknown>>")]
2378pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
2379
2380#[cfg(feature = "plugins")]
2382mod fromjs_impls {
2383 use super::*;
2384 use rquickjs::{Ctx, FromJs, Value};
2385
2386 impl<'js> FromJs<'js> for JsTextPropertyEntry {
2387 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2388 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2389 from: "object",
2390 to: "JsTextPropertyEntry",
2391 message: Some(e.to_string()),
2392 })
2393 }
2394 }
2395
2396 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
2397 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2398 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2399 from: "object",
2400 to: "CreateVirtualBufferOptions",
2401 message: Some(e.to_string()),
2402 })
2403 }
2404 }
2405
2406 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
2407 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2408 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2409 from: "object",
2410 to: "CreateVirtualBufferInSplitOptions",
2411 message: Some(e.to_string()),
2412 })
2413 }
2414 }
2415
2416 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
2417 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2418 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2419 from: "object",
2420 to: "CreateVirtualBufferInExistingSplitOptions",
2421 message: Some(e.to_string()),
2422 })
2423 }
2424 }
2425
2426 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
2427 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
2428 rquickjs_serde::to_value(ctx.clone(), &self.0)
2429 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
2430 }
2431 }
2432
2433 impl<'js> FromJs<'js> for ActionSpec {
2436 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2437 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2438 from: "object",
2439 to: "ActionSpec",
2440 message: Some(e.to_string()),
2441 })
2442 }
2443 }
2444
2445 impl<'js> FromJs<'js> for ActionPopupAction {
2446 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2447 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2448 from: "object",
2449 to: "ActionPopupAction",
2450 message: Some(e.to_string()),
2451 })
2452 }
2453 }
2454
2455 impl<'js> FromJs<'js> for ActionPopupOptions {
2456 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2457 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2458 from: "object",
2459 to: "ActionPopupOptions",
2460 message: Some(e.to_string()),
2461 })
2462 }
2463 }
2464
2465 impl<'js> FromJs<'js> for ViewTokenWire {
2466 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2467 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2468 from: "object",
2469 to: "ViewTokenWire",
2470 message: Some(e.to_string()),
2471 })
2472 }
2473 }
2474
2475 impl<'js> FromJs<'js> for ViewTokenStyle {
2476 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2477 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2478 from: "object",
2479 to: "ViewTokenStyle",
2480 message: Some(e.to_string()),
2481 })
2482 }
2483 }
2484
2485 impl<'js> FromJs<'js> for LayoutHints {
2486 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2487 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2488 from: "object",
2489 to: "LayoutHints",
2490 message: Some(e.to_string()),
2491 })
2492 }
2493 }
2494
2495 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
2496 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2497 let json: serde_json::Value =
2499 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2500 from: "object",
2501 to: "CreateCompositeBufferOptions (json)",
2502 message: Some(e.to_string()),
2503 })?;
2504 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
2505 from: "json",
2506 to: "CreateCompositeBufferOptions",
2507 message: Some(e.to_string()),
2508 })
2509 }
2510 }
2511
2512 impl<'js> FromJs<'js> for CompositeHunk {
2513 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2514 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2515 from: "object",
2516 to: "CompositeHunk",
2517 message: Some(e.to_string()),
2518 })
2519 }
2520 }
2521
2522 impl<'js> FromJs<'js> for LanguagePackConfig {
2523 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2524 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2525 from: "object",
2526 to: "LanguagePackConfig",
2527 message: Some(e.to_string()),
2528 })
2529 }
2530 }
2531
2532 impl<'js> FromJs<'js> for LspServerPackConfig {
2533 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2534 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2535 from: "object",
2536 to: "LspServerPackConfig",
2537 message: Some(e.to_string()),
2538 })
2539 }
2540 }
2541
2542 impl<'js> FromJs<'js> for ProcessLimitsPackConfig {
2543 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2544 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2545 from: "object",
2546 to: "ProcessLimitsPackConfig",
2547 message: Some(e.to_string()),
2548 })
2549 }
2550 }
2551
2552 impl<'js> FromJs<'js> for CreateTerminalOptions {
2553 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2554 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2555 from: "object",
2556 to: "CreateTerminalOptions",
2557 message: Some(e.to_string()),
2558 })
2559 }
2560 }
2561
2562 #[cfg(test)]
2574 mod tests {
2575 use super::*;
2576 use rquickjs::{Context, Runtime};
2577
2578 fn with_js<R>(f: impl for<'js> FnOnce(Ctx<'js>) -> R) -> R {
2581 let rt = Runtime::new().expect("create rquickjs runtime");
2582 let ctx = Context::full(&rt).expect("create rquickjs context");
2583 ctx.with(f)
2584 }
2585
2586 fn eval_as<T>(src: &str) -> T
2588 where
2589 for<'js> T: rquickjs::FromJs<'js>,
2590 {
2591 with_js(|ctx| {
2592 let value: Value = ctx
2593 .eval::<Value, _>(src.as_bytes())
2594 .expect("eval JS source");
2595 T::from_js(&ctx, value).expect("from_js decode")
2596 })
2597 }
2598
2599 #[test]
2600 fn js_text_property_entry_decodes_text_and_properties() {
2601 let got: JsTextPropertyEntry =
2602 eval_as("({text: 'hello', properties: {file: '/x.rs'}})");
2603 assert_eq!(got.text, "hello");
2604 let props = got.properties.expect("properties present");
2605 assert_eq!(props.get("file").and_then(|v| v.as_str()), Some("/x.rs"));
2606 }
2607
2608 #[test]
2609 fn create_virtual_buffer_options_decodes_name() {
2610 let got: CreateVirtualBufferOptions = eval_as("({name: 'logs', readOnly: true})");
2611 assert_eq!(got.name, "logs");
2612 assert_eq!(got.read_only, Some(true));
2613 }
2614
2615 #[test]
2616 fn create_virtual_buffer_in_split_options_decodes_ratio() {
2617 let got: CreateVirtualBufferInSplitOptions =
2618 eval_as("({name: 'diag', ratio: 0.25, direction: 'horizontal'})");
2619 assert_eq!(got.name, "diag");
2620 assert!(matches!(got.ratio, Some(r) if (r - 0.25).abs() < 1e-6));
2621 assert_eq!(got.direction.as_deref(), Some("horizontal"));
2622 }
2623
2624 #[test]
2625 fn create_virtual_buffer_in_existing_split_options_decodes_splitid() {
2626 let got: CreateVirtualBufferInExistingSplitOptions =
2627 eval_as("({name: 'n', splitId: 7})");
2628 assert_eq!(got.name, "n");
2629 assert_eq!(got.split_id, 7);
2630 }
2631
2632 #[test]
2633 fn create_terminal_options_decodes_cwd_and_focus() {
2634 let got: CreateTerminalOptions =
2635 eval_as("({cwd: '/tmp', direction: 'vertical', focus: false})");
2636 assert_eq!(got.cwd.as_deref(), Some("/tmp"));
2637 assert_eq!(got.direction.as_deref(), Some("vertical"));
2638 assert_eq!(got.focus, Some(false));
2639 }
2640
2641 #[test]
2642 fn action_spec_decodes_action_and_count() {
2643 let got: ActionSpec = eval_as("({action: 'move_word_right', count: 5})");
2644 assert_eq!(got.action, "move_word_right");
2645 assert_eq!(got.count, 5);
2646 }
2647
2648 #[test]
2649 fn action_popup_action_decodes_id_and_label() {
2650 let got: ActionPopupAction = eval_as("({id: 'ok', label: 'OK'})");
2651 assert_eq!(got.id, "ok");
2652 assert_eq!(got.label, "OK");
2653 }
2654
2655 #[test]
2656 fn action_popup_options_decodes_actions_list() {
2657 let got: ActionPopupOptions = eval_as(
2658 "({id: 'p', title: 't', message: 'm', \
2659 actions: [{id: 'ok', label: 'OK'}]})",
2660 );
2661 assert_eq!(got.id, "p");
2662 assert_eq!(got.title, "t");
2663 assert_eq!(got.message, "m");
2664 assert_eq!(got.actions.len(), 1);
2665 assert_eq!(got.actions[0].id, "ok");
2666 }
2667
2668 #[test]
2669 fn view_token_wire_decodes_offset_and_kind() {
2670 let got: ViewTokenWire = eval_as("({source_offset: 42, kind: 'Newline'})");
2674 assert_eq!(got.source_offset, Some(42));
2675 assert!(matches!(got.kind, ViewTokenWireKind::Newline));
2676 }
2677
2678 #[test]
2679 fn view_token_style_decodes_boolean_flags() {
2680 let got: ViewTokenStyle = eval_as("({bold: true, italic: true})");
2684 assert!(got.bold);
2685 assert!(got.italic);
2686 assert!(got.fg.is_none());
2687 }
2688
2689 #[test]
2690 fn layout_hints_decodes_compose_width() {
2691 let got: LayoutHints = eval_as("({composeWidth: 120})");
2692 assert_eq!(got.compose_width, Some(120));
2693 assert!(got.column_guides.is_none());
2694 }
2695
2696 #[test]
2697 fn create_composite_buffer_options_decodes_name_and_sources() {
2698 let got: CreateCompositeBufferOptions = eval_as(
2699 "({name: 'diff', mode: 'm', \
2700 layout: {type: 'side-by-side', ratios: [0.5, 0.5], showSeparator: true}, \
2701 sources: [{bufferId: 3, label: 'OLD'}]})",
2702 );
2703 assert_eq!(got.name, "diff");
2704 assert_eq!(got.layout.layout_type, "side-by-side");
2705 assert_eq!(got.sources.len(), 1);
2706 assert_eq!(got.sources[0].buffer_id, 3);
2707 assert_eq!(got.sources[0].label, "OLD");
2708 }
2709
2710 #[test]
2711 fn composite_hunk_decodes_all_fields() {
2712 let got: CompositeHunk =
2713 eval_as("({oldStart: 1, oldCount: 2, newStart: 3, newCount: 4})");
2714 assert_eq!(got.old_start, 1);
2715 assert_eq!(got.old_count, 2);
2716 assert_eq!(got.new_start, 3);
2717 assert_eq!(got.new_count, 4);
2718 }
2719
2720 #[test]
2721 fn language_pack_config_decodes_comment_prefix_and_tab_size() {
2722 let got: LanguagePackConfig =
2723 eval_as("({commentPrefix: '//', tabSize: 7, useTabs: true})");
2724 assert_eq!(got.comment_prefix.as_deref(), Some("//"));
2725 assert_eq!(got.tab_size, Some(7));
2726 assert_eq!(got.use_tabs, Some(true));
2727 }
2728
2729 #[test]
2730 fn lsp_server_pack_config_decodes_command_and_args() {
2731 let got: LspServerPackConfig =
2732 eval_as("({command: 'rust-analyzer', args: ['--log'], autoStart: true})");
2733 assert_eq!(got.command, "rust-analyzer");
2734 assert_eq!(got.args, vec!["--log".to_string()]);
2735 assert_eq!(got.auto_start, Some(true));
2736 }
2737
2738 #[test]
2739 fn process_limits_pack_config_decodes_percentages() {
2740 let got: ProcessLimitsPackConfig =
2741 eval_as("({maxMemoryPercent: 75, maxCpuPercent: 50, enabled: true})");
2742 assert_eq!(got.max_memory_percent, Some(75));
2743 assert_eq!(got.max_cpu_percent, Some(50));
2744 assert_eq!(got.enabled, Some(true));
2745 }
2746
2747 #[test]
2752 fn text_properties_at_cursor_into_js_preserves_length() {
2753 use rquickjs::IntoJs;
2754 with_js(|ctx| {
2755 let mut entry = std::collections::HashMap::new();
2756 entry.insert("k".to_string(), serde_json::json!("v"));
2757 let payload = TextPropertiesAtCursor(vec![entry.clone(), entry]);
2758
2759 let v = payload.into_js(&ctx).expect("into_js");
2760 let arr = v.as_array().expect("expected JS array");
2761 assert_eq!(arr.len(), 2);
2762 });
2763 }
2764 }
2765}
2766
2767pub struct PluginApi {
2769 hooks: Arc<RwLock<HookRegistry>>,
2771
2772 commands: Arc<RwLock<CommandRegistry>>,
2774
2775 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2777
2778 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2780}
2781
2782impl PluginApi {
2783 pub fn new(
2785 hooks: Arc<RwLock<HookRegistry>>,
2786 commands: Arc<RwLock<CommandRegistry>>,
2787 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2788 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2789 ) -> Self {
2790 Self {
2791 hooks,
2792 commands,
2793 command_sender,
2794 state_snapshot,
2795 }
2796 }
2797
2798 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
2800 let mut hooks = self.hooks.write().unwrap();
2801 hooks.add_hook(hook_name, callback);
2802 }
2803
2804 pub fn unregister_hooks(&self, hook_name: &str) {
2806 let mut hooks = self.hooks.write().unwrap();
2807 hooks.remove_hooks(hook_name);
2808 }
2809
2810 pub fn register_command(&self, command: Command) {
2812 let commands = self.commands.read().unwrap();
2813 commands.register(command);
2814 }
2815
2816 pub fn unregister_command(&self, name: &str) {
2818 let commands = self.commands.read().unwrap();
2819 commands.unregister(name);
2820 }
2821
2822 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
2824 self.command_sender
2825 .send(command)
2826 .map_err(|e| format!("Failed to send command: {}", e))
2827 }
2828
2829 pub fn insert_text(
2831 &self,
2832 buffer_id: BufferId,
2833 position: usize,
2834 text: String,
2835 ) -> Result<(), String> {
2836 self.send_command(PluginCommand::InsertText {
2837 buffer_id,
2838 position,
2839 text,
2840 })
2841 }
2842
2843 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
2845 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
2846 }
2847
2848 pub fn add_overlay(
2856 &self,
2857 buffer_id: BufferId,
2858 namespace: Option<String>,
2859 range: Range<usize>,
2860 options: OverlayOptions,
2861 ) -> Result<(), String> {
2862 self.send_command(PluginCommand::AddOverlay {
2863 buffer_id,
2864 namespace: namespace.map(OverlayNamespace::from_string),
2865 range,
2866 options,
2867 })
2868 }
2869
2870 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
2872 self.send_command(PluginCommand::RemoveOverlay {
2873 buffer_id,
2874 handle: OverlayHandle::from_string(handle),
2875 })
2876 }
2877
2878 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
2880 self.send_command(PluginCommand::ClearNamespace {
2881 buffer_id,
2882 namespace: OverlayNamespace::from_string(namespace),
2883 })
2884 }
2885
2886 pub fn clear_overlays_in_range(
2889 &self,
2890 buffer_id: BufferId,
2891 start: usize,
2892 end: usize,
2893 ) -> Result<(), String> {
2894 self.send_command(PluginCommand::ClearOverlaysInRange {
2895 buffer_id,
2896 start,
2897 end,
2898 })
2899 }
2900
2901 pub fn set_status(&self, message: String) -> Result<(), String> {
2903 self.send_command(PluginCommand::SetStatus { message })
2904 }
2905
2906 pub fn open_file_at_location(
2909 &self,
2910 path: PathBuf,
2911 line: Option<usize>,
2912 column: Option<usize>,
2913 ) -> Result<(), String> {
2914 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
2915 }
2916
2917 pub fn open_file_in_split(
2922 &self,
2923 split_id: usize,
2924 path: PathBuf,
2925 line: Option<usize>,
2926 column: Option<usize>,
2927 ) -> Result<(), String> {
2928 self.send_command(PluginCommand::OpenFileInSplit {
2929 split_id,
2930 path,
2931 line,
2932 column,
2933 })
2934 }
2935
2936 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
2939 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
2940 }
2941
2942 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
2945 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
2946 }
2947
2948 pub fn set_prompt_input_sync(&self, sync: bool) -> Result<(), String> {
2950 self.send_command(PluginCommand::SetPromptInputSync { sync })
2951 }
2952
2953 pub fn add_menu_item(
2955 &self,
2956 menu_label: String,
2957 item: MenuItem,
2958 position: MenuPosition,
2959 ) -> Result<(), String> {
2960 self.send_command(PluginCommand::AddMenuItem {
2961 menu_label,
2962 item,
2963 position,
2964 })
2965 }
2966
2967 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
2969 self.send_command(PluginCommand::AddMenu { menu, position })
2970 }
2971
2972 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
2974 self.send_command(PluginCommand::RemoveMenuItem {
2975 menu_label,
2976 item_label,
2977 })
2978 }
2979
2980 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
2982 self.send_command(PluginCommand::RemoveMenu { menu_label })
2983 }
2984
2985 pub fn create_virtual_buffer(
2992 &self,
2993 name: String,
2994 mode: String,
2995 read_only: bool,
2996 ) -> Result<(), String> {
2997 self.send_command(PluginCommand::CreateVirtualBuffer {
2998 name,
2999 mode,
3000 read_only,
3001 })
3002 }
3003
3004 pub fn create_virtual_buffer_with_content(
3010 &self,
3011 name: String,
3012 mode: String,
3013 read_only: bool,
3014 entries: Vec<TextPropertyEntry>,
3015 ) -> Result<(), String> {
3016 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
3017 name,
3018 mode,
3019 read_only,
3020 entries,
3021 show_line_numbers: true,
3022 show_cursors: true,
3023 editing_disabled: false,
3024 hidden_from_tabs: false,
3025 request_id: None,
3026 })
3027 }
3028
3029 pub fn set_virtual_buffer_content(
3033 &self,
3034 buffer_id: BufferId,
3035 entries: Vec<TextPropertyEntry>,
3036 ) -> Result<(), String> {
3037 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
3038 }
3039
3040 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
3044 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
3045 }
3046
3047 pub fn define_mode(
3051 &self,
3052 name: String,
3053 bindings: Vec<(String, String)>,
3054 read_only: bool,
3055 allow_text_input: bool,
3056 ) -> Result<(), String> {
3057 self.send_command(PluginCommand::DefineMode {
3058 name,
3059 bindings,
3060 read_only,
3061 allow_text_input,
3062 inherit_normal_bindings: false,
3063 plugin_name: None,
3064 })
3065 }
3066
3067 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
3069 self.send_command(PluginCommand::ShowBuffer { buffer_id })
3070 }
3071
3072 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
3074 self.send_command(PluginCommand::SetSplitScroll {
3075 split_id: SplitId(split_id),
3076 top_byte,
3077 })
3078 }
3079
3080 pub fn get_highlights(
3082 &self,
3083 buffer_id: BufferId,
3084 range: Range<usize>,
3085 request_id: u64,
3086 ) -> Result<(), String> {
3087 self.send_command(PluginCommand::RequestHighlights {
3088 buffer_id,
3089 range,
3090 request_id,
3091 })
3092 }
3093
3094 pub fn get_active_buffer_id(&self) -> BufferId {
3098 let snapshot = self.state_snapshot.read().unwrap();
3099 snapshot.active_buffer_id
3100 }
3101
3102 pub fn get_active_split_id(&self) -> usize {
3104 let snapshot = self.state_snapshot.read().unwrap();
3105 snapshot.active_split_id
3106 }
3107
3108 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
3110 let snapshot = self.state_snapshot.read().unwrap();
3111 snapshot.buffers.get(&buffer_id).cloned()
3112 }
3113
3114 pub fn list_buffers(&self) -> Vec<BufferInfo> {
3116 let snapshot = self.state_snapshot.read().unwrap();
3117 snapshot.buffers.values().cloned().collect()
3118 }
3119
3120 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
3122 let snapshot = self.state_snapshot.read().unwrap();
3123 snapshot.primary_cursor.clone()
3124 }
3125
3126 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
3128 let snapshot = self.state_snapshot.read().unwrap();
3129 snapshot.all_cursors.clone()
3130 }
3131
3132 pub fn get_viewport(&self) -> Option<ViewportInfo> {
3134 let snapshot = self.state_snapshot.read().unwrap();
3135 snapshot.viewport.clone()
3136 }
3137
3138 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
3140 Arc::clone(&self.state_snapshot)
3141 }
3142}
3143
3144impl Clone for PluginApi {
3145 fn clone(&self) -> Self {
3146 Self {
3147 hooks: Arc::clone(&self.hooks),
3148 commands: Arc::clone(&self.commands),
3149 command_sender: self.command_sender.clone(),
3150 state_snapshot: Arc::clone(&self.state_snapshot),
3151 }
3152 }
3153}
3154
3155#[derive(Debug, Clone, Serialize, Deserialize, TS)]
3169#[serde(rename_all = "camelCase", deny_unknown_fields)]
3170#[ts(export, rename_all = "camelCase")]
3171pub struct TsCompletionCandidate {
3172 pub label: String,
3174
3175 #[serde(skip_serializing_if = "Option::is_none")]
3177 pub insert_text: Option<String>,
3178
3179 #[serde(skip_serializing_if = "Option::is_none")]
3181 pub detail: Option<String>,
3182
3183 #[serde(skip_serializing_if = "Option::is_none")]
3185 pub icon: Option<String>,
3186
3187 #[serde(default)]
3189 pub score: i64,
3190
3191 #[serde(default)]
3193 pub is_snippet: bool,
3194
3195 #[serde(skip_serializing_if = "Option::is_none")]
3197 pub provider_data: Option<String>,
3198}
3199
3200#[derive(Debug, Clone, Serialize, Deserialize, TS)]
3205#[serde(rename_all = "camelCase")]
3206#[ts(export, rename_all = "camelCase")]
3207pub struct TsCompletionContext {
3208 pub prefix: String,
3210
3211 pub cursor_byte: usize,
3213
3214 pub word_start_byte: usize,
3216
3217 pub buffer_len: usize,
3219
3220 pub is_large_file: bool,
3222
3223 pub text_around_cursor: String,
3226
3227 pub cursor_offset_in_text: usize,
3229
3230 #[serde(skip_serializing_if = "Option::is_none")]
3232 pub language_id: Option<String>,
3233}
3234
3235#[derive(Debug, Clone, Serialize, Deserialize, TS)]
3237#[serde(rename_all = "camelCase", deny_unknown_fields)]
3238#[ts(export, rename_all = "camelCase")]
3239pub struct TsCompletionProviderRegistration {
3240 pub id: String,
3242
3243 pub display_name: String,
3245
3246 #[serde(default = "default_plugin_provider_priority")]
3249 pub priority: u32,
3250
3251 #[serde(default)]
3254 pub language_ids: Vec<String>,
3255}
3256
3257fn default_plugin_provider_priority() -> u32 {
3258 50
3259}
3260
3261#[cfg(test)]
3262mod tests {
3263 use super::*;
3264
3265 #[test]
3266 fn test_plugin_api_creation() {
3267 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3268 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3269 let (tx, _rx) = std::sync::mpsc::channel();
3270 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3271
3272 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3273
3274 let _clone = api.clone();
3276 }
3277
3278 #[test]
3279 fn test_register_hook() {
3280 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3281 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3282 let (tx, _rx) = std::sync::mpsc::channel();
3283 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3284
3285 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
3286
3287 api.register_hook("test-hook", Box::new(|_| true));
3288
3289 let hook_registry = hooks.read().unwrap();
3290 assert_eq!(hook_registry.hook_count("test-hook"), 1);
3291 }
3292
3293 #[test]
3294 fn test_send_command() {
3295 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3296 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3297 let (tx, rx) = std::sync::mpsc::channel();
3298 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3299
3300 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3301
3302 let result = api.insert_text(BufferId(1), 0, "test".to_string());
3303 assert!(result.is_ok());
3304
3305 let received = rx.try_recv();
3307 assert!(received.is_ok());
3308
3309 match received.unwrap() {
3310 PluginCommand::InsertText {
3311 buffer_id,
3312 position,
3313 text,
3314 } => {
3315 assert_eq!(buffer_id.0, 1);
3316 assert_eq!(position, 0);
3317 assert_eq!(text, "test");
3318 }
3319 _ => panic!("Wrong command type"),
3320 }
3321 }
3322
3323 #[test]
3324 fn test_add_overlay_command() {
3325 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3326 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3327 let (tx, rx) = std::sync::mpsc::channel();
3328 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3329
3330 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3331
3332 let result = api.add_overlay(
3333 BufferId(1),
3334 Some("test-overlay".to_string()),
3335 0..10,
3336 OverlayOptions {
3337 fg: Some(OverlayColorSpec::ThemeKey("ui.status_bar_fg".to_string())),
3338 bg: None,
3339 underline: true,
3340 bold: false,
3341 italic: false,
3342 strikethrough: false,
3343 extend_to_line_end: false,
3344 url: None,
3345 },
3346 );
3347 assert!(result.is_ok());
3348
3349 let received = rx.try_recv().unwrap();
3350 match received {
3351 PluginCommand::AddOverlay {
3352 buffer_id,
3353 namespace,
3354 range,
3355 options,
3356 } => {
3357 assert_eq!(buffer_id.0, 1);
3358 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
3359 assert_eq!(range, 0..10);
3360 assert!(matches!(
3361 options.fg,
3362 Some(OverlayColorSpec::ThemeKey(ref k)) if k == "ui.status_bar_fg"
3363 ));
3364 assert!(options.bg.is_none());
3365 assert!(options.underline);
3366 assert!(!options.bold);
3367 assert!(!options.italic);
3368 assert!(!options.extend_to_line_end);
3369 }
3370 _ => panic!("Wrong command type"),
3371 }
3372 }
3373
3374 #[test]
3375 fn test_set_status_command() {
3376 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3377 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3378 let (tx, rx) = std::sync::mpsc::channel();
3379 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3380
3381 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3382
3383 let result = api.set_status("Test status".to_string());
3384 assert!(result.is_ok());
3385
3386 let received = rx.try_recv().unwrap();
3387 match received {
3388 PluginCommand::SetStatus { message } => {
3389 assert_eq!(message, "Test status");
3390 }
3391 _ => panic!("Wrong command type"),
3392 }
3393 }
3394
3395 #[test]
3396 fn test_get_active_buffer_id() {
3397 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3398 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3399 let (tx, _rx) = std::sync::mpsc::channel();
3400 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3401
3402 {
3404 let mut snapshot = state_snapshot.write().unwrap();
3405 snapshot.active_buffer_id = BufferId(5);
3406 }
3407
3408 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3409
3410 let active_id = api.get_active_buffer_id();
3411 assert_eq!(active_id.0, 5);
3412 }
3413
3414 #[test]
3415 fn test_get_buffer_info() {
3416 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3417 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3418 let (tx, _rx) = std::sync::mpsc::channel();
3419 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3420
3421 {
3423 let mut snapshot = state_snapshot.write().unwrap();
3424 let buffer_info = BufferInfo {
3425 id: BufferId(1),
3426 path: Some(std::path::PathBuf::from("/test/file.txt")),
3427 modified: true,
3428 length: 100,
3429 is_virtual: false,
3430 view_mode: "source".to_string(),
3431 is_composing_in_any_split: false,
3432 compose_width: None,
3433 language: "text".to_string(),
3434 is_preview: false,
3435 };
3436 snapshot.buffers.insert(BufferId(1), buffer_info);
3437 }
3438
3439 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3440
3441 let info = api.get_buffer_info(BufferId(1));
3442 assert!(info.is_some());
3443 let info = info.unwrap();
3444 assert_eq!(info.id.0, 1);
3445 assert_eq!(
3446 info.path.as_ref().unwrap().to_str().unwrap(),
3447 "/test/file.txt"
3448 );
3449 assert!(info.modified);
3450 assert_eq!(info.length, 100);
3451
3452 let no_info = api.get_buffer_info(BufferId(999));
3454 assert!(no_info.is_none());
3455 }
3456
3457 #[test]
3458 fn test_list_buffers() {
3459 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3460 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3461 let (tx, _rx) = std::sync::mpsc::channel();
3462 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3463
3464 {
3466 let mut snapshot = state_snapshot.write().unwrap();
3467 snapshot.buffers.insert(
3468 BufferId(1),
3469 BufferInfo {
3470 id: BufferId(1),
3471 path: Some(std::path::PathBuf::from("/file1.txt")),
3472 modified: false,
3473 length: 50,
3474 is_virtual: false,
3475 view_mode: "source".to_string(),
3476 is_composing_in_any_split: false,
3477 compose_width: None,
3478 language: "text".to_string(),
3479 is_preview: false,
3480 },
3481 );
3482 snapshot.buffers.insert(
3483 BufferId(2),
3484 BufferInfo {
3485 id: BufferId(2),
3486 path: Some(std::path::PathBuf::from("/file2.txt")),
3487 modified: true,
3488 length: 100,
3489 is_virtual: false,
3490 view_mode: "source".to_string(),
3491 is_composing_in_any_split: false,
3492 compose_width: None,
3493 language: "text".to_string(),
3494 is_preview: false,
3495 },
3496 );
3497 snapshot.buffers.insert(
3498 BufferId(3),
3499 BufferInfo {
3500 id: BufferId(3),
3501 path: None,
3502 modified: false,
3503 length: 0,
3504 is_virtual: true,
3505 view_mode: "source".to_string(),
3506 is_composing_in_any_split: false,
3507 compose_width: None,
3508 language: "text".to_string(),
3509 is_preview: false,
3510 },
3511 );
3512 }
3513
3514 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3515
3516 let buffers = api.list_buffers();
3517 assert_eq!(buffers.len(), 3);
3518
3519 assert!(buffers.iter().any(|b| b.id.0 == 1));
3521 assert!(buffers.iter().any(|b| b.id.0 == 2));
3522 assert!(buffers.iter().any(|b| b.id.0 == 3));
3523 }
3524
3525 #[test]
3526 fn test_get_primary_cursor() {
3527 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3528 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3529 let (tx, _rx) = std::sync::mpsc::channel();
3530 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3531
3532 {
3534 let mut snapshot = state_snapshot.write().unwrap();
3535 snapshot.primary_cursor = Some(CursorInfo {
3536 position: 42,
3537 selection: Some(10..42),
3538 });
3539 }
3540
3541 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3542
3543 let cursor = api.get_primary_cursor();
3544 assert!(cursor.is_some());
3545 let cursor = cursor.unwrap();
3546 assert_eq!(cursor.position, 42);
3547 assert_eq!(cursor.selection, Some(10..42));
3548 }
3549
3550 #[test]
3551 fn test_get_all_cursors() {
3552 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3553 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3554 let (tx, _rx) = std::sync::mpsc::channel();
3555 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3556
3557 {
3559 let mut snapshot = state_snapshot.write().unwrap();
3560 snapshot.all_cursors = vec![
3561 CursorInfo {
3562 position: 10,
3563 selection: None,
3564 },
3565 CursorInfo {
3566 position: 20,
3567 selection: Some(15..20),
3568 },
3569 CursorInfo {
3570 position: 30,
3571 selection: Some(25..30),
3572 },
3573 ];
3574 }
3575
3576 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3577
3578 let cursors = api.get_all_cursors();
3579 assert_eq!(cursors.len(), 3);
3580 assert_eq!(cursors[0].position, 10);
3581 assert_eq!(cursors[0].selection, None);
3582 assert_eq!(cursors[1].position, 20);
3583 assert_eq!(cursors[1].selection, Some(15..20));
3584 assert_eq!(cursors[2].position, 30);
3585 assert_eq!(cursors[2].selection, Some(25..30));
3586 }
3587
3588 #[test]
3589 fn test_get_viewport() {
3590 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3591 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3592 let (tx, _rx) = std::sync::mpsc::channel();
3593 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3594
3595 {
3597 let mut snapshot = state_snapshot.write().unwrap();
3598 snapshot.viewport = Some(ViewportInfo {
3599 top_byte: 100,
3600 top_line: Some(5),
3601 left_column: 5,
3602 width: 80,
3603 height: 24,
3604 });
3605 }
3606
3607 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3608
3609 let viewport = api.get_viewport();
3610 assert!(viewport.is_some());
3611 let viewport = viewport.unwrap();
3612 assert_eq!(viewport.top_byte, 100);
3613 assert_eq!(viewport.left_column, 5);
3614 assert_eq!(viewport.width, 80);
3615 assert_eq!(viewport.height, 24);
3616 }
3617
3618 #[test]
3619 fn test_composite_buffer_options_rejects_unknown_fields() {
3620 let valid_json = r#"{
3622 "name": "test",
3623 "mode": "diff",
3624 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3625 "sources": [{"bufferId": 1, "label": "old"}]
3626 }"#;
3627 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
3628 assert!(
3629 result.is_ok(),
3630 "Valid JSON should parse: {:?}",
3631 result.err()
3632 );
3633
3634 let invalid_json = r#"{
3636 "name": "test",
3637 "mode": "diff",
3638 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3639 "sources": [{"buffer_id": 1, "label": "old"}]
3640 }"#;
3641 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
3642 assert!(
3643 result.is_err(),
3644 "JSON with unknown field should fail to parse"
3645 );
3646 let err = result.unwrap_err().to_string();
3647 assert!(
3648 err.contains("unknown field") || err.contains("buffer_id"),
3649 "Error should mention unknown field: {}",
3650 err
3651 );
3652 }
3653
3654 #[test]
3655 fn test_composite_hunk_rejects_unknown_fields() {
3656 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3658 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
3659 assert!(
3660 result.is_ok(),
3661 "Valid JSON should parse: {:?}",
3662 result.err()
3663 );
3664
3665 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3667 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
3668 assert!(
3669 result.is_err(),
3670 "JSON with unknown field should fail to parse"
3671 );
3672 let err = result.unwrap_err().to_string();
3673 assert!(
3674 err.contains("unknown field") || err.contains("old_start"),
3675 "Error should mention unknown field: {}",
3676 err
3677 );
3678 }
3679
3680 #[test]
3681 fn test_plugin_response_line_end_position() {
3682 let response = PluginResponse::LineEndPosition {
3683 request_id: 42,
3684 position: Some(100),
3685 };
3686 let json = serde_json::to_string(&response).unwrap();
3687 assert!(json.contains("LineEndPosition"));
3688 assert!(json.contains("42"));
3689 assert!(json.contains("100"));
3690
3691 let response_none = PluginResponse::LineEndPosition {
3693 request_id: 1,
3694 position: None,
3695 };
3696 let json_none = serde_json::to_string(&response_none).unwrap();
3697 assert!(json_none.contains("null"));
3698 }
3699
3700 #[test]
3701 fn test_plugin_response_buffer_line_count() {
3702 let response = PluginResponse::BufferLineCount {
3703 request_id: 99,
3704 count: Some(500),
3705 };
3706 let json = serde_json::to_string(&response).unwrap();
3707 assert!(json.contains("BufferLineCount"));
3708 assert!(json.contains("99"));
3709 assert!(json.contains("500"));
3710 }
3711
3712 #[test]
3713 fn test_plugin_command_get_line_end_position() {
3714 let command = PluginCommand::GetLineEndPosition {
3715 buffer_id: BufferId(1),
3716 line: 10,
3717 request_id: 123,
3718 };
3719 let json = serde_json::to_string(&command).unwrap();
3720 assert!(json.contains("GetLineEndPosition"));
3721 assert!(json.contains("10"));
3722 }
3723
3724 #[test]
3725 fn test_plugin_command_get_buffer_line_count() {
3726 let command = PluginCommand::GetBufferLineCount {
3727 buffer_id: BufferId(0),
3728 request_id: 456,
3729 };
3730 let json = serde_json::to_string(&command).unwrap();
3731 assert!(json.contains("GetBufferLineCount"));
3732 assert!(json.contains("456"));
3733 }
3734
3735 #[test]
3736 fn test_plugin_command_scroll_to_line_center() {
3737 let command = PluginCommand::ScrollToLineCenter {
3738 split_id: SplitId(1),
3739 buffer_id: BufferId(2),
3740 line: 50,
3741 };
3742 let json = serde_json::to_string(&command).unwrap();
3743 assert!(json.contains("ScrollToLineCenter"));
3744 assert!(json.contains("50"));
3745 }
3746
3747 #[test]
3750 fn js_callback_id_conversions_and_display() {
3751 for raw in [0u64, 1, 42, u64::MAX] {
3752 let id = JsCallbackId::new(raw);
3753 assert_eq!(id.as_u64(), raw);
3754 assert_eq!(u64::from(id), raw);
3755 assert_eq!(JsCallbackId::from(raw), id);
3756 assert_eq!(id.to_string(), raw.to_string());
3757 }
3758 }
3759
3760 #[test]
3764 fn serde_defaults_fire_when_fields_are_omitted() {
3765 let spec: ActionSpec = serde_json::from_str(r#"{"action": "move_left"}"#).unwrap();
3767 assert_eq!(spec.count, 1);
3768 let spec: ActionSpec =
3769 serde_json::from_str(r#"{"action": "move_left", "count": 5}"#).unwrap();
3770 assert_eq!(spec.count, 5);
3771
3772 let layout: CompositeLayoutConfig =
3774 serde_json::from_str(r#"{"type": "side-by-side"}"#).unwrap();
3775 assert!(layout.show_separator);
3776 let layout: CompositeLayoutConfig =
3777 serde_json::from_str(r#"{"type": "side-by-side", "showSeparator": false}"#).unwrap();
3778 assert!(!layout.show_separator);
3779
3780 let reg: TsCompletionProviderRegistration =
3782 serde_json::from_str(r#"{"id": "p", "displayName": "P"}"#).unwrap();
3783 assert_eq!(reg.priority, 50);
3784 let reg: TsCompletionProviderRegistration =
3785 serde_json::from_str(r#"{"id": "p", "displayName": "P", "priority": 3}"#).unwrap();
3786 assert_eq!(reg.priority, 3);
3787 }
3788
3789 fn mk_cmd(name: &str) -> Command {
3797 Command {
3798 name: name.to_string(),
3799 description: String::new(),
3800 action_name: String::new(),
3801 plugin_name: String::new(),
3802 custom_contexts: Vec::new(),
3803 }
3804 }
3805
3806 #[test]
3813 fn command_registry_register_and_unregister_semantics() {
3814 let r = CommandRegistry::new();
3815
3816 r.register(mk_cmd("a"));
3817 r.register(mk_cmd("b"));
3818 assert_eq!(r.commands.read().unwrap().len(), 2);
3819
3820 r.register(mk_cmd("a"));
3823 let names: Vec<String> = r
3824 .commands
3825 .read()
3826 .unwrap()
3827 .iter()
3828 .map(|c| c.name.clone())
3829 .collect();
3830 assert_eq!(names, vec!["b".to_string(), "a".to_string()]);
3831
3832 r.unregister("a");
3835 let names: Vec<String> = r
3836 .commands
3837 .read()
3838 .unwrap()
3839 .iter()
3840 .map(|c| c.name.clone())
3841 .collect();
3842 assert_eq!(names, vec!["b".to_string()]);
3843
3844 r.unregister("nope");
3846 assert_eq!(r.commands.read().unwrap().len(), 1);
3847 }
3848
3849 #[test]
3855 fn overlay_color_spec_accessors_are_variant_specific() {
3856 let rgb = OverlayColorSpec::rgb(12, 34, 56);
3857 assert_eq!(rgb.as_rgb(), Some((12, 34, 56)));
3858 assert_eq!(rgb.as_theme_key(), None);
3859
3860 let tk = OverlayColorSpec::theme_key("ui.status_bar_bg");
3861 assert_eq!(tk.as_rgb(), None);
3862 assert_eq!(tk.as_theme_key(), Some("ui.status_bar_bg"));
3863 }
3864
3865 #[test]
3868 fn plugin_command_debug_variant_name_returns_real_variant() {
3869 let c = PluginCommand::SetStatus {
3870 message: "hi".into(),
3871 };
3872 assert_eq!(c.debug_variant_name(), "SetStatus");
3873
3874 let c2 = PluginCommand::InsertText {
3875 buffer_id: BufferId(1),
3876 position: 0,
3877 text: String::new(),
3878 };
3879 assert_eq!(c2.debug_variant_name(), "InsertText");
3880 }
3881
3882 fn mk_api() -> (
3890 PluginApi,
3891 std::sync::mpsc::Receiver<PluginCommand>,
3892 Arc<RwLock<HookRegistry>>,
3893 Arc<RwLock<CommandRegistry>>,
3894 Arc<RwLock<EditorStateSnapshot>>,
3895 ) {
3896 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3897 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3898 let (tx, rx) = std::sync::mpsc::channel();
3899 let snap = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3900 let api = PluginApi::new(hooks.clone(), commands.clone(), tx, snap.clone());
3901 (api, rx, hooks, commands, snap)
3902 }
3903
3904 #[test]
3907 fn plugin_api_unregister_hooks_clears_registry() {
3908 let (api, _rx, hooks, _cmds, _snap) = mk_api();
3909 api.register_hook("h", Box::new(|_| true));
3910 assert_eq!(hooks.read().unwrap().hook_count("h"), 1);
3911 api.unregister_hooks("h");
3912 assert_eq!(hooks.read().unwrap().hook_count("h"), 0);
3913 }
3914
3915 #[test]
3918 fn plugin_api_register_and_unregister_command_write_through() {
3919 let (api, _rx, _hooks, cmds, _snap) = mk_api();
3920
3921 api.register_command(mk_cmd("x"));
3922 assert_eq!(cmds.read().unwrap().commands.read().unwrap().len(), 1);
3923
3924 api.unregister_command("x");
3925 assert_eq!(cmds.read().unwrap().commands.read().unwrap().len(), 0);
3926 }
3927
3928 macro_rules! assert_dispatches {
3932 ($call:expr, $pattern:pat $(if $guard:expr)?) => {{
3933 let (api, rx, _h, _c, _s) = mk_api();
3934 let _ = $call(&api);
3935 match rx.try_recv().expect("no command sent") {
3936 $pattern $(if $guard)? => {}
3937 other => panic!("unexpected command variant: {:?}", other),
3938 }
3939 }};
3940 }
3941
3942 #[test]
3946 fn plugin_api_send_command_methods_dispatch_correctly() {
3947 assert_dispatches!(
3949 |a: &PluginApi| a.delete_range(BufferId(7), 3..9),
3950 PluginCommand::DeleteRange { buffer_id, range }
3951 if buffer_id == BufferId(7) && range == (3..9)
3952 );
3953
3954 assert_dispatches!(
3956 |a: &PluginApi| a.remove_overlay(BufferId(2), "h-1".into()),
3957 PluginCommand::RemoveOverlay { buffer_id, handle }
3958 if buffer_id == BufferId(2) && handle.as_str() == "h-1"
3959 );
3960
3961 assert_dispatches!(
3963 |a: &PluginApi| a.clear_namespace(BufferId(3), "diag".into()),
3964 PluginCommand::ClearNamespace { buffer_id, namespace }
3965 if buffer_id == BufferId(3) && namespace.as_str() == "diag"
3966 );
3967
3968 assert_dispatches!(
3970 |a: &PluginApi| a.clear_overlays_in_range(BufferId(4), 10, 20),
3971 PluginCommand::ClearOverlaysInRange { buffer_id, start, end }
3972 if buffer_id == BufferId(4) && start == 10 && end == 20
3973 );
3974
3975 assert_dispatches!(
3977 |a: &PluginApi| a.open_file_at_location(
3978 PathBuf::from("/tmp/x.rs"), Some(4), Some(8)
3979 ),
3980 PluginCommand::OpenFileAtLocation { path, line, column }
3981 if path == PathBuf::from("/tmp/x.rs")
3982 && line == Some(4)
3983 && column == Some(8)
3984 );
3985
3986 assert_dispatches!(
3988 |a: &PluginApi| a.open_file_in_split(
3989 2, PathBuf::from("/tmp/y.rs"), Some(5), None
3990 ),
3991 PluginCommand::OpenFileInSplit { split_id, path, line, column }
3992 if split_id == 2
3993 && path == PathBuf::from("/tmp/y.rs")
3994 && line == Some(5)
3995 && column.is_none()
3996 );
3997
3998 assert_dispatches!(
4000 |a: &PluginApi| a.start_prompt("label".into(), "cmd".into()),
4001 PluginCommand::StartPrompt { label, prompt_type }
4002 if label == "label" && prompt_type == "cmd"
4003 );
4004
4005 assert_dispatches!(
4007 |a: &PluginApi| a.set_prompt_suggestions(vec![
4008 Suggestion::new("one".into()),
4009 Suggestion::new("two".into()),
4010 ]),
4011 PluginCommand::SetPromptSuggestions { suggestions }
4012 if suggestions.len() == 2
4013 && suggestions[0].text == "one"
4014 && suggestions[1].text == "two"
4015 );
4016
4017 assert_dispatches!(
4019 |a: &PluginApi| a.set_prompt_input_sync(true),
4020 PluginCommand::SetPromptInputSync { sync } if sync
4021 );
4022 assert_dispatches!(
4023 |a: &PluginApi| a.set_prompt_input_sync(false),
4024 PluginCommand::SetPromptInputSync { sync } if !sync
4025 );
4026
4027 assert_dispatches!(
4029 |a: &PluginApi| a.add_menu_item(
4030 "File".into(),
4031 MenuItem::Label { info: "info".into() },
4032 MenuPosition::Bottom,
4033 ),
4034 PluginCommand::AddMenuItem { menu_label, item, position }
4035 if menu_label == "File"
4036 && matches!(item, MenuItem::Label { ref info } if info == "info")
4037 && matches!(position, MenuPosition::Bottom)
4038 );
4039
4040 assert_dispatches!(
4042 |a: &PluginApi| a.add_menu(
4043 Menu {
4044 id: None,
4045 label: "Help".into(),
4046 items: vec![],
4047 when: None,
4048 },
4049 MenuPosition::After("Edit".into()),
4050 ),
4051 PluginCommand::AddMenu { menu, position }
4052 if menu.label == "Help"
4053 && matches!(position, MenuPosition::After(ref s) if s == "Edit")
4054 );
4055
4056 assert_dispatches!(
4058 |a: &PluginApi| a.remove_menu_item("File".into(), "Open".into()),
4059 PluginCommand::RemoveMenuItem { menu_label, item_label }
4060 if menu_label == "File" && item_label == "Open"
4061 );
4062
4063 assert_dispatches!(
4065 |a: &PluginApi| a.remove_menu("File".into()),
4066 PluginCommand::RemoveMenu { menu_label } if menu_label == "File"
4067 );
4068
4069 assert_dispatches!(
4071 |a: &PluginApi| a.create_virtual_buffer("buf".into(), "mode".into(), true),
4072 PluginCommand::CreateVirtualBuffer { name, mode, read_only }
4073 if name == "buf" && mode == "mode" && read_only
4074 );
4075
4076 assert_dispatches!(
4078 |a: &PluginApi| a.create_virtual_buffer_with_content(
4079 "n".into(), "m".into(), false, vec![]
4080 ),
4081 PluginCommand::CreateVirtualBufferWithContent {
4082 name, mode, read_only, show_line_numbers, show_cursors,
4083 editing_disabled, hidden_from_tabs, request_id, ..
4084 }
4085 if name == "n" && mode == "m" && !read_only
4086 && show_line_numbers && show_cursors
4087 && !editing_disabled && !hidden_from_tabs
4088 && request_id.is_none()
4089 );
4090
4091 assert_dispatches!(
4093 |a: &PluginApi| a.set_virtual_buffer_content(BufferId(9), vec![]),
4094 PluginCommand::SetVirtualBufferContent { buffer_id, entries }
4095 if buffer_id == BufferId(9) && entries.is_empty()
4096 );
4097
4098 assert_dispatches!(
4100 |a: &PluginApi| a.get_text_properties_at_cursor(BufferId(11)),
4101 PluginCommand::GetTextPropertiesAtCursor { buffer_id }
4102 if buffer_id == BufferId(11)
4103 );
4104
4105 assert_dispatches!(
4107 |a: &PluginApi| a.define_mode(
4108 "m".into(),
4109 vec![("j".into(), "move_down".into())],
4110 true,
4111 false,
4112 ),
4113 PluginCommand::DefineMode {
4114 name, bindings, read_only, allow_text_input, inherit_normal_bindings, plugin_name
4115 }
4116 if name == "m"
4117 && bindings.len() == 1
4118 && bindings[0].0 == "j"
4119 && bindings[0].1 == "move_down"
4120 && read_only
4121 && !allow_text_input
4122 && !inherit_normal_bindings
4123 && plugin_name.is_none()
4124 );
4125
4126 assert_dispatches!(
4128 |a: &PluginApi| a.show_buffer(BufferId(77)),
4129 PluginCommand::ShowBuffer { buffer_id } if buffer_id == BufferId(77)
4130 );
4131
4132 assert_dispatches!(
4134 |a: &PluginApi| a.set_split_scroll(5, 128),
4135 PluginCommand::SetSplitScroll { split_id, top_byte }
4136 if split_id == SplitId(5) && top_byte == 128
4137 );
4138
4139 assert_dispatches!(
4141 |a: &PluginApi| a.get_highlights(BufferId(1), 0..10, 7),
4142 PluginCommand::RequestHighlights { buffer_id, range, request_id }
4143 if buffer_id == BufferId(1) && range == (0..10) && request_id == 7
4144 );
4145 }
4146
4147 #[test]
4150 fn plugin_api_get_active_split_id_reads_snapshot() {
4151 let (api, _rx, _h, _c, snap) = mk_api();
4152 snap.write().unwrap().active_split_id = 42;
4153 assert_eq!(api.get_active_split_id(), 42);
4154 }
4155
4156 #[test]
4160 fn plugin_api_state_snapshot_handle_shares_underlying_arc() {
4161 let (api, _rx, _h, _c, snap) = mk_api();
4162 snap.write().unwrap().active_buffer_id = BufferId(42);
4163
4164 let h = api.state_snapshot_handle();
4165 assert_eq!(h.read().unwrap().active_buffer_id, BufferId(42));
4166 assert!(Arc::ptr_eq(&h, &snap));
4167 }
4168}