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#[ts(export)]
170pub enum PluginResponse {
171 VirtualBufferCreated {
173 request_id: u64,
174 buffer_id: BufferId,
175 split_id: Option<SplitId>,
176 },
177 TerminalCreated {
179 request_id: u64,
180 buffer_id: BufferId,
181 terminal_id: TerminalId,
182 split_id: Option<SplitId>,
183 },
184 LspRequest {
186 request_id: u64,
187 #[ts(type = "any")]
188 result: Result<JsonValue, String>,
189 },
190 HighlightsComputed {
192 request_id: u64,
193 spans: Vec<TsHighlightSpan>,
194 },
195 BufferText {
197 request_id: u64,
198 text: Result<String, String>,
199 },
200 LineStartPosition {
202 request_id: u64,
203 position: Option<usize>,
205 },
206 LineEndPosition {
208 request_id: u64,
209 position: Option<usize>,
211 },
212 BufferLineCount {
214 request_id: u64,
215 count: Option<usize>,
217 },
218 CompositeBufferCreated {
220 request_id: u64,
221 buffer_id: BufferId,
222 },
223 SplitByLabel {
225 request_id: u64,
226 split_id: Option<SplitId>,
227 },
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize, TS)]
232#[ts(export)]
233pub enum PluginAsyncMessage {
234 ProcessOutput {
236 process_id: u64,
238 stdout: String,
240 stderr: String,
242 exit_code: i32,
244 },
245 DelayComplete {
247 callback_id: u64,
249 },
250 ProcessStdout { process_id: u64, data: String },
252 ProcessStderr { process_id: u64, data: String },
254 ProcessExit {
256 process_id: u64,
257 callback_id: u64,
258 exit_code: i32,
259 },
260 LspResponse {
262 language: String,
263 request_id: u64,
264 #[ts(type = "any")]
265 result: Result<JsonValue, String>,
266 },
267 PluginResponse(crate::api::PluginResponse),
269
270 GrepStreamingProgress {
272 search_id: u64,
274 matches_json: String,
276 },
277
278 GrepStreamingComplete {
280 search_id: u64,
282 callback_id: u64,
284 total_matches: usize,
286 truncated: bool,
288 },
289}
290
291#[derive(Debug, Clone, Serialize, Deserialize, TS)]
293#[ts(export)]
294pub struct CursorInfo {
295 pub position: usize,
297 #[cfg_attr(
299 feature = "plugins",
300 ts(type = "{ start: number; end: number } | null")
301 )]
302 pub selection: Option<Range<usize>>,
303}
304
305#[derive(Debug, Clone, Serialize, Deserialize, TS)]
307#[serde(deny_unknown_fields)]
308#[ts(export)]
309pub struct ActionSpec {
310 pub action: String,
312 #[serde(default = "default_action_count")]
314 pub count: u32,
315}
316
317fn default_action_count() -> u32 {
318 1
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize, TS)]
323#[ts(export)]
324pub struct BufferInfo {
325 #[ts(type = "number")]
327 pub id: BufferId,
328 #[serde(serialize_with = "serialize_path")]
330 #[ts(type = "string")]
331 pub path: Option<PathBuf>,
332 pub modified: bool,
334 pub length: usize,
336 pub is_virtual: bool,
338 pub view_mode: String,
340 pub is_composing_in_any_split: bool,
345 pub compose_width: Option<u16>,
347 pub language: String,
349}
350
351fn serialize_path<S: serde::Serializer>(path: &Option<PathBuf>, s: S) -> Result<S::Ok, S::Error> {
352 s.serialize_str(
353 &path
354 .as_ref()
355 .map(|p| p.to_string_lossy().to_string())
356 .unwrap_or_default(),
357 )
358}
359
360fn serialize_ranges_as_tuples<S>(ranges: &[Range<usize>], serializer: S) -> Result<S::Ok, S::Error>
362where
363 S: serde::Serializer,
364{
365 use serde::ser::SerializeSeq;
366 let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
367 for range in ranges {
368 seq.serialize_element(&(range.start, range.end))?;
369 }
370 seq.end()
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize, TS)]
375#[ts(export)]
376pub struct BufferSavedDiff {
377 pub equal: bool,
378 #[serde(serialize_with = "serialize_ranges_as_tuples")]
379 #[ts(type = "Array<[number, number]>")]
380 pub byte_ranges: Vec<Range<usize>>,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize, TS)]
385#[serde(rename_all = "camelCase")]
386#[ts(export, rename_all = "camelCase")]
387pub struct ViewportInfo {
388 pub top_byte: usize,
390 pub top_line: Option<usize>,
392 pub left_column: usize,
394 pub width: u16,
396 pub height: u16,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize, TS)]
402#[serde(rename_all = "camelCase")]
403#[ts(export, rename_all = "camelCase")]
404pub struct LayoutHints {
405 #[ts(optional)]
407 pub compose_width: Option<u16>,
408 #[ts(optional)]
410 pub column_guides: Option<Vec<u16>>,
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize, TS)]
428#[serde(untagged)]
429#[ts(export)]
430pub enum OverlayColorSpec {
431 #[ts(type = "[number, number, number]")]
433 Rgb(u8, u8, u8),
434 ThemeKey(String),
436}
437
438impl OverlayColorSpec {
439 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
441 Self::Rgb(r, g, b)
442 }
443
444 pub fn theme_key(key: impl Into<String>) -> Self {
446 Self::ThemeKey(key.into())
447 }
448
449 pub fn as_rgb(&self) -> Option<(u8, u8, u8)> {
451 match self {
452 Self::Rgb(r, g, b) => Some((*r, *g, *b)),
453 Self::ThemeKey(_) => None,
454 }
455 }
456
457 pub fn as_theme_key(&self) -> Option<&str> {
459 match self {
460 Self::ThemeKey(key) => Some(key),
461 Self::Rgb(_, _, _) => None,
462 }
463 }
464}
465
466#[derive(Debug, Clone, Serialize, Deserialize, TS)]
471#[serde(deny_unknown_fields, rename_all = "camelCase")]
472#[ts(export, rename_all = "camelCase")]
473#[derive(Default)]
474pub struct OverlayOptions {
475 #[serde(default, skip_serializing_if = "Option::is_none")]
477 pub fg: Option<OverlayColorSpec>,
478
479 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub bg: Option<OverlayColorSpec>,
482
483 #[serde(default)]
485 pub underline: bool,
486
487 #[serde(default)]
489 pub bold: bool,
490
491 #[serde(default)]
493 pub italic: bool,
494
495 #[serde(default)]
497 pub strikethrough: bool,
498
499 #[serde(default)]
501 pub extend_to_line_end: bool,
502
503 #[serde(default, skip_serializing_if = "Option::is_none")]
507 pub url: Option<String>,
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize, TS)]
516#[serde(deny_unknown_fields)]
517#[ts(export, rename = "TsCompositeLayoutConfig")]
518pub struct CompositeLayoutConfig {
519 #[serde(rename = "type")]
521 #[ts(rename = "type")]
522 pub layout_type: String,
523 #[serde(default)]
525 #[ts(optional)]
526 pub ratios: Option<Vec<f32>>,
527 #[serde(default = "default_true", rename = "showSeparator")]
529 #[ts(rename = "showSeparator")]
530 pub show_separator: bool,
531 #[serde(default)]
533 #[ts(optional)]
534 pub spacing: Option<u16>,
535}
536
537fn default_true() -> bool {
538 true
539}
540
541#[derive(Debug, Clone, Serialize, Deserialize, TS)]
543#[serde(deny_unknown_fields)]
544#[ts(export, rename = "TsCompositeSourceConfig")]
545pub struct CompositeSourceConfig {
546 #[serde(rename = "bufferId")]
548 #[ts(rename = "bufferId")]
549 pub buffer_id: usize,
550 pub label: String,
552 #[serde(default)]
554 pub editable: bool,
555 #[serde(default)]
557 pub style: Option<CompositePaneStyle>,
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
562#[serde(deny_unknown_fields)]
563#[ts(export, rename = "TsCompositePaneStyle")]
564pub struct CompositePaneStyle {
565 #[serde(default, rename = "addBg")]
568 #[ts(optional, rename = "addBg", type = "[number, number, number]")]
569 pub add_bg: Option<[u8; 3]>,
570 #[serde(default, rename = "removeBg")]
572 #[ts(optional, rename = "removeBg", type = "[number, number, number]")]
573 pub remove_bg: Option<[u8; 3]>,
574 #[serde(default, rename = "modifyBg")]
576 #[ts(optional, rename = "modifyBg", type = "[number, number, number]")]
577 pub modify_bg: Option<[u8; 3]>,
578 #[serde(default, rename = "gutterStyle")]
580 #[ts(optional, rename = "gutterStyle")]
581 pub gutter_style: Option<String>,
582}
583
584#[derive(Debug, Clone, Serialize, Deserialize, TS)]
586#[serde(deny_unknown_fields)]
587#[ts(export, rename = "TsCompositeHunk")]
588pub struct CompositeHunk {
589 #[serde(rename = "oldStart")]
591 #[ts(rename = "oldStart")]
592 pub old_start: usize,
593 #[serde(rename = "oldCount")]
595 #[ts(rename = "oldCount")]
596 pub old_count: usize,
597 #[serde(rename = "newStart")]
599 #[ts(rename = "newStart")]
600 pub new_start: usize,
601 #[serde(rename = "newCount")]
603 #[ts(rename = "newCount")]
604 pub new_count: usize,
605}
606
607#[derive(Debug, Clone, Serialize, Deserialize, TS)]
609#[serde(deny_unknown_fields)]
610#[ts(export, rename = "TsCreateCompositeBufferOptions")]
611pub struct CreateCompositeBufferOptions {
612 #[serde(default)]
614 pub name: String,
615 #[serde(default)]
617 pub mode: String,
618 pub layout: CompositeLayoutConfig,
620 pub sources: Vec<CompositeSourceConfig>,
622 #[serde(default)]
624 pub hunks: Option<Vec<CompositeHunk>>,
625}
626
627#[derive(Debug, Clone, Serialize, Deserialize, TS)]
629#[ts(export)]
630pub enum ViewTokenWireKind {
631 Text(String),
632 Newline,
633 Space,
634 Break,
637 BinaryByte(u8),
641}
642
643#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
649#[serde(deny_unknown_fields)]
650#[ts(export)]
651pub struct ViewTokenStyle {
652 #[serde(default)]
654 #[ts(type = "[number, number, number] | null")]
655 pub fg: Option<(u8, u8, u8)>,
656 #[serde(default)]
658 #[ts(type = "[number, number, number] | null")]
659 pub bg: Option<(u8, u8, u8)>,
660 #[serde(default)]
662 pub bold: bool,
663 #[serde(default)]
665 pub italic: bool,
666}
667
668#[derive(Debug, Clone, Serialize, Deserialize, TS)]
670#[serde(deny_unknown_fields)]
671#[ts(export)]
672pub struct ViewTokenWire {
673 #[ts(type = "number | null")]
675 pub source_offset: Option<usize>,
676 pub kind: ViewTokenWireKind,
678 #[serde(default)]
680 #[ts(optional)]
681 pub style: Option<ViewTokenStyle>,
682}
683
684#[derive(Debug, Clone, Serialize, Deserialize, TS)]
686#[ts(export)]
687pub struct ViewTransformPayload {
688 pub range: Range<usize>,
690 pub tokens: Vec<ViewTokenWire>,
692 pub layout_hints: Option<LayoutHints>,
694}
695
696#[derive(Debug, Clone, Serialize, Deserialize, TS)]
699#[ts(export)]
700pub struct EditorStateSnapshot {
701 pub active_buffer_id: BufferId,
703 pub active_split_id: usize,
705 pub buffers: HashMap<BufferId, BufferInfo>,
707 pub buffer_saved_diffs: HashMap<BufferId, BufferSavedDiff>,
709 pub primary_cursor: Option<CursorInfo>,
711 pub all_cursors: Vec<CursorInfo>,
713 pub viewport: Option<ViewportInfo>,
715 pub buffer_cursor_positions: HashMap<BufferId, usize>,
717 pub buffer_text_properties: HashMap<BufferId, Vec<TextProperty>>,
719 pub selected_text: Option<String>,
722 pub clipboard: String,
724 pub working_dir: PathBuf,
726 #[ts(type = "any")]
729 pub diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
730 #[ts(type = "any")]
733 pub folding_ranges: HashMap<String, Vec<lsp_types::FoldingRange>>,
734 #[ts(type = "any")]
737 pub config: serde_json::Value,
738 #[ts(type = "any")]
741 pub user_config: serde_json::Value,
742 #[ts(type = "GrammarInfo[]")]
744 pub available_grammars: Vec<GrammarInfoSnapshot>,
745 pub editor_mode: Option<String>,
748
749 #[ts(type = "any")]
753 pub plugin_view_states: HashMap<BufferId, HashMap<String, serde_json::Value>>,
754
755 #[serde(skip)]
758 #[ts(skip)]
759 pub plugin_view_states_split: usize,
760
761 #[serde(skip)]
764 #[ts(skip)]
765 pub keybinding_labels: HashMap<String, String>,
766
767 #[ts(type = "any")]
774 pub plugin_global_states: HashMap<String, HashMap<String, serde_json::Value>>,
775}
776
777impl EditorStateSnapshot {
778 pub fn new() -> Self {
779 Self {
780 active_buffer_id: BufferId(0),
781 active_split_id: 0,
782 buffers: HashMap::new(),
783 buffer_saved_diffs: HashMap::new(),
784 primary_cursor: None,
785 all_cursors: Vec::new(),
786 viewport: None,
787 buffer_cursor_positions: HashMap::new(),
788 buffer_text_properties: HashMap::new(),
789 selected_text: None,
790 clipboard: String::new(),
791 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
792 diagnostics: HashMap::new(),
793 folding_ranges: HashMap::new(),
794 config: serde_json::Value::Null,
795 user_config: serde_json::Value::Null,
796 available_grammars: Vec::new(),
797 editor_mode: None,
798 plugin_view_states: HashMap::new(),
799 plugin_view_states_split: 0,
800 keybinding_labels: HashMap::new(),
801 plugin_global_states: HashMap::new(),
802 }
803 }
804}
805
806impl Default for EditorStateSnapshot {
807 fn default() -> Self {
808 Self::new()
809 }
810}
811
812#[derive(Debug, Clone, Serialize, Deserialize, TS)]
814#[ts(export)]
815pub struct GrammarInfoSnapshot {
816 pub name: String,
818 pub source: String,
820 pub file_extensions: Vec<String>,
822}
823
824#[derive(Debug, Clone, Serialize, Deserialize, TS)]
826#[ts(export)]
827pub enum MenuPosition {
828 Top,
830 Bottom,
832 Before(String),
834 After(String),
836}
837
838#[derive(Debug, Clone, Serialize, Deserialize, TS)]
840#[ts(export)]
841pub enum PluginCommand {
842 InsertText {
844 buffer_id: BufferId,
845 position: usize,
846 text: String,
847 },
848
849 DeleteRange {
851 buffer_id: BufferId,
852 range: Range<usize>,
853 },
854
855 AddOverlay {
860 buffer_id: BufferId,
861 namespace: Option<OverlayNamespace>,
862 range: Range<usize>,
863 options: OverlayOptions,
865 },
866
867 RemoveOverlay {
869 buffer_id: BufferId,
870 handle: OverlayHandle,
871 },
872
873 SetStatus { message: String },
875
876 ApplyTheme { theme_name: String },
878
879 ReloadConfig,
882
883 RegisterCommand { command: Command },
885
886 UnregisterCommand { name: String },
888
889 OpenFileInBackground { path: PathBuf },
891
892 InsertAtCursor { text: String },
894
895 SpawnProcess {
897 command: String,
898 args: Vec<String>,
899 cwd: Option<String>,
900 callback_id: JsCallbackId,
901 },
902
903 Delay {
905 callback_id: JsCallbackId,
906 duration_ms: u64,
907 },
908
909 SpawnBackgroundProcess {
913 process_id: u64,
915 command: String,
917 args: Vec<String>,
919 cwd: Option<String>,
921 callback_id: JsCallbackId,
923 },
924
925 KillBackgroundProcess { process_id: u64 },
927
928 SpawnProcessWait {
931 process_id: u64,
933 callback_id: JsCallbackId,
935 },
936
937 SetLayoutHints {
939 buffer_id: BufferId,
940 split_id: Option<SplitId>,
941 range: Range<usize>,
942 hints: LayoutHints,
943 },
944
945 SetLineNumbers { buffer_id: BufferId, enabled: bool },
947
948 SetViewMode { buffer_id: BufferId, mode: String },
950
951 SetLineWrap {
953 buffer_id: BufferId,
954 split_id: Option<SplitId>,
955 enabled: bool,
956 },
957
958 SubmitViewTransform {
960 buffer_id: BufferId,
961 split_id: Option<SplitId>,
962 payload: ViewTransformPayload,
963 },
964
965 ClearViewTransform {
967 buffer_id: BufferId,
968 split_id: Option<SplitId>,
969 },
970
971 SetViewState {
974 buffer_id: BufferId,
975 key: String,
976 #[ts(type = "any")]
977 value: Option<serde_json::Value>,
978 },
979
980 SetGlobalState {
984 plugin_name: String,
985 key: String,
986 #[ts(type = "any")]
987 value: Option<serde_json::Value>,
988 },
989
990 ClearAllOverlays { buffer_id: BufferId },
992
993 ClearNamespace {
995 buffer_id: BufferId,
996 namespace: OverlayNamespace,
997 },
998
999 ClearOverlaysInRange {
1002 buffer_id: BufferId,
1003 start: usize,
1004 end: usize,
1005 },
1006
1007 AddVirtualText {
1010 buffer_id: BufferId,
1011 virtual_text_id: String,
1012 position: usize,
1013 text: String,
1014 color: (u8, u8, u8),
1015 use_bg: bool, before: bool, },
1018
1019 RemoveVirtualText {
1021 buffer_id: BufferId,
1022 virtual_text_id: String,
1023 },
1024
1025 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
1027
1028 ClearVirtualTexts { buffer_id: BufferId },
1030
1031 AddVirtualLine {
1035 buffer_id: BufferId,
1036 position: usize,
1038 text: String,
1040 fg_color: (u8, u8, u8),
1042 bg_color: Option<(u8, u8, u8)>,
1044 above: bool,
1046 namespace: String,
1048 priority: i32,
1050 },
1051
1052 ClearVirtualTextNamespace {
1055 buffer_id: BufferId,
1056 namespace: String,
1057 },
1058
1059 AddConceal {
1062 buffer_id: BufferId,
1063 namespace: OverlayNamespace,
1065 start: usize,
1067 end: usize,
1068 replacement: Option<String>,
1070 },
1071
1072 ClearConcealNamespace {
1074 buffer_id: BufferId,
1075 namespace: OverlayNamespace,
1076 },
1077
1078 ClearConcealsInRange {
1081 buffer_id: BufferId,
1082 start: usize,
1083 end: usize,
1084 },
1085
1086 AddSoftBreak {
1090 buffer_id: BufferId,
1091 namespace: OverlayNamespace,
1093 position: usize,
1095 indent: u16,
1097 },
1098
1099 ClearSoftBreakNamespace {
1101 buffer_id: BufferId,
1102 namespace: OverlayNamespace,
1103 },
1104
1105 ClearSoftBreaksInRange {
1107 buffer_id: BufferId,
1108 start: usize,
1109 end: usize,
1110 },
1111
1112 RefreshLines { buffer_id: BufferId },
1114
1115 RefreshAllLines,
1119
1120 HookCompleted { hook_name: String },
1124
1125 SetLineIndicator {
1128 buffer_id: BufferId,
1129 line: usize,
1131 namespace: String,
1133 symbol: String,
1135 color: (u8, u8, u8),
1137 priority: i32,
1139 },
1140
1141 SetLineIndicators {
1144 buffer_id: BufferId,
1145 lines: Vec<usize>,
1147 namespace: String,
1149 symbol: String,
1151 color: (u8, u8, u8),
1153 priority: i32,
1155 },
1156
1157 ClearLineIndicators {
1159 buffer_id: BufferId,
1160 namespace: String,
1162 },
1163
1164 SetFileExplorerDecorations {
1166 namespace: String,
1168 decorations: Vec<FileExplorerDecoration>,
1170 },
1171
1172 ClearFileExplorerDecorations {
1174 namespace: String,
1176 },
1177
1178 OpenFileAtLocation {
1181 path: PathBuf,
1182 line: Option<usize>, column: Option<usize>, },
1185
1186 OpenFileInSplit {
1189 split_id: usize,
1190 path: PathBuf,
1191 line: Option<usize>, column: Option<usize>, },
1194
1195 StartPrompt {
1198 label: String,
1199 prompt_type: String, },
1201
1202 StartPromptWithInitial {
1204 label: String,
1205 prompt_type: String,
1206 initial_value: String,
1207 },
1208
1209 StartPromptAsync {
1212 label: String,
1213 initial_value: String,
1214 callback_id: JsCallbackId,
1215 },
1216
1217 SetPromptSuggestions { suggestions: Vec<Suggestion> },
1220
1221 SetPromptInputSync { sync: bool },
1223
1224 AddMenuItem {
1227 menu_label: String,
1228 item: MenuItem,
1229 position: MenuPosition,
1230 },
1231
1232 AddMenu { menu: Menu, position: MenuPosition },
1234
1235 RemoveMenuItem {
1237 menu_label: String,
1238 item_label: String,
1239 },
1240
1241 RemoveMenu { menu_label: String },
1243
1244 CreateVirtualBuffer {
1246 name: String,
1248 mode: String,
1250 read_only: bool,
1252 },
1253
1254 CreateVirtualBufferWithContent {
1258 name: String,
1260 mode: String,
1262 read_only: bool,
1264 entries: Vec<TextPropertyEntry>,
1266 show_line_numbers: bool,
1268 show_cursors: bool,
1270 editing_disabled: bool,
1272 hidden_from_tabs: bool,
1274 request_id: Option<u64>,
1276 },
1277
1278 CreateVirtualBufferInSplit {
1281 name: String,
1283 mode: String,
1285 read_only: bool,
1287 entries: Vec<TextPropertyEntry>,
1289 ratio: f32,
1291 direction: Option<String>,
1293 panel_id: Option<String>,
1295 show_line_numbers: bool,
1297 show_cursors: bool,
1299 editing_disabled: bool,
1301 line_wrap: Option<bool>,
1303 before: bool,
1305 request_id: Option<u64>,
1307 },
1308
1309 SetVirtualBufferContent {
1311 buffer_id: BufferId,
1312 entries: Vec<TextPropertyEntry>,
1314 },
1315
1316 GetTextPropertiesAtCursor { buffer_id: BufferId },
1318
1319 DefineMode {
1321 name: String,
1322 bindings: Vec<(String, String)>, read_only: bool,
1324 allow_text_input: bool,
1326 plugin_name: Option<String>,
1328 },
1329
1330 ShowBuffer { buffer_id: BufferId },
1332
1333 CreateVirtualBufferInExistingSplit {
1335 name: String,
1337 mode: String,
1339 read_only: bool,
1341 entries: Vec<TextPropertyEntry>,
1343 split_id: SplitId,
1345 show_line_numbers: bool,
1347 show_cursors: bool,
1349 editing_disabled: bool,
1351 line_wrap: Option<bool>,
1353 request_id: Option<u64>,
1355 },
1356
1357 CloseBuffer { buffer_id: BufferId },
1359
1360 CreateCompositeBuffer {
1363 name: String,
1365 mode: String,
1367 layout: CompositeLayoutConfig,
1369 sources: Vec<CompositeSourceConfig>,
1371 hunks: Option<Vec<CompositeHunk>>,
1373 request_id: Option<u64>,
1375 },
1376
1377 UpdateCompositeAlignment {
1379 buffer_id: BufferId,
1380 hunks: Vec<CompositeHunk>,
1381 },
1382
1383 CloseCompositeBuffer { buffer_id: BufferId },
1385
1386 FocusSplit { split_id: SplitId },
1388
1389 SetSplitBuffer {
1391 split_id: SplitId,
1392 buffer_id: BufferId,
1393 },
1394
1395 SetSplitScroll { split_id: SplitId, top_byte: usize },
1397
1398 RequestHighlights {
1400 buffer_id: BufferId,
1401 range: Range<usize>,
1402 request_id: u64,
1403 },
1404
1405 CloseSplit { split_id: SplitId },
1407
1408 SetSplitRatio {
1410 split_id: SplitId,
1411 ratio: f32,
1413 },
1414
1415 SetSplitLabel { split_id: SplitId, label: String },
1417
1418 ClearSplitLabel { split_id: SplitId },
1420
1421 GetSplitByLabel { label: String, request_id: u64 },
1423
1424 DistributeSplitsEvenly {
1426 split_ids: Vec<SplitId>,
1428 },
1429
1430 SetBufferCursor {
1432 buffer_id: BufferId,
1433 position: usize,
1435 },
1436
1437 SendLspRequest {
1439 language: String,
1440 method: String,
1441 #[ts(type = "any")]
1442 params: Option<JsonValue>,
1443 request_id: u64,
1444 },
1445
1446 SetClipboard { text: String },
1448
1449 DeleteSelection,
1452
1453 SetContext {
1457 name: String,
1459 active: bool,
1461 },
1462
1463 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1465
1466 ExecuteAction {
1469 action_name: String,
1471 },
1472
1473 ExecuteActions {
1477 actions: Vec<ActionSpec>,
1479 },
1480
1481 GetBufferText {
1483 buffer_id: BufferId,
1485 start: usize,
1487 end: usize,
1489 request_id: u64,
1491 },
1492
1493 GetLineStartPosition {
1496 buffer_id: BufferId,
1498 line: u32,
1500 request_id: u64,
1502 },
1503
1504 GetLineEndPosition {
1508 buffer_id: BufferId,
1510 line: u32,
1512 request_id: u64,
1514 },
1515
1516 GetBufferLineCount {
1518 buffer_id: BufferId,
1520 request_id: u64,
1522 },
1523
1524 ScrollToLineCenter {
1527 split_id: SplitId,
1529 buffer_id: BufferId,
1531 line: usize,
1533 },
1534
1535 SetEditorMode {
1538 mode: Option<String>,
1540 },
1541
1542 ShowActionPopup {
1545 popup_id: String,
1547 title: String,
1549 message: String,
1551 actions: Vec<ActionPopupAction>,
1553 },
1554
1555 DisableLspForLanguage {
1557 language: String,
1559 },
1560
1561 RestartLspForLanguage {
1563 language: String,
1565 },
1566
1567 SetLspRootUri {
1571 language: String,
1573 uri: String,
1575 },
1576
1577 CreateScrollSyncGroup {
1581 group_id: u32,
1583 left_split: SplitId,
1585 right_split: SplitId,
1587 },
1588
1589 SetScrollSyncAnchors {
1592 group_id: u32,
1594 anchors: Vec<(usize, usize)>,
1596 },
1597
1598 RemoveScrollSyncGroup {
1600 group_id: u32,
1602 },
1603
1604 SaveBufferToPath {
1607 buffer_id: BufferId,
1609 path: PathBuf,
1611 },
1612
1613 LoadPlugin {
1616 path: PathBuf,
1618 callback_id: JsCallbackId,
1620 },
1621
1622 UnloadPlugin {
1625 name: String,
1627 callback_id: JsCallbackId,
1629 },
1630
1631 ReloadPlugin {
1634 name: String,
1636 callback_id: JsCallbackId,
1638 },
1639
1640 ListPlugins {
1643 callback_id: JsCallbackId,
1645 },
1646
1647 ReloadThemes { apply_theme: Option<String> },
1651
1652 RegisterGrammar {
1655 language: String,
1657 grammar_path: String,
1659 extensions: Vec<String>,
1661 },
1662
1663 RegisterLanguageConfig {
1666 language: String,
1668 config: LanguagePackConfig,
1670 },
1671
1672 RegisterLspServer {
1675 language: String,
1677 config: LspServerPackConfig,
1679 },
1680
1681 ReloadGrammars { callback_id: JsCallbackId },
1685
1686 CreateTerminal {
1690 cwd: Option<String>,
1692 direction: Option<String>,
1694 ratio: Option<f32>,
1696 focus: Option<bool>,
1698 request_id: u64,
1700 },
1701
1702 SendTerminalInput {
1704 terminal_id: TerminalId,
1706 data: String,
1708 },
1709
1710 CloseTerminal {
1712 terminal_id: TerminalId,
1714 },
1715
1716 GrepProject {
1720 pattern: String,
1722 fixed_string: bool,
1724 case_sensitive: bool,
1726 max_results: usize,
1728 whole_words: bool,
1730 callback_id: JsCallbackId,
1732 },
1733
1734 GrepProjectStreaming {
1739 pattern: String,
1741 fixed_string: bool,
1743 case_sensitive: bool,
1745 max_results: usize,
1747 whole_words: bool,
1749 search_id: u64,
1751 callback_id: JsCallbackId,
1753 },
1754
1755 ReplaceInBuffer {
1759 file_path: PathBuf,
1761 matches: Vec<(usize, usize)>,
1763 replacement: String,
1765 callback_id: JsCallbackId,
1767 },
1768}
1769
1770impl PluginCommand {
1771 pub fn debug_variant_name(&self) -> String {
1773 let dbg = format!("{:?}", self);
1774 dbg.split([' ', '{', '(']).next().unwrap_or("?").to_string()
1775 }
1776}
1777
1778#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
1787#[serde(rename_all = "camelCase")]
1788#[ts(export)]
1789pub struct LanguagePackConfig {
1790 #[serde(default)]
1792 pub comment_prefix: Option<String>,
1793
1794 #[serde(default)]
1796 pub block_comment_start: Option<String>,
1797
1798 #[serde(default)]
1800 pub block_comment_end: Option<String>,
1801
1802 #[serde(default)]
1804 pub use_tabs: Option<bool>,
1805
1806 #[serde(default)]
1808 pub tab_size: Option<usize>,
1809
1810 #[serde(default)]
1812 pub auto_indent: Option<bool>,
1813
1814 #[serde(default)]
1817 pub show_whitespace_tabs: Option<bool>,
1818
1819 #[serde(default)]
1821 pub formatter: Option<FormatterPackConfig>,
1822}
1823
1824#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1826#[serde(rename_all = "camelCase")]
1827#[ts(export)]
1828pub struct FormatterPackConfig {
1829 pub command: String,
1831
1832 #[serde(default)]
1834 pub args: Vec<String>,
1835}
1836
1837#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1839#[serde(rename_all = "camelCase")]
1840#[ts(export)]
1841pub struct ProcessLimitsPackConfig {
1842 #[serde(default)]
1844 pub max_memory_percent: Option<u32>,
1845
1846 #[serde(default)]
1848 pub max_cpu_percent: Option<u32>,
1849
1850 #[serde(default)]
1852 pub enabled: Option<bool>,
1853}
1854
1855#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1857#[serde(rename_all = "camelCase")]
1858#[ts(export)]
1859pub struct LspServerPackConfig {
1860 pub command: String,
1862
1863 #[serde(default)]
1865 pub args: Vec<String>,
1866
1867 #[serde(default)]
1869 pub auto_start: Option<bool>,
1870
1871 #[serde(default)]
1873 #[ts(type = "Record<string, unknown> | null")]
1874 pub initialization_options: Option<JsonValue>,
1875
1876 #[serde(default)]
1878 pub process_limits: Option<ProcessLimitsPackConfig>,
1879}
1880
1881#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1883#[ts(export)]
1884pub enum HunkStatus {
1885 Pending,
1886 Staged,
1887 Discarded,
1888}
1889
1890#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1892#[ts(export)]
1893pub struct ReviewHunk {
1894 pub id: String,
1895 pub file: String,
1896 pub context_header: String,
1897 pub status: HunkStatus,
1898 pub base_range: Option<(usize, usize)>,
1900 pub modified_range: Option<(usize, usize)>,
1902}
1903
1904#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1906#[serde(deny_unknown_fields)]
1907#[ts(export, rename = "TsActionPopupAction")]
1908pub struct ActionPopupAction {
1909 pub id: String,
1911 pub label: String,
1913}
1914
1915#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1917#[serde(deny_unknown_fields)]
1918#[ts(export)]
1919pub struct ActionPopupOptions {
1920 pub id: String,
1922 pub title: String,
1924 pub message: String,
1926 pub actions: Vec<ActionPopupAction>,
1928}
1929
1930#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1932#[ts(export)]
1933pub struct TsHighlightSpan {
1934 pub start: u32,
1935 pub end: u32,
1936 #[ts(type = "[number, number, number]")]
1937 pub color: (u8, u8, u8),
1938 pub bold: bool,
1939 pub italic: bool,
1940}
1941
1942#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1944#[ts(export)]
1945pub struct SpawnResult {
1946 pub stdout: String,
1948 pub stderr: String,
1950 pub exit_code: i32,
1952}
1953
1954#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1956#[ts(export)]
1957pub struct BackgroundProcessResult {
1958 #[ts(type = "number")]
1960 pub process_id: u64,
1961 pub exit_code: i32,
1964}
1965
1966#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1968#[serde(rename_all = "camelCase")]
1969#[ts(export, rename_all = "camelCase")]
1970pub struct GrepMatch {
1971 pub file: String,
1973 #[ts(type = "number")]
1975 pub buffer_id: usize,
1976 #[ts(type = "number")]
1978 pub byte_offset: usize,
1979 #[ts(type = "number")]
1981 pub length: usize,
1982 #[ts(type = "number")]
1984 pub line: usize,
1985 #[ts(type = "number")]
1987 pub column: usize,
1988 pub context: String,
1990}
1991
1992#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1994#[serde(rename_all = "camelCase")]
1995#[ts(export, rename_all = "camelCase")]
1996pub struct ReplaceResult {
1997 #[ts(type = "number")]
1999 pub replacements: usize,
2000 #[ts(type = "number")]
2002 pub buffer_id: usize,
2003}
2004
2005#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2007#[serde(deny_unknown_fields, rename_all = "camelCase")]
2008#[ts(export, rename = "TextPropertyEntry", rename_all = "camelCase")]
2009pub struct JsTextPropertyEntry {
2010 pub text: String,
2012 #[serde(default)]
2014 #[ts(optional, type = "Record<string, unknown>")]
2015 pub properties: Option<HashMap<String, JsonValue>>,
2016 #[serde(default)]
2018 #[ts(optional, type = "Partial<OverlayOptions>")]
2019 pub style: Option<OverlayOptions>,
2020 #[serde(default)]
2022 #[ts(optional)]
2023 pub inline_overlays: Option<Vec<crate::text_property::InlineOverlay>>,
2024}
2025
2026#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2028#[ts(export)]
2029pub struct DirEntry {
2030 pub name: String,
2032 pub is_file: bool,
2034 pub is_dir: bool,
2036}
2037
2038#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2040#[ts(export)]
2041pub struct JsPosition {
2042 pub line: u32,
2044 pub character: u32,
2046}
2047
2048#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2050#[ts(export)]
2051pub struct JsRange {
2052 pub start: JsPosition,
2054 pub end: JsPosition,
2056}
2057
2058#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2060#[ts(export)]
2061pub struct JsDiagnostic {
2062 pub uri: String,
2064 pub message: String,
2066 pub severity: Option<u8>,
2068 pub range: JsRange,
2070 #[ts(optional)]
2072 pub source: Option<String>,
2073}
2074
2075#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2077#[serde(deny_unknown_fields)]
2078#[ts(export)]
2079pub struct CreateVirtualBufferOptions {
2080 pub name: String,
2082 #[serde(default)]
2084 #[ts(optional)]
2085 pub mode: Option<String>,
2086 #[serde(default, rename = "readOnly")]
2088 #[ts(optional, rename = "readOnly")]
2089 pub read_only: Option<bool>,
2090 #[serde(default, rename = "showLineNumbers")]
2092 #[ts(optional, rename = "showLineNumbers")]
2093 pub show_line_numbers: Option<bool>,
2094 #[serde(default, rename = "showCursors")]
2096 #[ts(optional, rename = "showCursors")]
2097 pub show_cursors: Option<bool>,
2098 #[serde(default, rename = "editingDisabled")]
2100 #[ts(optional, rename = "editingDisabled")]
2101 pub editing_disabled: Option<bool>,
2102 #[serde(default, rename = "hiddenFromTabs")]
2104 #[ts(optional, rename = "hiddenFromTabs")]
2105 pub hidden_from_tabs: Option<bool>,
2106 #[serde(default)]
2108 #[ts(optional)]
2109 pub entries: Option<Vec<JsTextPropertyEntry>>,
2110}
2111
2112#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2114#[serde(deny_unknown_fields)]
2115#[ts(export)]
2116pub struct CreateVirtualBufferInSplitOptions {
2117 pub name: String,
2119 #[serde(default)]
2121 #[ts(optional)]
2122 pub mode: Option<String>,
2123 #[serde(default, rename = "readOnly")]
2125 #[ts(optional, rename = "readOnly")]
2126 pub read_only: Option<bool>,
2127 #[serde(default)]
2129 #[ts(optional)]
2130 pub ratio: Option<f32>,
2131 #[serde(default)]
2133 #[ts(optional)]
2134 pub direction: Option<String>,
2135 #[serde(default, rename = "panelId")]
2137 #[ts(optional, rename = "panelId")]
2138 pub panel_id: Option<String>,
2139 #[serde(default, rename = "showLineNumbers")]
2141 #[ts(optional, rename = "showLineNumbers")]
2142 pub show_line_numbers: Option<bool>,
2143 #[serde(default, rename = "showCursors")]
2145 #[ts(optional, rename = "showCursors")]
2146 pub show_cursors: Option<bool>,
2147 #[serde(default, rename = "editingDisabled")]
2149 #[ts(optional, rename = "editingDisabled")]
2150 pub editing_disabled: Option<bool>,
2151 #[serde(default, rename = "lineWrap")]
2153 #[ts(optional, rename = "lineWrap")]
2154 pub line_wrap: Option<bool>,
2155 #[serde(default)]
2157 #[ts(optional)]
2158 pub before: Option<bool>,
2159 #[serde(default)]
2161 #[ts(optional)]
2162 pub entries: Option<Vec<JsTextPropertyEntry>>,
2163}
2164
2165#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2167#[serde(deny_unknown_fields)]
2168#[ts(export)]
2169pub struct CreateVirtualBufferInExistingSplitOptions {
2170 pub name: String,
2172 #[serde(rename = "splitId")]
2174 #[ts(rename = "splitId")]
2175 pub split_id: usize,
2176 #[serde(default)]
2178 #[ts(optional)]
2179 pub mode: Option<String>,
2180 #[serde(default, rename = "readOnly")]
2182 #[ts(optional, rename = "readOnly")]
2183 pub read_only: Option<bool>,
2184 #[serde(default, rename = "showLineNumbers")]
2186 #[ts(optional, rename = "showLineNumbers")]
2187 pub show_line_numbers: Option<bool>,
2188 #[serde(default, rename = "showCursors")]
2190 #[ts(optional, rename = "showCursors")]
2191 pub show_cursors: Option<bool>,
2192 #[serde(default, rename = "editingDisabled")]
2194 #[ts(optional, rename = "editingDisabled")]
2195 pub editing_disabled: Option<bool>,
2196 #[serde(default, rename = "lineWrap")]
2198 #[ts(optional, rename = "lineWrap")]
2199 pub line_wrap: Option<bool>,
2200 #[serde(default)]
2202 #[ts(optional)]
2203 pub entries: Option<Vec<JsTextPropertyEntry>>,
2204}
2205
2206#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2208#[serde(deny_unknown_fields)]
2209#[ts(export)]
2210pub struct CreateTerminalOptions {
2211 #[serde(default)]
2213 #[ts(optional)]
2214 pub cwd: Option<String>,
2215 #[serde(default)]
2217 #[ts(optional)]
2218 pub direction: Option<String>,
2219 #[serde(default)]
2221 #[ts(optional)]
2222 pub ratio: Option<f32>,
2223 #[serde(default)]
2225 #[ts(optional)]
2226 pub focus: Option<bool>,
2227}
2228
2229#[derive(Debug, Clone, Serialize, TS)]
2234#[ts(export, type = "Array<Record<string, unknown>>")]
2235pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
2236
2237#[cfg(feature = "plugins")]
2239mod fromjs_impls {
2240 use super::*;
2241 use rquickjs::{Ctx, FromJs, Value};
2242
2243 impl<'js> FromJs<'js> for JsTextPropertyEntry {
2244 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2245 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2246 from: "object",
2247 to: "JsTextPropertyEntry",
2248 message: Some(e.to_string()),
2249 })
2250 }
2251 }
2252
2253 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
2254 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2255 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2256 from: "object",
2257 to: "CreateVirtualBufferOptions",
2258 message: Some(e.to_string()),
2259 })
2260 }
2261 }
2262
2263 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
2264 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2265 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2266 from: "object",
2267 to: "CreateVirtualBufferInSplitOptions",
2268 message: Some(e.to_string()),
2269 })
2270 }
2271 }
2272
2273 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
2274 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2275 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2276 from: "object",
2277 to: "CreateVirtualBufferInExistingSplitOptions",
2278 message: Some(e.to_string()),
2279 })
2280 }
2281 }
2282
2283 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
2284 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
2285 rquickjs_serde::to_value(ctx.clone(), &self.0)
2286 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
2287 }
2288 }
2289
2290 impl<'js> FromJs<'js> for ActionSpec {
2293 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2294 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2295 from: "object",
2296 to: "ActionSpec",
2297 message: Some(e.to_string()),
2298 })
2299 }
2300 }
2301
2302 impl<'js> FromJs<'js> for ActionPopupAction {
2303 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2304 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2305 from: "object",
2306 to: "ActionPopupAction",
2307 message: Some(e.to_string()),
2308 })
2309 }
2310 }
2311
2312 impl<'js> FromJs<'js> for ActionPopupOptions {
2313 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2314 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2315 from: "object",
2316 to: "ActionPopupOptions",
2317 message: Some(e.to_string()),
2318 })
2319 }
2320 }
2321
2322 impl<'js> FromJs<'js> for ViewTokenWire {
2323 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2324 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2325 from: "object",
2326 to: "ViewTokenWire",
2327 message: Some(e.to_string()),
2328 })
2329 }
2330 }
2331
2332 impl<'js> FromJs<'js> for ViewTokenStyle {
2333 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2334 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2335 from: "object",
2336 to: "ViewTokenStyle",
2337 message: Some(e.to_string()),
2338 })
2339 }
2340 }
2341
2342 impl<'js> FromJs<'js> for LayoutHints {
2343 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2344 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2345 from: "object",
2346 to: "LayoutHints",
2347 message: Some(e.to_string()),
2348 })
2349 }
2350 }
2351
2352 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
2353 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2354 let json: serde_json::Value =
2356 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2357 from: "object",
2358 to: "CreateCompositeBufferOptions (json)",
2359 message: Some(e.to_string()),
2360 })?;
2361 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
2362 from: "json",
2363 to: "CreateCompositeBufferOptions",
2364 message: Some(e.to_string()),
2365 })
2366 }
2367 }
2368
2369 impl<'js> FromJs<'js> for CompositeHunk {
2370 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2371 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2372 from: "object",
2373 to: "CompositeHunk",
2374 message: Some(e.to_string()),
2375 })
2376 }
2377 }
2378
2379 impl<'js> FromJs<'js> for LanguagePackConfig {
2380 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2381 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2382 from: "object",
2383 to: "LanguagePackConfig",
2384 message: Some(e.to_string()),
2385 })
2386 }
2387 }
2388
2389 impl<'js> FromJs<'js> for LspServerPackConfig {
2390 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2391 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2392 from: "object",
2393 to: "LspServerPackConfig",
2394 message: Some(e.to_string()),
2395 })
2396 }
2397 }
2398
2399 impl<'js> FromJs<'js> for ProcessLimitsPackConfig {
2400 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2401 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2402 from: "object",
2403 to: "ProcessLimitsPackConfig",
2404 message: Some(e.to_string()),
2405 })
2406 }
2407 }
2408
2409 impl<'js> FromJs<'js> for CreateTerminalOptions {
2410 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2411 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2412 from: "object",
2413 to: "CreateTerminalOptions",
2414 message: Some(e.to_string()),
2415 })
2416 }
2417 }
2418}
2419
2420pub struct PluginApi {
2422 hooks: Arc<RwLock<HookRegistry>>,
2424
2425 commands: Arc<RwLock<CommandRegistry>>,
2427
2428 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2430
2431 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2433}
2434
2435impl PluginApi {
2436 pub fn new(
2438 hooks: Arc<RwLock<HookRegistry>>,
2439 commands: Arc<RwLock<CommandRegistry>>,
2440 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2441 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2442 ) -> Self {
2443 Self {
2444 hooks,
2445 commands,
2446 command_sender,
2447 state_snapshot,
2448 }
2449 }
2450
2451 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
2453 let mut hooks = self.hooks.write().unwrap();
2454 hooks.add_hook(hook_name, callback);
2455 }
2456
2457 pub fn unregister_hooks(&self, hook_name: &str) {
2459 let mut hooks = self.hooks.write().unwrap();
2460 hooks.remove_hooks(hook_name);
2461 }
2462
2463 pub fn register_command(&self, command: Command) {
2465 let commands = self.commands.read().unwrap();
2466 commands.register(command);
2467 }
2468
2469 pub fn unregister_command(&self, name: &str) {
2471 let commands = self.commands.read().unwrap();
2472 commands.unregister(name);
2473 }
2474
2475 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
2477 self.command_sender
2478 .send(command)
2479 .map_err(|e| format!("Failed to send command: {}", e))
2480 }
2481
2482 pub fn insert_text(
2484 &self,
2485 buffer_id: BufferId,
2486 position: usize,
2487 text: String,
2488 ) -> Result<(), String> {
2489 self.send_command(PluginCommand::InsertText {
2490 buffer_id,
2491 position,
2492 text,
2493 })
2494 }
2495
2496 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
2498 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
2499 }
2500
2501 pub fn add_overlay(
2509 &self,
2510 buffer_id: BufferId,
2511 namespace: Option<String>,
2512 range: Range<usize>,
2513 options: OverlayOptions,
2514 ) -> Result<(), String> {
2515 self.send_command(PluginCommand::AddOverlay {
2516 buffer_id,
2517 namespace: namespace.map(OverlayNamespace::from_string),
2518 range,
2519 options,
2520 })
2521 }
2522
2523 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
2525 self.send_command(PluginCommand::RemoveOverlay {
2526 buffer_id,
2527 handle: OverlayHandle::from_string(handle),
2528 })
2529 }
2530
2531 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
2533 self.send_command(PluginCommand::ClearNamespace {
2534 buffer_id,
2535 namespace: OverlayNamespace::from_string(namespace),
2536 })
2537 }
2538
2539 pub fn clear_overlays_in_range(
2542 &self,
2543 buffer_id: BufferId,
2544 start: usize,
2545 end: usize,
2546 ) -> Result<(), String> {
2547 self.send_command(PluginCommand::ClearOverlaysInRange {
2548 buffer_id,
2549 start,
2550 end,
2551 })
2552 }
2553
2554 pub fn set_status(&self, message: String) -> Result<(), String> {
2556 self.send_command(PluginCommand::SetStatus { message })
2557 }
2558
2559 pub fn open_file_at_location(
2562 &self,
2563 path: PathBuf,
2564 line: Option<usize>,
2565 column: Option<usize>,
2566 ) -> Result<(), String> {
2567 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
2568 }
2569
2570 pub fn open_file_in_split(
2575 &self,
2576 split_id: usize,
2577 path: PathBuf,
2578 line: Option<usize>,
2579 column: Option<usize>,
2580 ) -> Result<(), String> {
2581 self.send_command(PluginCommand::OpenFileInSplit {
2582 split_id,
2583 path,
2584 line,
2585 column,
2586 })
2587 }
2588
2589 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
2592 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
2593 }
2594
2595 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
2598 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
2599 }
2600
2601 pub fn set_prompt_input_sync(&self, sync: bool) -> Result<(), String> {
2603 self.send_command(PluginCommand::SetPromptInputSync { sync })
2604 }
2605
2606 pub fn add_menu_item(
2608 &self,
2609 menu_label: String,
2610 item: MenuItem,
2611 position: MenuPosition,
2612 ) -> Result<(), String> {
2613 self.send_command(PluginCommand::AddMenuItem {
2614 menu_label,
2615 item,
2616 position,
2617 })
2618 }
2619
2620 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
2622 self.send_command(PluginCommand::AddMenu { menu, position })
2623 }
2624
2625 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
2627 self.send_command(PluginCommand::RemoveMenuItem {
2628 menu_label,
2629 item_label,
2630 })
2631 }
2632
2633 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
2635 self.send_command(PluginCommand::RemoveMenu { menu_label })
2636 }
2637
2638 pub fn create_virtual_buffer(
2645 &self,
2646 name: String,
2647 mode: String,
2648 read_only: bool,
2649 ) -> Result<(), String> {
2650 self.send_command(PluginCommand::CreateVirtualBuffer {
2651 name,
2652 mode,
2653 read_only,
2654 })
2655 }
2656
2657 pub fn create_virtual_buffer_with_content(
2663 &self,
2664 name: String,
2665 mode: String,
2666 read_only: bool,
2667 entries: Vec<TextPropertyEntry>,
2668 ) -> Result<(), String> {
2669 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
2670 name,
2671 mode,
2672 read_only,
2673 entries,
2674 show_line_numbers: true,
2675 show_cursors: true,
2676 editing_disabled: false,
2677 hidden_from_tabs: false,
2678 request_id: None,
2679 })
2680 }
2681
2682 pub fn set_virtual_buffer_content(
2686 &self,
2687 buffer_id: BufferId,
2688 entries: Vec<TextPropertyEntry>,
2689 ) -> Result<(), String> {
2690 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
2691 }
2692
2693 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
2697 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
2698 }
2699
2700 pub fn define_mode(
2704 &self,
2705 name: String,
2706 bindings: Vec<(String, String)>,
2707 read_only: bool,
2708 allow_text_input: bool,
2709 ) -> Result<(), String> {
2710 self.send_command(PluginCommand::DefineMode {
2711 name,
2712 bindings,
2713 read_only,
2714 allow_text_input,
2715 plugin_name: None,
2716 })
2717 }
2718
2719 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
2721 self.send_command(PluginCommand::ShowBuffer { buffer_id })
2722 }
2723
2724 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
2726 self.send_command(PluginCommand::SetSplitScroll {
2727 split_id: SplitId(split_id),
2728 top_byte,
2729 })
2730 }
2731
2732 pub fn get_highlights(
2734 &self,
2735 buffer_id: BufferId,
2736 range: Range<usize>,
2737 request_id: u64,
2738 ) -> Result<(), String> {
2739 self.send_command(PluginCommand::RequestHighlights {
2740 buffer_id,
2741 range,
2742 request_id,
2743 })
2744 }
2745
2746 pub fn get_active_buffer_id(&self) -> BufferId {
2750 let snapshot = self.state_snapshot.read().unwrap();
2751 snapshot.active_buffer_id
2752 }
2753
2754 pub fn get_active_split_id(&self) -> usize {
2756 let snapshot = self.state_snapshot.read().unwrap();
2757 snapshot.active_split_id
2758 }
2759
2760 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
2762 let snapshot = self.state_snapshot.read().unwrap();
2763 snapshot.buffers.get(&buffer_id).cloned()
2764 }
2765
2766 pub fn list_buffers(&self) -> Vec<BufferInfo> {
2768 let snapshot = self.state_snapshot.read().unwrap();
2769 snapshot.buffers.values().cloned().collect()
2770 }
2771
2772 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
2774 let snapshot = self.state_snapshot.read().unwrap();
2775 snapshot.primary_cursor.clone()
2776 }
2777
2778 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
2780 let snapshot = self.state_snapshot.read().unwrap();
2781 snapshot.all_cursors.clone()
2782 }
2783
2784 pub fn get_viewport(&self) -> Option<ViewportInfo> {
2786 let snapshot = self.state_snapshot.read().unwrap();
2787 snapshot.viewport.clone()
2788 }
2789
2790 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
2792 Arc::clone(&self.state_snapshot)
2793 }
2794}
2795
2796impl Clone for PluginApi {
2797 fn clone(&self) -> Self {
2798 Self {
2799 hooks: Arc::clone(&self.hooks),
2800 commands: Arc::clone(&self.commands),
2801 command_sender: self.command_sender.clone(),
2802 state_snapshot: Arc::clone(&self.state_snapshot),
2803 }
2804 }
2805}
2806
2807#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2821#[serde(rename_all = "camelCase", deny_unknown_fields)]
2822#[ts(export, rename_all = "camelCase")]
2823pub struct TsCompletionCandidate {
2824 pub label: String,
2826
2827 #[serde(skip_serializing_if = "Option::is_none")]
2829 pub insert_text: Option<String>,
2830
2831 #[serde(skip_serializing_if = "Option::is_none")]
2833 pub detail: Option<String>,
2834
2835 #[serde(skip_serializing_if = "Option::is_none")]
2837 pub icon: Option<String>,
2838
2839 #[serde(default)]
2841 pub score: i64,
2842
2843 #[serde(default)]
2845 pub is_snippet: bool,
2846
2847 #[serde(skip_serializing_if = "Option::is_none")]
2849 pub provider_data: Option<String>,
2850}
2851
2852#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2857#[serde(rename_all = "camelCase")]
2858#[ts(export, rename_all = "camelCase")]
2859pub struct TsCompletionContext {
2860 pub prefix: String,
2862
2863 pub cursor_byte: usize,
2865
2866 pub word_start_byte: usize,
2868
2869 pub buffer_len: usize,
2871
2872 pub is_large_file: bool,
2874
2875 pub text_around_cursor: String,
2878
2879 pub cursor_offset_in_text: usize,
2881
2882 #[serde(skip_serializing_if = "Option::is_none")]
2884 pub language_id: Option<String>,
2885}
2886
2887#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2889#[serde(rename_all = "camelCase", deny_unknown_fields)]
2890#[ts(export, rename_all = "camelCase")]
2891pub struct TsCompletionProviderRegistration {
2892 pub id: String,
2894
2895 pub display_name: String,
2897
2898 #[serde(default = "default_plugin_provider_priority")]
2901 pub priority: u32,
2902
2903 #[serde(default)]
2906 pub language_ids: Vec<String>,
2907}
2908
2909fn default_plugin_provider_priority() -> u32 {
2910 50
2911}
2912
2913#[cfg(test)]
2914mod tests {
2915 use super::*;
2916
2917 #[test]
2918 fn test_plugin_api_creation() {
2919 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2920 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2921 let (tx, _rx) = std::sync::mpsc::channel();
2922 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2923
2924 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2925
2926 let _clone = api.clone();
2928 }
2929
2930 #[test]
2931 fn test_register_hook() {
2932 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2933 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2934 let (tx, _rx) = std::sync::mpsc::channel();
2935 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2936
2937 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
2938
2939 api.register_hook("test-hook", Box::new(|_| true));
2940
2941 let hook_registry = hooks.read().unwrap();
2942 assert_eq!(hook_registry.hook_count("test-hook"), 1);
2943 }
2944
2945 #[test]
2946 fn test_send_command() {
2947 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2948 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2949 let (tx, rx) = std::sync::mpsc::channel();
2950 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2951
2952 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2953
2954 let result = api.insert_text(BufferId(1), 0, "test".to_string());
2955 assert!(result.is_ok());
2956
2957 let received = rx.try_recv();
2959 assert!(received.is_ok());
2960
2961 match received.unwrap() {
2962 PluginCommand::InsertText {
2963 buffer_id,
2964 position,
2965 text,
2966 } => {
2967 assert_eq!(buffer_id.0, 1);
2968 assert_eq!(position, 0);
2969 assert_eq!(text, "test");
2970 }
2971 _ => panic!("Wrong command type"),
2972 }
2973 }
2974
2975 #[test]
2976 fn test_add_overlay_command() {
2977 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2978 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2979 let (tx, rx) = std::sync::mpsc::channel();
2980 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2981
2982 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2983
2984 let result = api.add_overlay(
2985 BufferId(1),
2986 Some("test-overlay".to_string()),
2987 0..10,
2988 OverlayOptions {
2989 fg: Some(OverlayColorSpec::ThemeKey("ui.status_bar_fg".to_string())),
2990 bg: None,
2991 underline: true,
2992 bold: false,
2993 italic: false,
2994 strikethrough: false,
2995 extend_to_line_end: false,
2996 url: None,
2997 },
2998 );
2999 assert!(result.is_ok());
3000
3001 let received = rx.try_recv().unwrap();
3002 match received {
3003 PluginCommand::AddOverlay {
3004 buffer_id,
3005 namespace,
3006 range,
3007 options,
3008 } => {
3009 assert_eq!(buffer_id.0, 1);
3010 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
3011 assert_eq!(range, 0..10);
3012 assert!(matches!(
3013 options.fg,
3014 Some(OverlayColorSpec::ThemeKey(ref k)) if k == "ui.status_bar_fg"
3015 ));
3016 assert!(options.bg.is_none());
3017 assert!(options.underline);
3018 assert!(!options.bold);
3019 assert!(!options.italic);
3020 assert!(!options.extend_to_line_end);
3021 }
3022 _ => panic!("Wrong command type"),
3023 }
3024 }
3025
3026 #[test]
3027 fn test_set_status_command() {
3028 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3029 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3030 let (tx, rx) = std::sync::mpsc::channel();
3031 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3032
3033 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3034
3035 let result = api.set_status("Test status".to_string());
3036 assert!(result.is_ok());
3037
3038 let received = rx.try_recv().unwrap();
3039 match received {
3040 PluginCommand::SetStatus { message } => {
3041 assert_eq!(message, "Test status");
3042 }
3043 _ => panic!("Wrong command type"),
3044 }
3045 }
3046
3047 #[test]
3048 fn test_get_active_buffer_id() {
3049 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3050 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3051 let (tx, _rx) = std::sync::mpsc::channel();
3052 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3053
3054 {
3056 let mut snapshot = state_snapshot.write().unwrap();
3057 snapshot.active_buffer_id = BufferId(5);
3058 }
3059
3060 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3061
3062 let active_id = api.get_active_buffer_id();
3063 assert_eq!(active_id.0, 5);
3064 }
3065
3066 #[test]
3067 fn test_get_buffer_info() {
3068 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3069 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3070 let (tx, _rx) = std::sync::mpsc::channel();
3071 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3072
3073 {
3075 let mut snapshot = state_snapshot.write().unwrap();
3076 let buffer_info = BufferInfo {
3077 id: BufferId(1),
3078 path: Some(std::path::PathBuf::from("/test/file.txt")),
3079 modified: true,
3080 length: 100,
3081 is_virtual: false,
3082 view_mode: "source".to_string(),
3083 is_composing_in_any_split: false,
3084 compose_width: None,
3085 language: "text".to_string(),
3086 };
3087 snapshot.buffers.insert(BufferId(1), buffer_info);
3088 }
3089
3090 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3091
3092 let info = api.get_buffer_info(BufferId(1));
3093 assert!(info.is_some());
3094 let info = info.unwrap();
3095 assert_eq!(info.id.0, 1);
3096 assert_eq!(
3097 info.path.as_ref().unwrap().to_str().unwrap(),
3098 "/test/file.txt"
3099 );
3100 assert!(info.modified);
3101 assert_eq!(info.length, 100);
3102
3103 let no_info = api.get_buffer_info(BufferId(999));
3105 assert!(no_info.is_none());
3106 }
3107
3108 #[test]
3109 fn test_list_buffers() {
3110 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3111 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3112 let (tx, _rx) = std::sync::mpsc::channel();
3113 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3114
3115 {
3117 let mut snapshot = state_snapshot.write().unwrap();
3118 snapshot.buffers.insert(
3119 BufferId(1),
3120 BufferInfo {
3121 id: BufferId(1),
3122 path: Some(std::path::PathBuf::from("/file1.txt")),
3123 modified: false,
3124 length: 50,
3125 is_virtual: false,
3126 view_mode: "source".to_string(),
3127 is_composing_in_any_split: false,
3128 compose_width: None,
3129 language: "text".to_string(),
3130 },
3131 );
3132 snapshot.buffers.insert(
3133 BufferId(2),
3134 BufferInfo {
3135 id: BufferId(2),
3136 path: Some(std::path::PathBuf::from("/file2.txt")),
3137 modified: true,
3138 length: 100,
3139 is_virtual: false,
3140 view_mode: "source".to_string(),
3141 is_composing_in_any_split: false,
3142 compose_width: None,
3143 language: "text".to_string(),
3144 },
3145 );
3146 snapshot.buffers.insert(
3147 BufferId(3),
3148 BufferInfo {
3149 id: BufferId(3),
3150 path: None,
3151 modified: false,
3152 length: 0,
3153 is_virtual: true,
3154 view_mode: "source".to_string(),
3155 is_composing_in_any_split: false,
3156 compose_width: None,
3157 language: "text".to_string(),
3158 },
3159 );
3160 }
3161
3162 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3163
3164 let buffers = api.list_buffers();
3165 assert_eq!(buffers.len(), 3);
3166
3167 assert!(buffers.iter().any(|b| b.id.0 == 1));
3169 assert!(buffers.iter().any(|b| b.id.0 == 2));
3170 assert!(buffers.iter().any(|b| b.id.0 == 3));
3171 }
3172
3173 #[test]
3174 fn test_get_primary_cursor() {
3175 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3176 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3177 let (tx, _rx) = std::sync::mpsc::channel();
3178 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3179
3180 {
3182 let mut snapshot = state_snapshot.write().unwrap();
3183 snapshot.primary_cursor = Some(CursorInfo {
3184 position: 42,
3185 selection: Some(10..42),
3186 });
3187 }
3188
3189 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3190
3191 let cursor = api.get_primary_cursor();
3192 assert!(cursor.is_some());
3193 let cursor = cursor.unwrap();
3194 assert_eq!(cursor.position, 42);
3195 assert_eq!(cursor.selection, Some(10..42));
3196 }
3197
3198 #[test]
3199 fn test_get_all_cursors() {
3200 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3201 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3202 let (tx, _rx) = std::sync::mpsc::channel();
3203 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3204
3205 {
3207 let mut snapshot = state_snapshot.write().unwrap();
3208 snapshot.all_cursors = vec![
3209 CursorInfo {
3210 position: 10,
3211 selection: None,
3212 },
3213 CursorInfo {
3214 position: 20,
3215 selection: Some(15..20),
3216 },
3217 CursorInfo {
3218 position: 30,
3219 selection: Some(25..30),
3220 },
3221 ];
3222 }
3223
3224 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3225
3226 let cursors = api.get_all_cursors();
3227 assert_eq!(cursors.len(), 3);
3228 assert_eq!(cursors[0].position, 10);
3229 assert_eq!(cursors[0].selection, None);
3230 assert_eq!(cursors[1].position, 20);
3231 assert_eq!(cursors[1].selection, Some(15..20));
3232 assert_eq!(cursors[2].position, 30);
3233 assert_eq!(cursors[2].selection, Some(25..30));
3234 }
3235
3236 #[test]
3237 fn test_get_viewport() {
3238 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3239 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3240 let (tx, _rx) = std::sync::mpsc::channel();
3241 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3242
3243 {
3245 let mut snapshot = state_snapshot.write().unwrap();
3246 snapshot.viewport = Some(ViewportInfo {
3247 top_byte: 100,
3248 top_line: Some(5),
3249 left_column: 5,
3250 width: 80,
3251 height: 24,
3252 });
3253 }
3254
3255 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3256
3257 let viewport = api.get_viewport();
3258 assert!(viewport.is_some());
3259 let viewport = viewport.unwrap();
3260 assert_eq!(viewport.top_byte, 100);
3261 assert_eq!(viewport.left_column, 5);
3262 assert_eq!(viewport.width, 80);
3263 assert_eq!(viewport.height, 24);
3264 }
3265
3266 #[test]
3267 fn test_composite_buffer_options_rejects_unknown_fields() {
3268 let valid_json = r#"{
3270 "name": "test",
3271 "mode": "diff",
3272 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3273 "sources": [{"bufferId": 1, "label": "old"}]
3274 }"#;
3275 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
3276 assert!(
3277 result.is_ok(),
3278 "Valid JSON should parse: {:?}",
3279 result.err()
3280 );
3281
3282 let invalid_json = r#"{
3284 "name": "test",
3285 "mode": "diff",
3286 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3287 "sources": [{"buffer_id": 1, "label": "old"}]
3288 }"#;
3289 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
3290 assert!(
3291 result.is_err(),
3292 "JSON with unknown field should fail to parse"
3293 );
3294 let err = result.unwrap_err().to_string();
3295 assert!(
3296 err.contains("unknown field") || err.contains("buffer_id"),
3297 "Error should mention unknown field: {}",
3298 err
3299 );
3300 }
3301
3302 #[test]
3303 fn test_composite_hunk_rejects_unknown_fields() {
3304 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3306 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
3307 assert!(
3308 result.is_ok(),
3309 "Valid JSON should parse: {:?}",
3310 result.err()
3311 );
3312
3313 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3315 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
3316 assert!(
3317 result.is_err(),
3318 "JSON with unknown field should fail to parse"
3319 );
3320 let err = result.unwrap_err().to_string();
3321 assert!(
3322 err.contains("unknown field") || err.contains("old_start"),
3323 "Error should mention unknown field: {}",
3324 err
3325 );
3326 }
3327
3328 #[test]
3329 fn test_plugin_response_line_end_position() {
3330 let response = PluginResponse::LineEndPosition {
3331 request_id: 42,
3332 position: Some(100),
3333 };
3334 let json = serde_json::to_string(&response).unwrap();
3335 assert!(json.contains("LineEndPosition"));
3336 assert!(json.contains("42"));
3337 assert!(json.contains("100"));
3338
3339 let response_none = PluginResponse::LineEndPosition {
3341 request_id: 1,
3342 position: None,
3343 };
3344 let json_none = serde_json::to_string(&response_none).unwrap();
3345 assert!(json_none.contains("null"));
3346 }
3347
3348 #[test]
3349 fn test_plugin_response_buffer_line_count() {
3350 let response = PluginResponse::BufferLineCount {
3351 request_id: 99,
3352 count: Some(500),
3353 };
3354 let json = serde_json::to_string(&response).unwrap();
3355 assert!(json.contains("BufferLineCount"));
3356 assert!(json.contains("99"));
3357 assert!(json.contains("500"));
3358 }
3359
3360 #[test]
3361 fn test_plugin_command_get_line_end_position() {
3362 let command = PluginCommand::GetLineEndPosition {
3363 buffer_id: BufferId(1),
3364 line: 10,
3365 request_id: 123,
3366 };
3367 let json = serde_json::to_string(&command).unwrap();
3368 assert!(json.contains("GetLineEndPosition"));
3369 assert!(json.contains("10"));
3370 }
3371
3372 #[test]
3373 fn test_plugin_command_get_buffer_line_count() {
3374 let command = PluginCommand::GetBufferLineCount {
3375 buffer_id: BufferId(0),
3376 request_id: 456,
3377 };
3378 let json = serde_json::to_string(&command).unwrap();
3379 assert!(json.contains("GetBufferLineCount"));
3380 assert!(json.contains("456"));
3381 }
3382
3383 #[test]
3384 fn test_plugin_command_scroll_to_line_center() {
3385 let command = PluginCommand::ScrollToLineCenter {
3386 split_id: SplitId(1),
3387 buffer_id: BufferId(2),
3388 line: 50,
3389 };
3390 let json = serde_json::to_string(&command).unwrap();
3391 assert!(json.contains("ScrollToLineCenter"));
3392 assert!(json.contains("50"));
3393 }
3394}