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 #[ts(type = "any")]
756 pub diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
757 #[ts(type = "any")]
760 pub folding_ranges: HashMap<String, Vec<lsp_types::FoldingRange>>,
761 #[ts(type = "any")]
764 pub config: serde_json::Value,
765 #[ts(type = "any")]
768 pub user_config: serde_json::Value,
769 #[ts(type = "GrammarInfo[]")]
771 pub available_grammars: Vec<GrammarInfoSnapshot>,
772 pub editor_mode: Option<String>,
775
776 #[ts(type = "any")]
780 pub plugin_view_states: HashMap<BufferId, HashMap<String, serde_json::Value>>,
781
782 #[serde(skip)]
785 #[ts(skip)]
786 pub plugin_view_states_split: usize,
787
788 #[serde(skip)]
791 #[ts(skip)]
792 pub keybinding_labels: HashMap<String, String>,
793
794 #[ts(type = "any")]
801 pub plugin_global_states: HashMap<String, HashMap<String, serde_json::Value>>,
802}
803
804impl EditorStateSnapshot {
805 pub fn new() -> Self {
806 Self {
807 active_buffer_id: BufferId(0),
808 active_split_id: 0,
809 buffers: HashMap::new(),
810 buffer_saved_diffs: HashMap::new(),
811 primary_cursor: None,
812 all_cursors: Vec::new(),
813 viewport: None,
814 buffer_cursor_positions: HashMap::new(),
815 buffer_text_properties: HashMap::new(),
816 selected_text: None,
817 clipboard: String::new(),
818 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
819 diagnostics: HashMap::new(),
820 folding_ranges: HashMap::new(),
821 config: serde_json::Value::Null,
822 user_config: serde_json::Value::Null,
823 available_grammars: Vec::new(),
824 editor_mode: None,
825 plugin_view_states: HashMap::new(),
826 plugin_view_states_split: 0,
827 keybinding_labels: HashMap::new(),
828 plugin_global_states: HashMap::new(),
829 }
830 }
831}
832
833impl Default for EditorStateSnapshot {
834 fn default() -> Self {
835 Self::new()
836 }
837}
838
839#[derive(Debug, Clone, Serialize, Deserialize, TS)]
841#[ts(export)]
842pub struct GrammarInfoSnapshot {
843 pub name: String,
845 pub source: String,
847 pub file_extensions: Vec<String>,
849 pub short_name: Option<String>,
851}
852
853#[derive(Debug, Clone, Serialize, Deserialize, TS)]
855#[ts(export)]
856pub enum MenuPosition {
857 Top,
859 Bottom,
861 Before(String),
863 After(String),
865}
866
867#[derive(Debug, Clone, Serialize, Deserialize, TS)]
869#[ts(export)]
870pub enum PluginCommand {
871 InsertText {
873 buffer_id: BufferId,
874 position: usize,
875 text: String,
876 },
877
878 DeleteRange {
880 buffer_id: BufferId,
881 range: Range<usize>,
882 },
883
884 AddOverlay {
889 buffer_id: BufferId,
890 namespace: Option<OverlayNamespace>,
891 range: Range<usize>,
892 options: OverlayOptions,
894 },
895
896 RemoveOverlay {
898 buffer_id: BufferId,
899 handle: OverlayHandle,
900 },
901
902 SetStatus { message: String },
904
905 ApplyTheme { theme_name: String },
907
908 ReloadConfig,
911
912 RegisterCommand { command: Command },
914
915 UnregisterCommand { name: String },
917
918 OpenFileInBackground { path: PathBuf },
920
921 InsertAtCursor { text: String },
923
924 SpawnProcess {
926 command: String,
927 args: Vec<String>,
928 cwd: Option<String>,
929 callback_id: JsCallbackId,
930 },
931
932 Delay {
934 callback_id: JsCallbackId,
935 duration_ms: u64,
936 },
937
938 SpawnBackgroundProcess {
942 process_id: u64,
944 command: String,
946 args: Vec<String>,
948 cwd: Option<String>,
950 callback_id: JsCallbackId,
952 },
953
954 KillBackgroundProcess { process_id: u64 },
956
957 SpawnProcessWait {
960 process_id: u64,
962 callback_id: JsCallbackId,
964 },
965
966 SetLayoutHints {
968 buffer_id: BufferId,
969 split_id: Option<SplitId>,
970 range: Range<usize>,
971 hints: LayoutHints,
972 },
973
974 SetLineNumbers { buffer_id: BufferId, enabled: bool },
976
977 SetViewMode { buffer_id: BufferId, mode: String },
979
980 SetLineWrap {
982 buffer_id: BufferId,
983 split_id: Option<SplitId>,
984 enabled: bool,
985 },
986
987 SubmitViewTransform {
989 buffer_id: BufferId,
990 split_id: Option<SplitId>,
991 payload: ViewTransformPayload,
992 },
993
994 ClearViewTransform {
996 buffer_id: BufferId,
997 split_id: Option<SplitId>,
998 },
999
1000 SetViewState {
1003 buffer_id: BufferId,
1004 key: String,
1005 #[ts(type = "any")]
1006 value: Option<serde_json::Value>,
1007 },
1008
1009 SetGlobalState {
1013 plugin_name: String,
1014 key: String,
1015 #[ts(type = "any")]
1016 value: Option<serde_json::Value>,
1017 },
1018
1019 ClearAllOverlays { buffer_id: BufferId },
1021
1022 ClearNamespace {
1024 buffer_id: BufferId,
1025 namespace: OverlayNamespace,
1026 },
1027
1028 ClearOverlaysInRange {
1031 buffer_id: BufferId,
1032 start: usize,
1033 end: usize,
1034 },
1035
1036 AddVirtualText {
1039 buffer_id: BufferId,
1040 virtual_text_id: String,
1041 position: usize,
1042 text: String,
1043 color: (u8, u8, u8),
1044 use_bg: bool, before: bool, },
1047
1048 RemoveVirtualText {
1050 buffer_id: BufferId,
1051 virtual_text_id: String,
1052 },
1053
1054 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
1056
1057 ClearVirtualTexts { buffer_id: BufferId },
1059
1060 AddVirtualLine {
1064 buffer_id: BufferId,
1065 position: usize,
1067 text: String,
1069 fg_color: Option<OverlayColorSpec>,
1073 bg_color: Option<OverlayColorSpec>,
1076 above: bool,
1078 namespace: String,
1080 priority: i32,
1082 },
1083
1084 ClearVirtualTextNamespace {
1087 buffer_id: BufferId,
1088 namespace: String,
1089 },
1090
1091 AddConceal {
1094 buffer_id: BufferId,
1095 namespace: OverlayNamespace,
1097 start: usize,
1099 end: usize,
1100 replacement: Option<String>,
1102 },
1103
1104 ClearConcealNamespace {
1106 buffer_id: BufferId,
1107 namespace: OverlayNamespace,
1108 },
1109
1110 ClearConcealsInRange {
1113 buffer_id: BufferId,
1114 start: usize,
1115 end: usize,
1116 },
1117
1118 AddFold {
1124 buffer_id: BufferId,
1125 start: usize,
1126 end: usize,
1127 placeholder: Option<String>,
1130 },
1131
1132 ClearFolds { buffer_id: BufferId },
1134
1135 AddSoftBreak {
1139 buffer_id: BufferId,
1140 namespace: OverlayNamespace,
1142 position: usize,
1144 indent: u16,
1146 },
1147
1148 ClearSoftBreakNamespace {
1150 buffer_id: BufferId,
1151 namespace: OverlayNamespace,
1152 },
1153
1154 ClearSoftBreaksInRange {
1156 buffer_id: BufferId,
1157 start: usize,
1158 end: usize,
1159 },
1160
1161 RefreshLines { buffer_id: BufferId },
1163
1164 RefreshAllLines,
1168
1169 HookCompleted { hook_name: String },
1173
1174 SetLineIndicator {
1177 buffer_id: BufferId,
1178 line: usize,
1180 namespace: String,
1182 symbol: String,
1184 color: (u8, u8, u8),
1186 priority: i32,
1188 },
1189
1190 SetLineIndicators {
1193 buffer_id: BufferId,
1194 lines: Vec<usize>,
1196 namespace: String,
1198 symbol: String,
1200 color: (u8, u8, u8),
1202 priority: i32,
1204 },
1205
1206 ClearLineIndicators {
1208 buffer_id: BufferId,
1209 namespace: String,
1211 },
1212
1213 SetFileExplorerDecorations {
1215 namespace: String,
1217 decorations: Vec<FileExplorerDecoration>,
1219 },
1220
1221 ClearFileExplorerDecorations {
1223 namespace: String,
1225 },
1226
1227 OpenFileAtLocation {
1230 path: PathBuf,
1231 line: Option<usize>, column: Option<usize>, },
1234
1235 OpenFileInSplit {
1238 split_id: usize,
1239 path: PathBuf,
1240 line: Option<usize>, column: Option<usize>, },
1243
1244 StartPrompt {
1247 label: String,
1248 prompt_type: String, },
1250
1251 StartPromptWithInitial {
1253 label: String,
1254 prompt_type: String,
1255 initial_value: String,
1256 },
1257
1258 StartPromptAsync {
1261 label: String,
1262 initial_value: String,
1263 callback_id: JsCallbackId,
1264 },
1265
1266 SetPromptSuggestions { suggestions: Vec<Suggestion> },
1269
1270 SetPromptInputSync { sync: bool },
1272
1273 AddMenuItem {
1276 menu_label: String,
1277 item: MenuItem,
1278 position: MenuPosition,
1279 },
1280
1281 AddMenu { menu: Menu, position: MenuPosition },
1283
1284 RemoveMenuItem {
1286 menu_label: String,
1287 item_label: String,
1288 },
1289
1290 RemoveMenu { menu_label: String },
1292
1293 CreateVirtualBuffer {
1295 name: String,
1297 mode: String,
1299 read_only: bool,
1301 },
1302
1303 CreateVirtualBufferWithContent {
1307 name: String,
1309 mode: String,
1311 read_only: bool,
1313 entries: Vec<TextPropertyEntry>,
1315 show_line_numbers: bool,
1317 show_cursors: bool,
1319 editing_disabled: bool,
1321 hidden_from_tabs: bool,
1323 request_id: Option<u64>,
1325 },
1326
1327 CreateVirtualBufferInSplit {
1330 name: String,
1332 mode: String,
1334 read_only: bool,
1336 entries: Vec<TextPropertyEntry>,
1338 ratio: f32,
1340 direction: Option<String>,
1342 panel_id: Option<String>,
1344 show_line_numbers: bool,
1346 show_cursors: bool,
1348 editing_disabled: bool,
1350 line_wrap: Option<bool>,
1352 before: bool,
1354 request_id: Option<u64>,
1356 },
1357
1358 SetVirtualBufferContent {
1360 buffer_id: BufferId,
1361 entries: Vec<TextPropertyEntry>,
1363 },
1364
1365 GetTextPropertiesAtCursor { buffer_id: BufferId },
1367
1368 CreateBufferGroup {
1371 name: String,
1373 mode: String,
1375 layout_json: String,
1377 request_id: Option<u64>,
1379 },
1380
1381 SetPanelContent {
1383 group_id: usize,
1385 panel_name: String,
1387 entries: Vec<TextPropertyEntry>,
1389 },
1390
1391 CloseBufferGroup { group_id: usize },
1393
1394 FocusPanel { group_id: usize, panel_name: String },
1396
1397 DefineMode {
1399 name: String,
1400 bindings: Vec<(String, String)>, read_only: bool,
1402 allow_text_input: bool,
1404 inherit_normal_bindings: bool,
1407 plugin_name: Option<String>,
1409 },
1410
1411 ShowBuffer { buffer_id: BufferId },
1413
1414 CreateVirtualBufferInExistingSplit {
1416 name: String,
1418 mode: String,
1420 read_only: bool,
1422 entries: Vec<TextPropertyEntry>,
1424 split_id: SplitId,
1426 show_line_numbers: bool,
1428 show_cursors: bool,
1430 editing_disabled: bool,
1432 line_wrap: Option<bool>,
1434 request_id: Option<u64>,
1436 },
1437
1438 CloseBuffer { buffer_id: BufferId },
1440
1441 CreateCompositeBuffer {
1444 name: String,
1446 mode: String,
1448 layout: CompositeLayoutConfig,
1450 sources: Vec<CompositeSourceConfig>,
1452 hunks: Option<Vec<CompositeHunk>>,
1454 initial_focus_hunk: Option<usize>,
1456 request_id: Option<u64>,
1458 },
1459
1460 UpdateCompositeAlignment {
1462 buffer_id: BufferId,
1463 hunks: Vec<CompositeHunk>,
1464 },
1465
1466 CloseCompositeBuffer { buffer_id: BufferId },
1468
1469 FlushLayout,
1476
1477 CompositeNextHunk { buffer_id: BufferId },
1479
1480 CompositePrevHunk { buffer_id: BufferId },
1482
1483 FocusSplit { split_id: SplitId },
1485
1486 SetSplitBuffer {
1488 split_id: SplitId,
1489 buffer_id: BufferId,
1490 },
1491
1492 SetSplitScroll { split_id: SplitId, top_byte: usize },
1494
1495 RequestHighlights {
1497 buffer_id: BufferId,
1498 range: Range<usize>,
1499 request_id: u64,
1500 },
1501
1502 CloseSplit { split_id: SplitId },
1504
1505 SetSplitRatio {
1507 split_id: SplitId,
1508 ratio: f32,
1510 },
1511
1512 SetSplitLabel { split_id: SplitId, label: String },
1514
1515 ClearSplitLabel { split_id: SplitId },
1517
1518 GetSplitByLabel { label: String, request_id: u64 },
1520
1521 DistributeSplitsEvenly {
1523 split_ids: Vec<SplitId>,
1525 },
1526
1527 SetBufferCursor {
1529 buffer_id: BufferId,
1530 position: usize,
1532 },
1533
1534 SetBufferShowCursors { buffer_id: BufferId, show: bool },
1542
1543 SendLspRequest {
1545 language: String,
1546 method: String,
1547 #[ts(type = "any")]
1548 params: Option<JsonValue>,
1549 request_id: u64,
1550 },
1551
1552 SetClipboard { text: String },
1554
1555 DeleteSelection,
1558
1559 SetContext {
1563 name: String,
1565 active: bool,
1567 },
1568
1569 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1571
1572 ExecuteAction {
1575 action_name: String,
1577 },
1578
1579 ExecuteActions {
1583 actions: Vec<ActionSpec>,
1585 },
1586
1587 GetBufferText {
1589 buffer_id: BufferId,
1591 start: usize,
1593 end: usize,
1595 request_id: u64,
1597 },
1598
1599 GetLineStartPosition {
1602 buffer_id: BufferId,
1604 line: u32,
1606 request_id: u64,
1608 },
1609
1610 GetLineEndPosition {
1614 buffer_id: BufferId,
1616 line: u32,
1618 request_id: u64,
1620 },
1621
1622 GetBufferLineCount {
1624 buffer_id: BufferId,
1626 request_id: u64,
1628 },
1629
1630 ScrollToLineCenter {
1633 split_id: SplitId,
1635 buffer_id: BufferId,
1637 line: usize,
1639 },
1640
1641 ScrollBufferToLine {
1647 buffer_id: BufferId,
1649 line: usize,
1651 },
1652
1653 SetEditorMode {
1656 mode: Option<String>,
1658 },
1659
1660 ShowActionPopup {
1663 popup_id: String,
1665 title: String,
1667 message: String,
1669 actions: Vec<ActionPopupAction>,
1671 },
1672
1673 DisableLspForLanguage {
1675 language: String,
1677 },
1678
1679 RestartLspForLanguage {
1681 language: String,
1683 },
1684
1685 SetLspRootUri {
1689 language: String,
1691 uri: String,
1693 },
1694
1695 CreateScrollSyncGroup {
1699 group_id: u32,
1701 left_split: SplitId,
1703 right_split: SplitId,
1705 },
1706
1707 SetScrollSyncAnchors {
1710 group_id: u32,
1712 anchors: Vec<(usize, usize)>,
1714 },
1715
1716 RemoveScrollSyncGroup {
1718 group_id: u32,
1720 },
1721
1722 SaveBufferToPath {
1725 buffer_id: BufferId,
1727 path: PathBuf,
1729 },
1730
1731 LoadPlugin {
1734 path: PathBuf,
1736 callback_id: JsCallbackId,
1738 },
1739
1740 UnloadPlugin {
1743 name: String,
1745 callback_id: JsCallbackId,
1747 },
1748
1749 ReloadPlugin {
1752 name: String,
1754 callback_id: JsCallbackId,
1756 },
1757
1758 ListPlugins {
1761 callback_id: JsCallbackId,
1763 },
1764
1765 ReloadThemes { apply_theme: Option<String> },
1769
1770 RegisterGrammar {
1773 language: String,
1775 grammar_path: String,
1777 extensions: Vec<String>,
1779 },
1780
1781 RegisterLanguageConfig {
1784 language: String,
1786 config: LanguagePackConfig,
1788 },
1789
1790 RegisterLspServer {
1793 language: String,
1795 config: LspServerPackConfig,
1797 },
1798
1799 ReloadGrammars { callback_id: JsCallbackId },
1803
1804 CreateTerminal {
1808 cwd: Option<String>,
1810 direction: Option<String>,
1812 ratio: Option<f32>,
1814 focus: Option<bool>,
1816 request_id: u64,
1818 },
1819
1820 SendTerminalInput {
1822 terminal_id: TerminalId,
1824 data: String,
1826 },
1827
1828 CloseTerminal {
1830 terminal_id: TerminalId,
1832 },
1833
1834 GrepProject {
1838 pattern: String,
1840 fixed_string: bool,
1842 case_sensitive: bool,
1844 max_results: usize,
1846 whole_words: bool,
1848 callback_id: JsCallbackId,
1850 },
1851
1852 GrepProjectStreaming {
1857 pattern: String,
1859 fixed_string: bool,
1861 case_sensitive: bool,
1863 max_results: usize,
1865 whole_words: bool,
1867 search_id: u64,
1869 callback_id: JsCallbackId,
1871 },
1872
1873 ReplaceInBuffer {
1877 file_path: PathBuf,
1879 matches: Vec<(usize, usize)>,
1881 replacement: String,
1883 callback_id: JsCallbackId,
1885 },
1886}
1887
1888impl PluginCommand {
1889 pub fn debug_variant_name(&self) -> String {
1891 let dbg = format!("{:?}", self);
1892 dbg.split([' ', '{', '(']).next().unwrap_or("?").to_string()
1893 }
1894}
1895
1896#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
1905#[serde(rename_all = "camelCase")]
1906#[ts(export)]
1907pub struct LanguagePackConfig {
1908 #[serde(default)]
1910 pub comment_prefix: Option<String>,
1911
1912 #[serde(default)]
1914 pub block_comment_start: Option<String>,
1915
1916 #[serde(default)]
1918 pub block_comment_end: Option<String>,
1919
1920 #[serde(default)]
1922 pub use_tabs: Option<bool>,
1923
1924 #[serde(default)]
1926 pub tab_size: Option<usize>,
1927
1928 #[serde(default)]
1930 pub auto_indent: Option<bool>,
1931
1932 #[serde(default)]
1935 pub show_whitespace_tabs: Option<bool>,
1936
1937 #[serde(default)]
1939 pub formatter: Option<FormatterPackConfig>,
1940}
1941
1942#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1944#[serde(rename_all = "camelCase")]
1945#[ts(export)]
1946pub struct FormatterPackConfig {
1947 pub command: String,
1949
1950 #[serde(default)]
1952 pub args: Vec<String>,
1953}
1954
1955#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1957#[serde(rename_all = "camelCase")]
1958#[ts(export)]
1959pub struct ProcessLimitsPackConfig {
1960 #[serde(default)]
1962 pub max_memory_percent: Option<u32>,
1963
1964 #[serde(default)]
1966 pub max_cpu_percent: Option<u32>,
1967
1968 #[serde(default)]
1970 pub enabled: Option<bool>,
1971}
1972
1973#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1975#[serde(rename_all = "camelCase")]
1976#[ts(export)]
1977pub struct LspServerPackConfig {
1978 pub command: String,
1980
1981 #[serde(default)]
1983 pub args: Vec<String>,
1984
1985 #[serde(default)]
1987 pub auto_start: Option<bool>,
1988
1989 #[serde(default)]
1991 #[ts(type = "Record<string, unknown> | null")]
1992 pub initialization_options: Option<JsonValue>,
1993
1994 #[serde(default)]
1996 pub process_limits: Option<ProcessLimitsPackConfig>,
1997}
1998
1999#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
2001#[ts(export)]
2002pub enum HunkStatus {
2003 Pending,
2004 Staged,
2005 Discarded,
2006}
2007
2008#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2010#[ts(export)]
2011pub struct ReviewHunk {
2012 pub id: String,
2013 pub file: String,
2014 pub context_header: String,
2015 pub status: HunkStatus,
2016 pub base_range: Option<(usize, usize)>,
2018 pub modified_range: Option<(usize, usize)>,
2020}
2021
2022#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2024#[serde(deny_unknown_fields)]
2025#[ts(export, rename = "TsActionPopupAction")]
2026pub struct ActionPopupAction {
2027 pub id: String,
2029 pub label: String,
2031}
2032
2033#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2035#[serde(deny_unknown_fields)]
2036#[ts(export)]
2037pub struct ActionPopupOptions {
2038 pub id: String,
2040 pub title: String,
2042 pub message: String,
2044 pub actions: Vec<ActionPopupAction>,
2046}
2047
2048#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2050#[ts(export)]
2051pub struct TsHighlightSpan {
2052 pub start: u32,
2053 pub end: u32,
2054 #[ts(type = "[number, number, number]")]
2055 pub color: (u8, u8, u8),
2056 pub bold: bool,
2057 pub italic: bool,
2058}
2059
2060#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2062#[ts(export)]
2063pub struct SpawnResult {
2064 pub stdout: String,
2066 pub stderr: String,
2068 pub exit_code: i32,
2070}
2071
2072#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2074#[ts(export)]
2075pub struct BackgroundProcessResult {
2076 #[ts(type = "number")]
2078 pub process_id: u64,
2079 pub exit_code: i32,
2082}
2083
2084#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2086#[serde(rename_all = "camelCase")]
2087#[ts(export, rename_all = "camelCase")]
2088pub struct GrepMatch {
2089 pub file: String,
2091 #[ts(type = "number")]
2093 pub buffer_id: usize,
2094 #[ts(type = "number")]
2096 pub byte_offset: usize,
2097 #[ts(type = "number")]
2099 pub length: usize,
2100 #[ts(type = "number")]
2102 pub line: usize,
2103 #[ts(type = "number")]
2105 pub column: usize,
2106 pub context: String,
2108}
2109
2110#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2112#[serde(rename_all = "camelCase")]
2113#[ts(export, rename_all = "camelCase")]
2114pub struct ReplaceResult {
2115 #[ts(type = "number")]
2117 pub replacements: usize,
2118 #[ts(type = "number")]
2120 pub buffer_id: usize,
2121}
2122
2123#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2125#[serde(deny_unknown_fields, rename_all = "camelCase")]
2126#[ts(export, rename = "TextPropertyEntry", rename_all = "camelCase")]
2127pub struct JsTextPropertyEntry {
2128 pub text: String,
2130 #[serde(default)]
2132 #[ts(optional, type = "Record<string, unknown>")]
2133 pub properties: Option<HashMap<String, JsonValue>>,
2134 #[serde(default)]
2136 #[ts(optional, type = "Partial<OverlayOptions>")]
2137 pub style: Option<OverlayOptions>,
2138 #[serde(default)]
2140 #[ts(optional)]
2141 pub inline_overlays: Option<Vec<crate::text_property::InlineOverlay>>,
2142}
2143
2144#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2146#[ts(export)]
2147pub struct DirEntry {
2148 pub name: String,
2150 pub is_file: bool,
2152 pub is_dir: bool,
2154}
2155
2156#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2158#[ts(export)]
2159pub struct JsPosition {
2160 pub line: u32,
2162 pub character: u32,
2164}
2165
2166#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2168#[ts(export)]
2169pub struct JsRange {
2170 pub start: JsPosition,
2172 pub end: JsPosition,
2174}
2175
2176#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2178#[ts(export)]
2179pub struct JsDiagnostic {
2180 pub uri: String,
2182 pub message: String,
2184 pub severity: Option<u8>,
2186 pub range: JsRange,
2188 #[ts(optional)]
2190 pub source: Option<String>,
2191}
2192
2193#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2195#[serde(deny_unknown_fields)]
2196#[ts(export)]
2197pub struct CreateVirtualBufferOptions {
2198 pub name: String,
2200 #[serde(default)]
2202 #[ts(optional)]
2203 pub mode: Option<String>,
2204 #[serde(default, rename = "readOnly")]
2206 #[ts(optional, rename = "readOnly")]
2207 pub read_only: Option<bool>,
2208 #[serde(default, rename = "showLineNumbers")]
2210 #[ts(optional, rename = "showLineNumbers")]
2211 pub show_line_numbers: Option<bool>,
2212 #[serde(default, rename = "showCursors")]
2214 #[ts(optional, rename = "showCursors")]
2215 pub show_cursors: Option<bool>,
2216 #[serde(default, rename = "editingDisabled")]
2218 #[ts(optional, rename = "editingDisabled")]
2219 pub editing_disabled: Option<bool>,
2220 #[serde(default, rename = "hiddenFromTabs")]
2222 #[ts(optional, rename = "hiddenFromTabs")]
2223 pub hidden_from_tabs: Option<bool>,
2224 #[serde(default)]
2226 #[ts(optional)]
2227 pub entries: Option<Vec<JsTextPropertyEntry>>,
2228}
2229
2230#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2232#[serde(deny_unknown_fields)]
2233#[ts(export)]
2234pub struct CreateVirtualBufferInSplitOptions {
2235 pub name: String,
2237 #[serde(default)]
2239 #[ts(optional)]
2240 pub mode: Option<String>,
2241 #[serde(default, rename = "readOnly")]
2243 #[ts(optional, rename = "readOnly")]
2244 pub read_only: Option<bool>,
2245 #[serde(default)]
2247 #[ts(optional)]
2248 pub ratio: Option<f32>,
2249 #[serde(default)]
2251 #[ts(optional)]
2252 pub direction: Option<String>,
2253 #[serde(default, rename = "panelId")]
2255 #[ts(optional, rename = "panelId")]
2256 pub panel_id: Option<String>,
2257 #[serde(default, rename = "showLineNumbers")]
2259 #[ts(optional, rename = "showLineNumbers")]
2260 pub show_line_numbers: Option<bool>,
2261 #[serde(default, rename = "showCursors")]
2263 #[ts(optional, rename = "showCursors")]
2264 pub show_cursors: Option<bool>,
2265 #[serde(default, rename = "editingDisabled")]
2267 #[ts(optional, rename = "editingDisabled")]
2268 pub editing_disabled: Option<bool>,
2269 #[serde(default, rename = "lineWrap")]
2271 #[ts(optional, rename = "lineWrap")]
2272 pub line_wrap: Option<bool>,
2273 #[serde(default)]
2275 #[ts(optional)]
2276 pub before: Option<bool>,
2277 #[serde(default)]
2279 #[ts(optional)]
2280 pub entries: Option<Vec<JsTextPropertyEntry>>,
2281}
2282
2283#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2285#[serde(deny_unknown_fields)]
2286#[ts(export)]
2287pub struct CreateVirtualBufferInExistingSplitOptions {
2288 pub name: String,
2290 #[serde(rename = "splitId")]
2292 #[ts(rename = "splitId")]
2293 pub split_id: usize,
2294 #[serde(default)]
2296 #[ts(optional)]
2297 pub mode: Option<String>,
2298 #[serde(default, rename = "readOnly")]
2300 #[ts(optional, rename = "readOnly")]
2301 pub read_only: Option<bool>,
2302 #[serde(default, rename = "showLineNumbers")]
2304 #[ts(optional, rename = "showLineNumbers")]
2305 pub show_line_numbers: Option<bool>,
2306 #[serde(default, rename = "showCursors")]
2308 #[ts(optional, rename = "showCursors")]
2309 pub show_cursors: Option<bool>,
2310 #[serde(default, rename = "editingDisabled")]
2312 #[ts(optional, rename = "editingDisabled")]
2313 pub editing_disabled: Option<bool>,
2314 #[serde(default, rename = "lineWrap")]
2316 #[ts(optional, rename = "lineWrap")]
2317 pub line_wrap: Option<bool>,
2318 #[serde(default)]
2320 #[ts(optional)]
2321 pub entries: Option<Vec<JsTextPropertyEntry>>,
2322}
2323
2324#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2326#[serde(deny_unknown_fields)]
2327#[ts(export)]
2328pub struct CreateTerminalOptions {
2329 #[serde(default)]
2331 #[ts(optional)]
2332 pub cwd: Option<String>,
2333 #[serde(default)]
2335 #[ts(optional)]
2336 pub direction: Option<String>,
2337 #[serde(default)]
2339 #[ts(optional)]
2340 pub ratio: Option<f32>,
2341 #[serde(default)]
2343 #[ts(optional)]
2344 pub focus: Option<bool>,
2345}
2346
2347#[derive(Debug, Clone, Serialize, TS)]
2352#[ts(export, type = "Array<Record<string, unknown>>")]
2353pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
2354
2355#[cfg(feature = "plugins")]
2357mod fromjs_impls {
2358 use super::*;
2359 use rquickjs::{Ctx, FromJs, Value};
2360
2361 impl<'js> FromJs<'js> for JsTextPropertyEntry {
2362 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2363 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2364 from: "object",
2365 to: "JsTextPropertyEntry",
2366 message: Some(e.to_string()),
2367 })
2368 }
2369 }
2370
2371 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
2372 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2373 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2374 from: "object",
2375 to: "CreateVirtualBufferOptions",
2376 message: Some(e.to_string()),
2377 })
2378 }
2379 }
2380
2381 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
2382 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2383 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2384 from: "object",
2385 to: "CreateVirtualBufferInSplitOptions",
2386 message: Some(e.to_string()),
2387 })
2388 }
2389 }
2390
2391 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
2392 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2393 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2394 from: "object",
2395 to: "CreateVirtualBufferInExistingSplitOptions",
2396 message: Some(e.to_string()),
2397 })
2398 }
2399 }
2400
2401 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
2402 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
2403 rquickjs_serde::to_value(ctx.clone(), &self.0)
2404 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
2405 }
2406 }
2407
2408 impl<'js> FromJs<'js> for ActionSpec {
2411 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2412 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2413 from: "object",
2414 to: "ActionSpec",
2415 message: Some(e.to_string()),
2416 })
2417 }
2418 }
2419
2420 impl<'js> FromJs<'js> for ActionPopupAction {
2421 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2422 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2423 from: "object",
2424 to: "ActionPopupAction",
2425 message: Some(e.to_string()),
2426 })
2427 }
2428 }
2429
2430 impl<'js> FromJs<'js> for ActionPopupOptions {
2431 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2432 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2433 from: "object",
2434 to: "ActionPopupOptions",
2435 message: Some(e.to_string()),
2436 })
2437 }
2438 }
2439
2440 impl<'js> FromJs<'js> for ViewTokenWire {
2441 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2442 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2443 from: "object",
2444 to: "ViewTokenWire",
2445 message: Some(e.to_string()),
2446 })
2447 }
2448 }
2449
2450 impl<'js> FromJs<'js> for ViewTokenStyle {
2451 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2452 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2453 from: "object",
2454 to: "ViewTokenStyle",
2455 message: Some(e.to_string()),
2456 })
2457 }
2458 }
2459
2460 impl<'js> FromJs<'js> for LayoutHints {
2461 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2462 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2463 from: "object",
2464 to: "LayoutHints",
2465 message: Some(e.to_string()),
2466 })
2467 }
2468 }
2469
2470 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
2471 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2472 let json: serde_json::Value =
2474 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2475 from: "object",
2476 to: "CreateCompositeBufferOptions (json)",
2477 message: Some(e.to_string()),
2478 })?;
2479 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
2480 from: "json",
2481 to: "CreateCompositeBufferOptions",
2482 message: Some(e.to_string()),
2483 })
2484 }
2485 }
2486
2487 impl<'js> FromJs<'js> for CompositeHunk {
2488 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2489 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2490 from: "object",
2491 to: "CompositeHunk",
2492 message: Some(e.to_string()),
2493 })
2494 }
2495 }
2496
2497 impl<'js> FromJs<'js> for LanguagePackConfig {
2498 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2499 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2500 from: "object",
2501 to: "LanguagePackConfig",
2502 message: Some(e.to_string()),
2503 })
2504 }
2505 }
2506
2507 impl<'js> FromJs<'js> for LspServerPackConfig {
2508 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2509 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2510 from: "object",
2511 to: "LspServerPackConfig",
2512 message: Some(e.to_string()),
2513 })
2514 }
2515 }
2516
2517 impl<'js> FromJs<'js> for ProcessLimitsPackConfig {
2518 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2519 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2520 from: "object",
2521 to: "ProcessLimitsPackConfig",
2522 message: Some(e.to_string()),
2523 })
2524 }
2525 }
2526
2527 impl<'js> FromJs<'js> for CreateTerminalOptions {
2528 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2529 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2530 from: "object",
2531 to: "CreateTerminalOptions",
2532 message: Some(e.to_string()),
2533 })
2534 }
2535 }
2536
2537 #[cfg(test)]
2549 mod tests {
2550 use super::*;
2551 use rquickjs::{Context, Runtime};
2552
2553 fn with_js<R>(f: impl for<'js> FnOnce(Ctx<'js>) -> R) -> R {
2556 let rt = Runtime::new().expect("create rquickjs runtime");
2557 let ctx = Context::full(&rt).expect("create rquickjs context");
2558 ctx.with(f)
2559 }
2560
2561 fn eval_as<T>(src: &str) -> T
2563 where
2564 for<'js> T: rquickjs::FromJs<'js>,
2565 {
2566 with_js(|ctx| {
2567 let value: Value = ctx
2568 .eval::<Value, _>(src.as_bytes())
2569 .expect("eval JS source");
2570 T::from_js(&ctx, value).expect("from_js decode")
2571 })
2572 }
2573
2574 #[test]
2575 fn js_text_property_entry_decodes_text_and_properties() {
2576 let got: JsTextPropertyEntry =
2577 eval_as("({text: 'hello', properties: {file: '/x.rs'}})");
2578 assert_eq!(got.text, "hello");
2579 let props = got.properties.expect("properties present");
2580 assert_eq!(props.get("file").and_then(|v| v.as_str()), Some("/x.rs"));
2581 }
2582
2583 #[test]
2584 fn create_virtual_buffer_options_decodes_name() {
2585 let got: CreateVirtualBufferOptions = eval_as("({name: 'logs', readOnly: true})");
2586 assert_eq!(got.name, "logs");
2587 assert_eq!(got.read_only, Some(true));
2588 }
2589
2590 #[test]
2591 fn create_virtual_buffer_in_split_options_decodes_ratio() {
2592 let got: CreateVirtualBufferInSplitOptions =
2593 eval_as("({name: 'diag', ratio: 0.25, direction: 'horizontal'})");
2594 assert_eq!(got.name, "diag");
2595 assert!(matches!(got.ratio, Some(r) if (r - 0.25).abs() < 1e-6));
2596 assert_eq!(got.direction.as_deref(), Some("horizontal"));
2597 }
2598
2599 #[test]
2600 fn create_virtual_buffer_in_existing_split_options_decodes_splitid() {
2601 let got: CreateVirtualBufferInExistingSplitOptions =
2602 eval_as("({name: 'n', splitId: 7})");
2603 assert_eq!(got.name, "n");
2604 assert_eq!(got.split_id, 7);
2605 }
2606
2607 #[test]
2608 fn create_terminal_options_decodes_cwd_and_focus() {
2609 let got: CreateTerminalOptions =
2610 eval_as("({cwd: '/tmp', direction: 'vertical', focus: false})");
2611 assert_eq!(got.cwd.as_deref(), Some("/tmp"));
2612 assert_eq!(got.direction.as_deref(), Some("vertical"));
2613 assert_eq!(got.focus, Some(false));
2614 }
2615
2616 #[test]
2617 fn action_spec_decodes_action_and_count() {
2618 let got: ActionSpec = eval_as("({action: 'move_word_right', count: 5})");
2619 assert_eq!(got.action, "move_word_right");
2620 assert_eq!(got.count, 5);
2621 }
2622
2623 #[test]
2624 fn action_popup_action_decodes_id_and_label() {
2625 let got: ActionPopupAction = eval_as("({id: 'ok', label: 'OK'})");
2626 assert_eq!(got.id, "ok");
2627 assert_eq!(got.label, "OK");
2628 }
2629
2630 #[test]
2631 fn action_popup_options_decodes_actions_list() {
2632 let got: ActionPopupOptions = eval_as(
2633 "({id: 'p', title: 't', message: 'm', \
2634 actions: [{id: 'ok', label: 'OK'}]})",
2635 );
2636 assert_eq!(got.id, "p");
2637 assert_eq!(got.title, "t");
2638 assert_eq!(got.message, "m");
2639 assert_eq!(got.actions.len(), 1);
2640 assert_eq!(got.actions[0].id, "ok");
2641 }
2642
2643 #[test]
2644 fn view_token_wire_decodes_offset_and_kind() {
2645 let got: ViewTokenWire = eval_as("({source_offset: 42, kind: 'Newline'})");
2649 assert_eq!(got.source_offset, Some(42));
2650 assert!(matches!(got.kind, ViewTokenWireKind::Newline));
2651 }
2652
2653 #[test]
2654 fn view_token_style_decodes_boolean_flags() {
2655 let got: ViewTokenStyle = eval_as("({bold: true, italic: true})");
2659 assert!(got.bold);
2660 assert!(got.italic);
2661 assert!(got.fg.is_none());
2662 }
2663
2664 #[test]
2665 fn layout_hints_decodes_compose_width() {
2666 let got: LayoutHints = eval_as("({composeWidth: 120})");
2667 assert_eq!(got.compose_width, Some(120));
2668 assert!(got.column_guides.is_none());
2669 }
2670
2671 #[test]
2672 fn create_composite_buffer_options_decodes_name_and_sources() {
2673 let got: CreateCompositeBufferOptions = eval_as(
2674 "({name: 'diff', mode: 'm', \
2675 layout: {type: 'side-by-side', ratios: [0.5, 0.5], showSeparator: true}, \
2676 sources: [{bufferId: 3, label: 'OLD'}]})",
2677 );
2678 assert_eq!(got.name, "diff");
2679 assert_eq!(got.layout.layout_type, "side-by-side");
2680 assert_eq!(got.sources.len(), 1);
2681 assert_eq!(got.sources[0].buffer_id, 3);
2682 assert_eq!(got.sources[0].label, "OLD");
2683 }
2684
2685 #[test]
2686 fn composite_hunk_decodes_all_fields() {
2687 let got: CompositeHunk =
2688 eval_as("({oldStart: 1, oldCount: 2, newStart: 3, newCount: 4})");
2689 assert_eq!(got.old_start, 1);
2690 assert_eq!(got.old_count, 2);
2691 assert_eq!(got.new_start, 3);
2692 assert_eq!(got.new_count, 4);
2693 }
2694
2695 #[test]
2696 fn language_pack_config_decodes_comment_prefix_and_tab_size() {
2697 let got: LanguagePackConfig =
2698 eval_as("({commentPrefix: '//', tabSize: 7, useTabs: true})");
2699 assert_eq!(got.comment_prefix.as_deref(), Some("//"));
2700 assert_eq!(got.tab_size, Some(7));
2701 assert_eq!(got.use_tabs, Some(true));
2702 }
2703
2704 #[test]
2705 fn lsp_server_pack_config_decodes_command_and_args() {
2706 let got: LspServerPackConfig =
2707 eval_as("({command: 'rust-analyzer', args: ['--log'], autoStart: true})");
2708 assert_eq!(got.command, "rust-analyzer");
2709 assert_eq!(got.args, vec!["--log".to_string()]);
2710 assert_eq!(got.auto_start, Some(true));
2711 }
2712
2713 #[test]
2714 fn process_limits_pack_config_decodes_percentages() {
2715 let got: ProcessLimitsPackConfig =
2716 eval_as("({maxMemoryPercent: 75, maxCpuPercent: 50, enabled: true})");
2717 assert_eq!(got.max_memory_percent, Some(75));
2718 assert_eq!(got.max_cpu_percent, Some(50));
2719 assert_eq!(got.enabled, Some(true));
2720 }
2721
2722 #[test]
2727 fn text_properties_at_cursor_into_js_preserves_length() {
2728 use rquickjs::IntoJs;
2729 with_js(|ctx| {
2730 let mut entry = std::collections::HashMap::new();
2731 entry.insert("k".to_string(), serde_json::json!("v"));
2732 let payload = TextPropertiesAtCursor(vec![entry.clone(), entry]);
2733
2734 let v = payload.into_js(&ctx).expect("into_js");
2735 let arr = v.as_array().expect("expected JS array");
2736 assert_eq!(arr.len(), 2);
2737 });
2738 }
2739 }
2740}
2741
2742pub struct PluginApi {
2744 hooks: Arc<RwLock<HookRegistry>>,
2746
2747 commands: Arc<RwLock<CommandRegistry>>,
2749
2750 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2752
2753 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2755}
2756
2757impl PluginApi {
2758 pub fn new(
2760 hooks: Arc<RwLock<HookRegistry>>,
2761 commands: Arc<RwLock<CommandRegistry>>,
2762 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2763 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2764 ) -> Self {
2765 Self {
2766 hooks,
2767 commands,
2768 command_sender,
2769 state_snapshot,
2770 }
2771 }
2772
2773 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
2775 let mut hooks = self.hooks.write().unwrap();
2776 hooks.add_hook(hook_name, callback);
2777 }
2778
2779 pub fn unregister_hooks(&self, hook_name: &str) {
2781 let mut hooks = self.hooks.write().unwrap();
2782 hooks.remove_hooks(hook_name);
2783 }
2784
2785 pub fn register_command(&self, command: Command) {
2787 let commands = self.commands.read().unwrap();
2788 commands.register(command);
2789 }
2790
2791 pub fn unregister_command(&self, name: &str) {
2793 let commands = self.commands.read().unwrap();
2794 commands.unregister(name);
2795 }
2796
2797 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
2799 self.command_sender
2800 .send(command)
2801 .map_err(|e| format!("Failed to send command: {}", e))
2802 }
2803
2804 pub fn insert_text(
2806 &self,
2807 buffer_id: BufferId,
2808 position: usize,
2809 text: String,
2810 ) -> Result<(), String> {
2811 self.send_command(PluginCommand::InsertText {
2812 buffer_id,
2813 position,
2814 text,
2815 })
2816 }
2817
2818 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
2820 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
2821 }
2822
2823 pub fn add_overlay(
2831 &self,
2832 buffer_id: BufferId,
2833 namespace: Option<String>,
2834 range: Range<usize>,
2835 options: OverlayOptions,
2836 ) -> Result<(), String> {
2837 self.send_command(PluginCommand::AddOverlay {
2838 buffer_id,
2839 namespace: namespace.map(OverlayNamespace::from_string),
2840 range,
2841 options,
2842 })
2843 }
2844
2845 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
2847 self.send_command(PluginCommand::RemoveOverlay {
2848 buffer_id,
2849 handle: OverlayHandle::from_string(handle),
2850 })
2851 }
2852
2853 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
2855 self.send_command(PluginCommand::ClearNamespace {
2856 buffer_id,
2857 namespace: OverlayNamespace::from_string(namespace),
2858 })
2859 }
2860
2861 pub fn clear_overlays_in_range(
2864 &self,
2865 buffer_id: BufferId,
2866 start: usize,
2867 end: usize,
2868 ) -> Result<(), String> {
2869 self.send_command(PluginCommand::ClearOverlaysInRange {
2870 buffer_id,
2871 start,
2872 end,
2873 })
2874 }
2875
2876 pub fn set_status(&self, message: String) -> Result<(), String> {
2878 self.send_command(PluginCommand::SetStatus { message })
2879 }
2880
2881 pub fn open_file_at_location(
2884 &self,
2885 path: PathBuf,
2886 line: Option<usize>,
2887 column: Option<usize>,
2888 ) -> Result<(), String> {
2889 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
2890 }
2891
2892 pub fn open_file_in_split(
2897 &self,
2898 split_id: usize,
2899 path: PathBuf,
2900 line: Option<usize>,
2901 column: Option<usize>,
2902 ) -> Result<(), String> {
2903 self.send_command(PluginCommand::OpenFileInSplit {
2904 split_id,
2905 path,
2906 line,
2907 column,
2908 })
2909 }
2910
2911 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
2914 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
2915 }
2916
2917 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
2920 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
2921 }
2922
2923 pub fn set_prompt_input_sync(&self, sync: bool) -> Result<(), String> {
2925 self.send_command(PluginCommand::SetPromptInputSync { sync })
2926 }
2927
2928 pub fn add_menu_item(
2930 &self,
2931 menu_label: String,
2932 item: MenuItem,
2933 position: MenuPosition,
2934 ) -> Result<(), String> {
2935 self.send_command(PluginCommand::AddMenuItem {
2936 menu_label,
2937 item,
2938 position,
2939 })
2940 }
2941
2942 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
2944 self.send_command(PluginCommand::AddMenu { menu, position })
2945 }
2946
2947 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
2949 self.send_command(PluginCommand::RemoveMenuItem {
2950 menu_label,
2951 item_label,
2952 })
2953 }
2954
2955 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
2957 self.send_command(PluginCommand::RemoveMenu { menu_label })
2958 }
2959
2960 pub fn create_virtual_buffer(
2967 &self,
2968 name: String,
2969 mode: String,
2970 read_only: bool,
2971 ) -> Result<(), String> {
2972 self.send_command(PluginCommand::CreateVirtualBuffer {
2973 name,
2974 mode,
2975 read_only,
2976 })
2977 }
2978
2979 pub fn create_virtual_buffer_with_content(
2985 &self,
2986 name: String,
2987 mode: String,
2988 read_only: bool,
2989 entries: Vec<TextPropertyEntry>,
2990 ) -> Result<(), String> {
2991 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
2992 name,
2993 mode,
2994 read_only,
2995 entries,
2996 show_line_numbers: true,
2997 show_cursors: true,
2998 editing_disabled: false,
2999 hidden_from_tabs: false,
3000 request_id: None,
3001 })
3002 }
3003
3004 pub fn set_virtual_buffer_content(
3008 &self,
3009 buffer_id: BufferId,
3010 entries: Vec<TextPropertyEntry>,
3011 ) -> Result<(), String> {
3012 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
3013 }
3014
3015 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
3019 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
3020 }
3021
3022 pub fn define_mode(
3026 &self,
3027 name: String,
3028 bindings: Vec<(String, String)>,
3029 read_only: bool,
3030 allow_text_input: bool,
3031 ) -> Result<(), String> {
3032 self.send_command(PluginCommand::DefineMode {
3033 name,
3034 bindings,
3035 read_only,
3036 allow_text_input,
3037 inherit_normal_bindings: false,
3038 plugin_name: None,
3039 })
3040 }
3041
3042 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
3044 self.send_command(PluginCommand::ShowBuffer { buffer_id })
3045 }
3046
3047 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
3049 self.send_command(PluginCommand::SetSplitScroll {
3050 split_id: SplitId(split_id),
3051 top_byte,
3052 })
3053 }
3054
3055 pub fn get_highlights(
3057 &self,
3058 buffer_id: BufferId,
3059 range: Range<usize>,
3060 request_id: u64,
3061 ) -> Result<(), String> {
3062 self.send_command(PluginCommand::RequestHighlights {
3063 buffer_id,
3064 range,
3065 request_id,
3066 })
3067 }
3068
3069 pub fn get_active_buffer_id(&self) -> BufferId {
3073 let snapshot = self.state_snapshot.read().unwrap();
3074 snapshot.active_buffer_id
3075 }
3076
3077 pub fn get_active_split_id(&self) -> usize {
3079 let snapshot = self.state_snapshot.read().unwrap();
3080 snapshot.active_split_id
3081 }
3082
3083 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
3085 let snapshot = self.state_snapshot.read().unwrap();
3086 snapshot.buffers.get(&buffer_id).cloned()
3087 }
3088
3089 pub fn list_buffers(&self) -> Vec<BufferInfo> {
3091 let snapshot = self.state_snapshot.read().unwrap();
3092 snapshot.buffers.values().cloned().collect()
3093 }
3094
3095 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
3097 let snapshot = self.state_snapshot.read().unwrap();
3098 snapshot.primary_cursor.clone()
3099 }
3100
3101 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
3103 let snapshot = self.state_snapshot.read().unwrap();
3104 snapshot.all_cursors.clone()
3105 }
3106
3107 pub fn get_viewport(&self) -> Option<ViewportInfo> {
3109 let snapshot = self.state_snapshot.read().unwrap();
3110 snapshot.viewport.clone()
3111 }
3112
3113 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
3115 Arc::clone(&self.state_snapshot)
3116 }
3117}
3118
3119impl Clone for PluginApi {
3120 fn clone(&self) -> Self {
3121 Self {
3122 hooks: Arc::clone(&self.hooks),
3123 commands: Arc::clone(&self.commands),
3124 command_sender: self.command_sender.clone(),
3125 state_snapshot: Arc::clone(&self.state_snapshot),
3126 }
3127 }
3128}
3129
3130#[derive(Debug, Clone, Serialize, Deserialize, TS)]
3144#[serde(rename_all = "camelCase", deny_unknown_fields)]
3145#[ts(export, rename_all = "camelCase")]
3146pub struct TsCompletionCandidate {
3147 pub label: String,
3149
3150 #[serde(skip_serializing_if = "Option::is_none")]
3152 pub insert_text: Option<String>,
3153
3154 #[serde(skip_serializing_if = "Option::is_none")]
3156 pub detail: Option<String>,
3157
3158 #[serde(skip_serializing_if = "Option::is_none")]
3160 pub icon: Option<String>,
3161
3162 #[serde(default)]
3164 pub score: i64,
3165
3166 #[serde(default)]
3168 pub is_snippet: bool,
3169
3170 #[serde(skip_serializing_if = "Option::is_none")]
3172 pub provider_data: Option<String>,
3173}
3174
3175#[derive(Debug, Clone, Serialize, Deserialize, TS)]
3180#[serde(rename_all = "camelCase")]
3181#[ts(export, rename_all = "camelCase")]
3182pub struct TsCompletionContext {
3183 pub prefix: String,
3185
3186 pub cursor_byte: usize,
3188
3189 pub word_start_byte: usize,
3191
3192 pub buffer_len: usize,
3194
3195 pub is_large_file: bool,
3197
3198 pub text_around_cursor: String,
3201
3202 pub cursor_offset_in_text: usize,
3204
3205 #[serde(skip_serializing_if = "Option::is_none")]
3207 pub language_id: Option<String>,
3208}
3209
3210#[derive(Debug, Clone, Serialize, Deserialize, TS)]
3212#[serde(rename_all = "camelCase", deny_unknown_fields)]
3213#[ts(export, rename_all = "camelCase")]
3214pub struct TsCompletionProviderRegistration {
3215 pub id: String,
3217
3218 pub display_name: String,
3220
3221 #[serde(default = "default_plugin_provider_priority")]
3224 pub priority: u32,
3225
3226 #[serde(default)]
3229 pub language_ids: Vec<String>,
3230}
3231
3232fn default_plugin_provider_priority() -> u32 {
3233 50
3234}
3235
3236#[cfg(test)]
3237mod tests {
3238 use super::*;
3239
3240 #[test]
3241 fn test_plugin_api_creation() {
3242 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3243 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3244 let (tx, _rx) = std::sync::mpsc::channel();
3245 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3246
3247 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3248
3249 let _clone = api.clone();
3251 }
3252
3253 #[test]
3254 fn test_register_hook() {
3255 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3256 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3257 let (tx, _rx) = std::sync::mpsc::channel();
3258 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3259
3260 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
3261
3262 api.register_hook("test-hook", Box::new(|_| true));
3263
3264 let hook_registry = hooks.read().unwrap();
3265 assert_eq!(hook_registry.hook_count("test-hook"), 1);
3266 }
3267
3268 #[test]
3269 fn test_send_command() {
3270 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3271 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3272 let (tx, rx) = std::sync::mpsc::channel();
3273 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3274
3275 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3276
3277 let result = api.insert_text(BufferId(1), 0, "test".to_string());
3278 assert!(result.is_ok());
3279
3280 let received = rx.try_recv();
3282 assert!(received.is_ok());
3283
3284 match received.unwrap() {
3285 PluginCommand::InsertText {
3286 buffer_id,
3287 position,
3288 text,
3289 } => {
3290 assert_eq!(buffer_id.0, 1);
3291 assert_eq!(position, 0);
3292 assert_eq!(text, "test");
3293 }
3294 _ => panic!("Wrong command type"),
3295 }
3296 }
3297
3298 #[test]
3299 fn test_add_overlay_command() {
3300 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3301 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3302 let (tx, rx) = std::sync::mpsc::channel();
3303 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3304
3305 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3306
3307 let result = api.add_overlay(
3308 BufferId(1),
3309 Some("test-overlay".to_string()),
3310 0..10,
3311 OverlayOptions {
3312 fg: Some(OverlayColorSpec::ThemeKey("ui.status_bar_fg".to_string())),
3313 bg: None,
3314 underline: true,
3315 bold: false,
3316 italic: false,
3317 strikethrough: false,
3318 extend_to_line_end: false,
3319 url: None,
3320 },
3321 );
3322 assert!(result.is_ok());
3323
3324 let received = rx.try_recv().unwrap();
3325 match received {
3326 PluginCommand::AddOverlay {
3327 buffer_id,
3328 namespace,
3329 range,
3330 options,
3331 } => {
3332 assert_eq!(buffer_id.0, 1);
3333 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
3334 assert_eq!(range, 0..10);
3335 assert!(matches!(
3336 options.fg,
3337 Some(OverlayColorSpec::ThemeKey(ref k)) if k == "ui.status_bar_fg"
3338 ));
3339 assert!(options.bg.is_none());
3340 assert!(options.underline);
3341 assert!(!options.bold);
3342 assert!(!options.italic);
3343 assert!(!options.extend_to_line_end);
3344 }
3345 _ => panic!("Wrong command type"),
3346 }
3347 }
3348
3349 #[test]
3350 fn test_set_status_command() {
3351 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3352 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3353 let (tx, rx) = std::sync::mpsc::channel();
3354 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3355
3356 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3357
3358 let result = api.set_status("Test status".to_string());
3359 assert!(result.is_ok());
3360
3361 let received = rx.try_recv().unwrap();
3362 match received {
3363 PluginCommand::SetStatus { message } => {
3364 assert_eq!(message, "Test status");
3365 }
3366 _ => panic!("Wrong command type"),
3367 }
3368 }
3369
3370 #[test]
3371 fn test_get_active_buffer_id() {
3372 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3373 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3374 let (tx, _rx) = std::sync::mpsc::channel();
3375 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3376
3377 {
3379 let mut snapshot = state_snapshot.write().unwrap();
3380 snapshot.active_buffer_id = BufferId(5);
3381 }
3382
3383 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3384
3385 let active_id = api.get_active_buffer_id();
3386 assert_eq!(active_id.0, 5);
3387 }
3388
3389 #[test]
3390 fn test_get_buffer_info() {
3391 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3392 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3393 let (tx, _rx) = std::sync::mpsc::channel();
3394 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3395
3396 {
3398 let mut snapshot = state_snapshot.write().unwrap();
3399 let buffer_info = BufferInfo {
3400 id: BufferId(1),
3401 path: Some(std::path::PathBuf::from("/test/file.txt")),
3402 modified: true,
3403 length: 100,
3404 is_virtual: false,
3405 view_mode: "source".to_string(),
3406 is_composing_in_any_split: false,
3407 compose_width: None,
3408 language: "text".to_string(),
3409 is_preview: false,
3410 };
3411 snapshot.buffers.insert(BufferId(1), buffer_info);
3412 }
3413
3414 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3415
3416 let info = api.get_buffer_info(BufferId(1));
3417 assert!(info.is_some());
3418 let info = info.unwrap();
3419 assert_eq!(info.id.0, 1);
3420 assert_eq!(
3421 info.path.as_ref().unwrap().to_str().unwrap(),
3422 "/test/file.txt"
3423 );
3424 assert!(info.modified);
3425 assert_eq!(info.length, 100);
3426
3427 let no_info = api.get_buffer_info(BufferId(999));
3429 assert!(no_info.is_none());
3430 }
3431
3432 #[test]
3433 fn test_list_buffers() {
3434 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3435 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3436 let (tx, _rx) = std::sync::mpsc::channel();
3437 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3438
3439 {
3441 let mut snapshot = state_snapshot.write().unwrap();
3442 snapshot.buffers.insert(
3443 BufferId(1),
3444 BufferInfo {
3445 id: BufferId(1),
3446 path: Some(std::path::PathBuf::from("/file1.txt")),
3447 modified: false,
3448 length: 50,
3449 is_virtual: false,
3450 view_mode: "source".to_string(),
3451 is_composing_in_any_split: false,
3452 compose_width: None,
3453 language: "text".to_string(),
3454 is_preview: false,
3455 },
3456 );
3457 snapshot.buffers.insert(
3458 BufferId(2),
3459 BufferInfo {
3460 id: BufferId(2),
3461 path: Some(std::path::PathBuf::from("/file2.txt")),
3462 modified: true,
3463 length: 100,
3464 is_virtual: false,
3465 view_mode: "source".to_string(),
3466 is_composing_in_any_split: false,
3467 compose_width: None,
3468 language: "text".to_string(),
3469 is_preview: false,
3470 },
3471 );
3472 snapshot.buffers.insert(
3473 BufferId(3),
3474 BufferInfo {
3475 id: BufferId(3),
3476 path: None,
3477 modified: false,
3478 length: 0,
3479 is_virtual: true,
3480 view_mode: "source".to_string(),
3481 is_composing_in_any_split: false,
3482 compose_width: None,
3483 language: "text".to_string(),
3484 is_preview: false,
3485 },
3486 );
3487 }
3488
3489 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3490
3491 let buffers = api.list_buffers();
3492 assert_eq!(buffers.len(), 3);
3493
3494 assert!(buffers.iter().any(|b| b.id.0 == 1));
3496 assert!(buffers.iter().any(|b| b.id.0 == 2));
3497 assert!(buffers.iter().any(|b| b.id.0 == 3));
3498 }
3499
3500 #[test]
3501 fn test_get_primary_cursor() {
3502 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3503 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3504 let (tx, _rx) = std::sync::mpsc::channel();
3505 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3506
3507 {
3509 let mut snapshot = state_snapshot.write().unwrap();
3510 snapshot.primary_cursor = Some(CursorInfo {
3511 position: 42,
3512 selection: Some(10..42),
3513 });
3514 }
3515
3516 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3517
3518 let cursor = api.get_primary_cursor();
3519 assert!(cursor.is_some());
3520 let cursor = cursor.unwrap();
3521 assert_eq!(cursor.position, 42);
3522 assert_eq!(cursor.selection, Some(10..42));
3523 }
3524
3525 #[test]
3526 fn test_get_all_cursors() {
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.all_cursors = vec![
3536 CursorInfo {
3537 position: 10,
3538 selection: None,
3539 },
3540 CursorInfo {
3541 position: 20,
3542 selection: Some(15..20),
3543 },
3544 CursorInfo {
3545 position: 30,
3546 selection: Some(25..30),
3547 },
3548 ];
3549 }
3550
3551 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3552
3553 let cursors = api.get_all_cursors();
3554 assert_eq!(cursors.len(), 3);
3555 assert_eq!(cursors[0].position, 10);
3556 assert_eq!(cursors[0].selection, None);
3557 assert_eq!(cursors[1].position, 20);
3558 assert_eq!(cursors[1].selection, Some(15..20));
3559 assert_eq!(cursors[2].position, 30);
3560 assert_eq!(cursors[2].selection, Some(25..30));
3561 }
3562
3563 #[test]
3564 fn test_get_viewport() {
3565 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3566 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3567 let (tx, _rx) = std::sync::mpsc::channel();
3568 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3569
3570 {
3572 let mut snapshot = state_snapshot.write().unwrap();
3573 snapshot.viewport = Some(ViewportInfo {
3574 top_byte: 100,
3575 top_line: Some(5),
3576 left_column: 5,
3577 width: 80,
3578 height: 24,
3579 });
3580 }
3581
3582 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3583
3584 let viewport = api.get_viewport();
3585 assert!(viewport.is_some());
3586 let viewport = viewport.unwrap();
3587 assert_eq!(viewport.top_byte, 100);
3588 assert_eq!(viewport.left_column, 5);
3589 assert_eq!(viewport.width, 80);
3590 assert_eq!(viewport.height, 24);
3591 }
3592
3593 #[test]
3594 fn test_composite_buffer_options_rejects_unknown_fields() {
3595 let valid_json = r#"{
3597 "name": "test",
3598 "mode": "diff",
3599 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3600 "sources": [{"bufferId": 1, "label": "old"}]
3601 }"#;
3602 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
3603 assert!(
3604 result.is_ok(),
3605 "Valid JSON should parse: {:?}",
3606 result.err()
3607 );
3608
3609 let invalid_json = r#"{
3611 "name": "test",
3612 "mode": "diff",
3613 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3614 "sources": [{"buffer_id": 1, "label": "old"}]
3615 }"#;
3616 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
3617 assert!(
3618 result.is_err(),
3619 "JSON with unknown field should fail to parse"
3620 );
3621 let err = result.unwrap_err().to_string();
3622 assert!(
3623 err.contains("unknown field") || err.contains("buffer_id"),
3624 "Error should mention unknown field: {}",
3625 err
3626 );
3627 }
3628
3629 #[test]
3630 fn test_composite_hunk_rejects_unknown_fields() {
3631 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3633 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
3634 assert!(
3635 result.is_ok(),
3636 "Valid JSON should parse: {:?}",
3637 result.err()
3638 );
3639
3640 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3642 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
3643 assert!(
3644 result.is_err(),
3645 "JSON with unknown field should fail to parse"
3646 );
3647 let err = result.unwrap_err().to_string();
3648 assert!(
3649 err.contains("unknown field") || err.contains("old_start"),
3650 "Error should mention unknown field: {}",
3651 err
3652 );
3653 }
3654
3655 #[test]
3656 fn test_plugin_response_line_end_position() {
3657 let response = PluginResponse::LineEndPosition {
3658 request_id: 42,
3659 position: Some(100),
3660 };
3661 let json = serde_json::to_string(&response).unwrap();
3662 assert!(json.contains("LineEndPosition"));
3663 assert!(json.contains("42"));
3664 assert!(json.contains("100"));
3665
3666 let response_none = PluginResponse::LineEndPosition {
3668 request_id: 1,
3669 position: None,
3670 };
3671 let json_none = serde_json::to_string(&response_none).unwrap();
3672 assert!(json_none.contains("null"));
3673 }
3674
3675 #[test]
3676 fn test_plugin_response_buffer_line_count() {
3677 let response = PluginResponse::BufferLineCount {
3678 request_id: 99,
3679 count: Some(500),
3680 };
3681 let json = serde_json::to_string(&response).unwrap();
3682 assert!(json.contains("BufferLineCount"));
3683 assert!(json.contains("99"));
3684 assert!(json.contains("500"));
3685 }
3686
3687 #[test]
3688 fn test_plugin_command_get_line_end_position() {
3689 let command = PluginCommand::GetLineEndPosition {
3690 buffer_id: BufferId(1),
3691 line: 10,
3692 request_id: 123,
3693 };
3694 let json = serde_json::to_string(&command).unwrap();
3695 assert!(json.contains("GetLineEndPosition"));
3696 assert!(json.contains("10"));
3697 }
3698
3699 #[test]
3700 fn test_plugin_command_get_buffer_line_count() {
3701 let command = PluginCommand::GetBufferLineCount {
3702 buffer_id: BufferId(0),
3703 request_id: 456,
3704 };
3705 let json = serde_json::to_string(&command).unwrap();
3706 assert!(json.contains("GetBufferLineCount"));
3707 assert!(json.contains("456"));
3708 }
3709
3710 #[test]
3711 fn test_plugin_command_scroll_to_line_center() {
3712 let command = PluginCommand::ScrollToLineCenter {
3713 split_id: SplitId(1),
3714 buffer_id: BufferId(2),
3715 line: 50,
3716 };
3717 let json = serde_json::to_string(&command).unwrap();
3718 assert!(json.contains("ScrollToLineCenter"));
3719 assert!(json.contains("50"));
3720 }
3721
3722 #[test]
3725 fn js_callback_id_conversions_and_display() {
3726 for raw in [0u64, 1, 42, u64::MAX] {
3727 let id = JsCallbackId::new(raw);
3728 assert_eq!(id.as_u64(), raw);
3729 assert_eq!(u64::from(id), raw);
3730 assert_eq!(JsCallbackId::from(raw), id);
3731 assert_eq!(id.to_string(), raw.to_string());
3732 }
3733 }
3734
3735 #[test]
3739 fn serde_defaults_fire_when_fields_are_omitted() {
3740 let spec: ActionSpec = serde_json::from_str(r#"{"action": "move_left"}"#).unwrap();
3742 assert_eq!(spec.count, 1);
3743 let spec: ActionSpec =
3744 serde_json::from_str(r#"{"action": "move_left", "count": 5}"#).unwrap();
3745 assert_eq!(spec.count, 5);
3746
3747 let layout: CompositeLayoutConfig =
3749 serde_json::from_str(r#"{"type": "side-by-side"}"#).unwrap();
3750 assert!(layout.show_separator);
3751 let layout: CompositeLayoutConfig =
3752 serde_json::from_str(r#"{"type": "side-by-side", "showSeparator": false}"#).unwrap();
3753 assert!(!layout.show_separator);
3754
3755 let reg: TsCompletionProviderRegistration =
3757 serde_json::from_str(r#"{"id": "p", "displayName": "P"}"#).unwrap();
3758 assert_eq!(reg.priority, 50);
3759 let reg: TsCompletionProviderRegistration =
3760 serde_json::from_str(r#"{"id": "p", "displayName": "P", "priority": 3}"#).unwrap();
3761 assert_eq!(reg.priority, 3);
3762 }
3763
3764 fn mk_cmd(name: &str) -> Command {
3772 Command {
3773 name: name.to_string(),
3774 description: String::new(),
3775 action_name: String::new(),
3776 plugin_name: String::new(),
3777 custom_contexts: Vec::new(),
3778 }
3779 }
3780
3781 #[test]
3788 fn command_registry_register_and_unregister_semantics() {
3789 let r = CommandRegistry::new();
3790
3791 r.register(mk_cmd("a"));
3792 r.register(mk_cmd("b"));
3793 assert_eq!(r.commands.read().unwrap().len(), 2);
3794
3795 r.register(mk_cmd("a"));
3798 let names: Vec<String> = r
3799 .commands
3800 .read()
3801 .unwrap()
3802 .iter()
3803 .map(|c| c.name.clone())
3804 .collect();
3805 assert_eq!(names, vec!["b".to_string(), "a".to_string()]);
3806
3807 r.unregister("a");
3810 let names: Vec<String> = r
3811 .commands
3812 .read()
3813 .unwrap()
3814 .iter()
3815 .map(|c| c.name.clone())
3816 .collect();
3817 assert_eq!(names, vec!["b".to_string()]);
3818
3819 r.unregister("nope");
3821 assert_eq!(r.commands.read().unwrap().len(), 1);
3822 }
3823
3824 #[test]
3830 fn overlay_color_spec_accessors_are_variant_specific() {
3831 let rgb = OverlayColorSpec::rgb(12, 34, 56);
3832 assert_eq!(rgb.as_rgb(), Some((12, 34, 56)));
3833 assert_eq!(rgb.as_theme_key(), None);
3834
3835 let tk = OverlayColorSpec::theme_key("ui.status_bar_bg");
3836 assert_eq!(tk.as_rgb(), None);
3837 assert_eq!(tk.as_theme_key(), Some("ui.status_bar_bg"));
3838 }
3839
3840 #[test]
3843 fn plugin_command_debug_variant_name_returns_real_variant() {
3844 let c = PluginCommand::SetStatus {
3845 message: "hi".into(),
3846 };
3847 assert_eq!(c.debug_variant_name(), "SetStatus");
3848
3849 let c2 = PluginCommand::InsertText {
3850 buffer_id: BufferId(1),
3851 position: 0,
3852 text: String::new(),
3853 };
3854 assert_eq!(c2.debug_variant_name(), "InsertText");
3855 }
3856
3857 fn mk_api() -> (
3865 PluginApi,
3866 std::sync::mpsc::Receiver<PluginCommand>,
3867 Arc<RwLock<HookRegistry>>,
3868 Arc<RwLock<CommandRegistry>>,
3869 Arc<RwLock<EditorStateSnapshot>>,
3870 ) {
3871 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3872 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3873 let (tx, rx) = std::sync::mpsc::channel();
3874 let snap = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3875 let api = PluginApi::new(hooks.clone(), commands.clone(), tx, snap.clone());
3876 (api, rx, hooks, commands, snap)
3877 }
3878
3879 #[test]
3882 fn plugin_api_unregister_hooks_clears_registry() {
3883 let (api, _rx, hooks, _cmds, _snap) = mk_api();
3884 api.register_hook("h", Box::new(|_| true));
3885 assert_eq!(hooks.read().unwrap().hook_count("h"), 1);
3886 api.unregister_hooks("h");
3887 assert_eq!(hooks.read().unwrap().hook_count("h"), 0);
3888 }
3889
3890 #[test]
3893 fn plugin_api_register_and_unregister_command_write_through() {
3894 let (api, _rx, _hooks, cmds, _snap) = mk_api();
3895
3896 api.register_command(mk_cmd("x"));
3897 assert_eq!(cmds.read().unwrap().commands.read().unwrap().len(), 1);
3898
3899 api.unregister_command("x");
3900 assert_eq!(cmds.read().unwrap().commands.read().unwrap().len(), 0);
3901 }
3902
3903 macro_rules! assert_dispatches {
3907 ($call:expr, $pattern:pat $(if $guard:expr)?) => {{
3908 let (api, rx, _h, _c, _s) = mk_api();
3909 let _ = $call(&api);
3910 match rx.try_recv().expect("no command sent") {
3911 $pattern $(if $guard)? => {}
3912 other => panic!("unexpected command variant: {:?}", other),
3913 }
3914 }};
3915 }
3916
3917 #[test]
3921 fn plugin_api_send_command_methods_dispatch_correctly() {
3922 assert_dispatches!(
3924 |a: &PluginApi| a.delete_range(BufferId(7), 3..9),
3925 PluginCommand::DeleteRange { buffer_id, range }
3926 if buffer_id == BufferId(7) && range == (3..9)
3927 );
3928
3929 assert_dispatches!(
3931 |a: &PluginApi| a.remove_overlay(BufferId(2), "h-1".into()),
3932 PluginCommand::RemoveOverlay { buffer_id, handle }
3933 if buffer_id == BufferId(2) && handle.as_str() == "h-1"
3934 );
3935
3936 assert_dispatches!(
3938 |a: &PluginApi| a.clear_namespace(BufferId(3), "diag".into()),
3939 PluginCommand::ClearNamespace { buffer_id, namespace }
3940 if buffer_id == BufferId(3) && namespace.as_str() == "diag"
3941 );
3942
3943 assert_dispatches!(
3945 |a: &PluginApi| a.clear_overlays_in_range(BufferId(4), 10, 20),
3946 PluginCommand::ClearOverlaysInRange { buffer_id, start, end }
3947 if buffer_id == BufferId(4) && start == 10 && end == 20
3948 );
3949
3950 assert_dispatches!(
3952 |a: &PluginApi| a.open_file_at_location(
3953 PathBuf::from("/tmp/x.rs"), Some(4), Some(8)
3954 ),
3955 PluginCommand::OpenFileAtLocation { path, line, column }
3956 if path == PathBuf::from("/tmp/x.rs")
3957 && line == Some(4)
3958 && column == Some(8)
3959 );
3960
3961 assert_dispatches!(
3963 |a: &PluginApi| a.open_file_in_split(
3964 2, PathBuf::from("/tmp/y.rs"), Some(5), None
3965 ),
3966 PluginCommand::OpenFileInSplit { split_id, path, line, column }
3967 if split_id == 2
3968 && path == PathBuf::from("/tmp/y.rs")
3969 && line == Some(5)
3970 && column.is_none()
3971 );
3972
3973 assert_dispatches!(
3975 |a: &PluginApi| a.start_prompt("label".into(), "cmd".into()),
3976 PluginCommand::StartPrompt { label, prompt_type }
3977 if label == "label" && prompt_type == "cmd"
3978 );
3979
3980 assert_dispatches!(
3982 |a: &PluginApi| a.set_prompt_suggestions(vec![
3983 Suggestion::new("one".into()),
3984 Suggestion::new("two".into()),
3985 ]),
3986 PluginCommand::SetPromptSuggestions { suggestions }
3987 if suggestions.len() == 2
3988 && suggestions[0].text == "one"
3989 && suggestions[1].text == "two"
3990 );
3991
3992 assert_dispatches!(
3994 |a: &PluginApi| a.set_prompt_input_sync(true),
3995 PluginCommand::SetPromptInputSync { sync } if sync
3996 );
3997 assert_dispatches!(
3998 |a: &PluginApi| a.set_prompt_input_sync(false),
3999 PluginCommand::SetPromptInputSync { sync } if !sync
4000 );
4001
4002 assert_dispatches!(
4004 |a: &PluginApi| a.add_menu_item(
4005 "File".into(),
4006 MenuItem::Label { info: "info".into() },
4007 MenuPosition::Bottom,
4008 ),
4009 PluginCommand::AddMenuItem { menu_label, item, position }
4010 if menu_label == "File"
4011 && matches!(item, MenuItem::Label { ref info } if info == "info")
4012 && matches!(position, MenuPosition::Bottom)
4013 );
4014
4015 assert_dispatches!(
4017 |a: &PluginApi| a.add_menu(
4018 Menu {
4019 id: None,
4020 label: "Help".into(),
4021 items: vec![],
4022 when: None,
4023 },
4024 MenuPosition::After("Edit".into()),
4025 ),
4026 PluginCommand::AddMenu { menu, position }
4027 if menu.label == "Help"
4028 && matches!(position, MenuPosition::After(ref s) if s == "Edit")
4029 );
4030
4031 assert_dispatches!(
4033 |a: &PluginApi| a.remove_menu_item("File".into(), "Open".into()),
4034 PluginCommand::RemoveMenuItem { menu_label, item_label }
4035 if menu_label == "File" && item_label == "Open"
4036 );
4037
4038 assert_dispatches!(
4040 |a: &PluginApi| a.remove_menu("File".into()),
4041 PluginCommand::RemoveMenu { menu_label } if menu_label == "File"
4042 );
4043
4044 assert_dispatches!(
4046 |a: &PluginApi| a.create_virtual_buffer("buf".into(), "mode".into(), true),
4047 PluginCommand::CreateVirtualBuffer { name, mode, read_only }
4048 if name == "buf" && mode == "mode" && read_only
4049 );
4050
4051 assert_dispatches!(
4053 |a: &PluginApi| a.create_virtual_buffer_with_content(
4054 "n".into(), "m".into(), false, vec![]
4055 ),
4056 PluginCommand::CreateVirtualBufferWithContent {
4057 name, mode, read_only, show_line_numbers, show_cursors,
4058 editing_disabled, hidden_from_tabs, request_id, ..
4059 }
4060 if name == "n" && mode == "m" && !read_only
4061 && show_line_numbers && show_cursors
4062 && !editing_disabled && !hidden_from_tabs
4063 && request_id.is_none()
4064 );
4065
4066 assert_dispatches!(
4068 |a: &PluginApi| a.set_virtual_buffer_content(BufferId(9), vec![]),
4069 PluginCommand::SetVirtualBufferContent { buffer_id, entries }
4070 if buffer_id == BufferId(9) && entries.is_empty()
4071 );
4072
4073 assert_dispatches!(
4075 |a: &PluginApi| a.get_text_properties_at_cursor(BufferId(11)),
4076 PluginCommand::GetTextPropertiesAtCursor { buffer_id }
4077 if buffer_id == BufferId(11)
4078 );
4079
4080 assert_dispatches!(
4082 |a: &PluginApi| a.define_mode(
4083 "m".into(),
4084 vec![("j".into(), "move_down".into())],
4085 true,
4086 false,
4087 ),
4088 PluginCommand::DefineMode {
4089 name, bindings, read_only, allow_text_input, inherit_normal_bindings, plugin_name
4090 }
4091 if name == "m"
4092 && bindings.len() == 1
4093 && bindings[0].0 == "j"
4094 && bindings[0].1 == "move_down"
4095 && read_only
4096 && !allow_text_input
4097 && !inherit_normal_bindings
4098 && plugin_name.is_none()
4099 );
4100
4101 assert_dispatches!(
4103 |a: &PluginApi| a.show_buffer(BufferId(77)),
4104 PluginCommand::ShowBuffer { buffer_id } if buffer_id == BufferId(77)
4105 );
4106
4107 assert_dispatches!(
4109 |a: &PluginApi| a.set_split_scroll(5, 128),
4110 PluginCommand::SetSplitScroll { split_id, top_byte }
4111 if split_id == SplitId(5) && top_byte == 128
4112 );
4113
4114 assert_dispatches!(
4116 |a: &PluginApi| a.get_highlights(BufferId(1), 0..10, 7),
4117 PluginCommand::RequestHighlights { buffer_id, range, request_id }
4118 if buffer_id == BufferId(1) && range == (0..10) && request_id == 7
4119 );
4120 }
4121
4122 #[test]
4125 fn plugin_api_get_active_split_id_reads_snapshot() {
4126 let (api, _rx, _h, _c, snap) = mk_api();
4127 snap.write().unwrap().active_split_id = 42;
4128 assert_eq!(api.get_active_split_id(), 42);
4129 }
4130
4131 #[test]
4135 fn plugin_api_state_snapshot_handle_shares_underlying_arc() {
4136 let (api, _rx, _h, _c, snap) = mk_api();
4137 snap.write().unwrap().active_buffer_id = BufferId(42);
4138
4139 let h = api.state_snapshot_handle();
4140 assert_eq!(h.read().unwrap().active_buffer_id, BufferId(42));
4141 assert!(Arc::ptr_eq(&h, &snap));
4142 }
4143}