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 #[serde(default, rename = "initialFocusHunk")]
629 #[ts(optional, rename = "initialFocusHunk")]
630 pub initial_focus_hunk: Option<usize>,
631}
632
633#[derive(Debug, Clone, Serialize, Deserialize, TS)]
635#[ts(export)]
636pub enum ViewTokenWireKind {
637 Text(String),
638 Newline,
639 Space,
640 Break,
643 BinaryByte(u8),
647}
648
649#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
655#[serde(deny_unknown_fields)]
656#[ts(export)]
657pub struct ViewTokenStyle {
658 #[serde(default)]
660 #[ts(type = "[number, number, number] | null")]
661 pub fg: Option<(u8, u8, u8)>,
662 #[serde(default)]
664 #[ts(type = "[number, number, number] | null")]
665 pub bg: Option<(u8, u8, u8)>,
666 #[serde(default)]
668 pub bold: bool,
669 #[serde(default)]
671 pub italic: bool,
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize, TS)]
676#[serde(deny_unknown_fields)]
677#[ts(export)]
678pub struct ViewTokenWire {
679 #[ts(type = "number | null")]
681 pub source_offset: Option<usize>,
682 pub kind: ViewTokenWireKind,
684 #[serde(default)]
686 #[ts(optional)]
687 pub style: Option<ViewTokenStyle>,
688}
689
690#[derive(Debug, Clone, Serialize, Deserialize, TS)]
692#[ts(export)]
693pub struct ViewTransformPayload {
694 pub range: Range<usize>,
696 pub tokens: Vec<ViewTokenWire>,
698 pub layout_hints: Option<LayoutHints>,
700}
701
702#[derive(Debug, Clone, Serialize, Deserialize, TS)]
705#[ts(export)]
706pub struct EditorStateSnapshot {
707 pub active_buffer_id: BufferId,
709 pub active_split_id: usize,
711 pub buffers: HashMap<BufferId, BufferInfo>,
713 pub buffer_saved_diffs: HashMap<BufferId, BufferSavedDiff>,
715 pub primary_cursor: Option<CursorInfo>,
717 pub all_cursors: Vec<CursorInfo>,
719 pub viewport: Option<ViewportInfo>,
721 pub buffer_cursor_positions: HashMap<BufferId, usize>,
723 pub buffer_text_properties: HashMap<BufferId, Vec<TextProperty>>,
725 pub selected_text: Option<String>,
728 pub clipboard: String,
730 pub working_dir: PathBuf,
732 #[ts(type = "any")]
735 pub diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
736 #[ts(type = "any")]
739 pub folding_ranges: HashMap<String, Vec<lsp_types::FoldingRange>>,
740 #[ts(type = "any")]
743 pub config: serde_json::Value,
744 #[ts(type = "any")]
747 pub user_config: serde_json::Value,
748 #[ts(type = "GrammarInfo[]")]
750 pub available_grammars: Vec<GrammarInfoSnapshot>,
751 pub editor_mode: Option<String>,
754
755 #[ts(type = "any")]
759 pub plugin_view_states: HashMap<BufferId, HashMap<String, serde_json::Value>>,
760
761 #[serde(skip)]
764 #[ts(skip)]
765 pub plugin_view_states_split: usize,
766
767 #[serde(skip)]
770 #[ts(skip)]
771 pub keybinding_labels: HashMap<String, String>,
772
773 #[ts(type = "any")]
780 pub plugin_global_states: HashMap<String, HashMap<String, serde_json::Value>>,
781}
782
783impl EditorStateSnapshot {
784 pub fn new() -> Self {
785 Self {
786 active_buffer_id: BufferId(0),
787 active_split_id: 0,
788 buffers: HashMap::new(),
789 buffer_saved_diffs: HashMap::new(),
790 primary_cursor: None,
791 all_cursors: Vec::new(),
792 viewport: None,
793 buffer_cursor_positions: HashMap::new(),
794 buffer_text_properties: HashMap::new(),
795 selected_text: None,
796 clipboard: String::new(),
797 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
798 diagnostics: HashMap::new(),
799 folding_ranges: HashMap::new(),
800 config: serde_json::Value::Null,
801 user_config: serde_json::Value::Null,
802 available_grammars: Vec::new(),
803 editor_mode: None,
804 plugin_view_states: HashMap::new(),
805 plugin_view_states_split: 0,
806 keybinding_labels: HashMap::new(),
807 plugin_global_states: HashMap::new(),
808 }
809 }
810}
811
812impl Default for EditorStateSnapshot {
813 fn default() -> Self {
814 Self::new()
815 }
816}
817
818#[derive(Debug, Clone, Serialize, Deserialize, TS)]
820#[ts(export)]
821pub struct GrammarInfoSnapshot {
822 pub name: String,
824 pub source: String,
826 pub file_extensions: Vec<String>,
828 pub short_name: Option<String>,
830}
831
832#[derive(Debug, Clone, Serialize, Deserialize, TS)]
834#[ts(export)]
835pub enum MenuPosition {
836 Top,
838 Bottom,
840 Before(String),
842 After(String),
844}
845
846#[derive(Debug, Clone, Serialize, Deserialize, TS)]
848#[ts(export)]
849pub enum PluginCommand {
850 InsertText {
852 buffer_id: BufferId,
853 position: usize,
854 text: String,
855 },
856
857 DeleteRange {
859 buffer_id: BufferId,
860 range: Range<usize>,
861 },
862
863 AddOverlay {
868 buffer_id: BufferId,
869 namespace: Option<OverlayNamespace>,
870 range: Range<usize>,
871 options: OverlayOptions,
873 },
874
875 RemoveOverlay {
877 buffer_id: BufferId,
878 handle: OverlayHandle,
879 },
880
881 SetStatus { message: String },
883
884 ApplyTheme { theme_name: String },
886
887 ReloadConfig,
890
891 RegisterCommand { command: Command },
893
894 UnregisterCommand { name: String },
896
897 OpenFileInBackground { path: PathBuf },
899
900 InsertAtCursor { text: String },
902
903 SpawnProcess {
905 command: String,
906 args: Vec<String>,
907 cwd: Option<String>,
908 callback_id: JsCallbackId,
909 },
910
911 Delay {
913 callback_id: JsCallbackId,
914 duration_ms: u64,
915 },
916
917 SpawnBackgroundProcess {
921 process_id: u64,
923 command: String,
925 args: Vec<String>,
927 cwd: Option<String>,
929 callback_id: JsCallbackId,
931 },
932
933 KillBackgroundProcess { process_id: u64 },
935
936 SpawnProcessWait {
939 process_id: u64,
941 callback_id: JsCallbackId,
943 },
944
945 SetLayoutHints {
947 buffer_id: BufferId,
948 split_id: Option<SplitId>,
949 range: Range<usize>,
950 hints: LayoutHints,
951 },
952
953 SetLineNumbers { buffer_id: BufferId, enabled: bool },
955
956 SetViewMode { buffer_id: BufferId, mode: String },
958
959 SetLineWrap {
961 buffer_id: BufferId,
962 split_id: Option<SplitId>,
963 enabled: bool,
964 },
965
966 SubmitViewTransform {
968 buffer_id: BufferId,
969 split_id: Option<SplitId>,
970 payload: ViewTransformPayload,
971 },
972
973 ClearViewTransform {
975 buffer_id: BufferId,
976 split_id: Option<SplitId>,
977 },
978
979 SetViewState {
982 buffer_id: BufferId,
983 key: String,
984 #[ts(type = "any")]
985 value: Option<serde_json::Value>,
986 },
987
988 SetGlobalState {
992 plugin_name: String,
993 key: String,
994 #[ts(type = "any")]
995 value: Option<serde_json::Value>,
996 },
997
998 ClearAllOverlays { buffer_id: BufferId },
1000
1001 ClearNamespace {
1003 buffer_id: BufferId,
1004 namespace: OverlayNamespace,
1005 },
1006
1007 ClearOverlaysInRange {
1010 buffer_id: BufferId,
1011 start: usize,
1012 end: usize,
1013 },
1014
1015 AddVirtualText {
1018 buffer_id: BufferId,
1019 virtual_text_id: String,
1020 position: usize,
1021 text: String,
1022 color: (u8, u8, u8),
1023 use_bg: bool, before: bool, },
1026
1027 RemoveVirtualText {
1029 buffer_id: BufferId,
1030 virtual_text_id: String,
1031 },
1032
1033 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
1035
1036 ClearVirtualTexts { buffer_id: BufferId },
1038
1039 AddVirtualLine {
1043 buffer_id: BufferId,
1044 position: usize,
1046 text: String,
1048 fg_color: (u8, u8, u8),
1050 bg_color: Option<(u8, u8, u8)>,
1052 above: bool,
1054 namespace: String,
1056 priority: i32,
1058 },
1059
1060 ClearVirtualTextNamespace {
1063 buffer_id: BufferId,
1064 namespace: String,
1065 },
1066
1067 AddConceal {
1070 buffer_id: BufferId,
1071 namespace: OverlayNamespace,
1073 start: usize,
1075 end: usize,
1076 replacement: Option<String>,
1078 },
1079
1080 ClearConcealNamespace {
1082 buffer_id: BufferId,
1083 namespace: OverlayNamespace,
1084 },
1085
1086 ClearConcealsInRange {
1089 buffer_id: BufferId,
1090 start: usize,
1091 end: usize,
1092 },
1093
1094 AddSoftBreak {
1098 buffer_id: BufferId,
1099 namespace: OverlayNamespace,
1101 position: usize,
1103 indent: u16,
1105 },
1106
1107 ClearSoftBreakNamespace {
1109 buffer_id: BufferId,
1110 namespace: OverlayNamespace,
1111 },
1112
1113 ClearSoftBreaksInRange {
1115 buffer_id: BufferId,
1116 start: usize,
1117 end: usize,
1118 },
1119
1120 RefreshLines { buffer_id: BufferId },
1122
1123 RefreshAllLines,
1127
1128 HookCompleted { hook_name: String },
1132
1133 SetLineIndicator {
1136 buffer_id: BufferId,
1137 line: usize,
1139 namespace: String,
1141 symbol: String,
1143 color: (u8, u8, u8),
1145 priority: i32,
1147 },
1148
1149 SetLineIndicators {
1152 buffer_id: BufferId,
1153 lines: Vec<usize>,
1155 namespace: String,
1157 symbol: String,
1159 color: (u8, u8, u8),
1161 priority: i32,
1163 },
1164
1165 ClearLineIndicators {
1167 buffer_id: BufferId,
1168 namespace: String,
1170 },
1171
1172 SetFileExplorerDecorations {
1174 namespace: String,
1176 decorations: Vec<FileExplorerDecoration>,
1178 },
1179
1180 ClearFileExplorerDecorations {
1182 namespace: String,
1184 },
1185
1186 OpenFileAtLocation {
1189 path: PathBuf,
1190 line: Option<usize>, column: Option<usize>, },
1193
1194 OpenFileInSplit {
1197 split_id: usize,
1198 path: PathBuf,
1199 line: Option<usize>, column: Option<usize>, },
1202
1203 StartPrompt {
1206 label: String,
1207 prompt_type: String, },
1209
1210 StartPromptWithInitial {
1212 label: String,
1213 prompt_type: String,
1214 initial_value: String,
1215 },
1216
1217 StartPromptAsync {
1220 label: String,
1221 initial_value: String,
1222 callback_id: JsCallbackId,
1223 },
1224
1225 SetPromptSuggestions { suggestions: Vec<Suggestion> },
1228
1229 SetPromptInputSync { sync: bool },
1231
1232 AddMenuItem {
1235 menu_label: String,
1236 item: MenuItem,
1237 position: MenuPosition,
1238 },
1239
1240 AddMenu { menu: Menu, position: MenuPosition },
1242
1243 RemoveMenuItem {
1245 menu_label: String,
1246 item_label: String,
1247 },
1248
1249 RemoveMenu { menu_label: String },
1251
1252 CreateVirtualBuffer {
1254 name: String,
1256 mode: String,
1258 read_only: bool,
1260 },
1261
1262 CreateVirtualBufferWithContent {
1266 name: String,
1268 mode: String,
1270 read_only: bool,
1272 entries: Vec<TextPropertyEntry>,
1274 show_line_numbers: bool,
1276 show_cursors: bool,
1278 editing_disabled: bool,
1280 hidden_from_tabs: bool,
1282 request_id: Option<u64>,
1284 },
1285
1286 CreateVirtualBufferInSplit {
1289 name: String,
1291 mode: String,
1293 read_only: bool,
1295 entries: Vec<TextPropertyEntry>,
1297 ratio: f32,
1299 direction: Option<String>,
1301 panel_id: Option<String>,
1303 show_line_numbers: bool,
1305 show_cursors: bool,
1307 editing_disabled: bool,
1309 line_wrap: Option<bool>,
1311 before: bool,
1313 request_id: Option<u64>,
1315 },
1316
1317 SetVirtualBufferContent {
1319 buffer_id: BufferId,
1320 entries: Vec<TextPropertyEntry>,
1322 },
1323
1324 GetTextPropertiesAtCursor { buffer_id: BufferId },
1326
1327 DefineMode {
1329 name: String,
1330 bindings: Vec<(String, String)>, read_only: bool,
1332 allow_text_input: bool,
1334 plugin_name: Option<String>,
1336 },
1337
1338 ShowBuffer { buffer_id: BufferId },
1340
1341 CreateVirtualBufferInExistingSplit {
1343 name: String,
1345 mode: String,
1347 read_only: bool,
1349 entries: Vec<TextPropertyEntry>,
1351 split_id: SplitId,
1353 show_line_numbers: bool,
1355 show_cursors: bool,
1357 editing_disabled: bool,
1359 line_wrap: Option<bool>,
1361 request_id: Option<u64>,
1363 },
1364
1365 CloseBuffer { buffer_id: BufferId },
1367
1368 CreateCompositeBuffer {
1371 name: String,
1373 mode: String,
1375 layout: CompositeLayoutConfig,
1377 sources: Vec<CompositeSourceConfig>,
1379 hunks: Option<Vec<CompositeHunk>>,
1381 initial_focus_hunk: Option<usize>,
1383 request_id: Option<u64>,
1385 },
1386
1387 UpdateCompositeAlignment {
1389 buffer_id: BufferId,
1390 hunks: Vec<CompositeHunk>,
1391 },
1392
1393 CloseCompositeBuffer { buffer_id: BufferId },
1395
1396 FlushLayout,
1403
1404 CompositeNextHunk { buffer_id: BufferId },
1406
1407 CompositePrevHunk { buffer_id: BufferId },
1409
1410 FocusSplit { split_id: SplitId },
1412
1413 SetSplitBuffer {
1415 split_id: SplitId,
1416 buffer_id: BufferId,
1417 },
1418
1419 SetSplitScroll { split_id: SplitId, top_byte: usize },
1421
1422 RequestHighlights {
1424 buffer_id: BufferId,
1425 range: Range<usize>,
1426 request_id: u64,
1427 },
1428
1429 CloseSplit { split_id: SplitId },
1431
1432 SetSplitRatio {
1434 split_id: SplitId,
1435 ratio: f32,
1437 },
1438
1439 SetSplitLabel { split_id: SplitId, label: String },
1441
1442 ClearSplitLabel { split_id: SplitId },
1444
1445 GetSplitByLabel { label: String, request_id: u64 },
1447
1448 DistributeSplitsEvenly {
1450 split_ids: Vec<SplitId>,
1452 },
1453
1454 SetBufferCursor {
1456 buffer_id: BufferId,
1457 position: usize,
1459 },
1460
1461 SendLspRequest {
1463 language: String,
1464 method: String,
1465 #[ts(type = "any")]
1466 params: Option<JsonValue>,
1467 request_id: u64,
1468 },
1469
1470 SetClipboard { text: String },
1472
1473 DeleteSelection,
1476
1477 SetContext {
1481 name: String,
1483 active: bool,
1485 },
1486
1487 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1489
1490 ExecuteAction {
1493 action_name: String,
1495 },
1496
1497 ExecuteActions {
1501 actions: Vec<ActionSpec>,
1503 },
1504
1505 GetBufferText {
1507 buffer_id: BufferId,
1509 start: usize,
1511 end: usize,
1513 request_id: u64,
1515 },
1516
1517 GetLineStartPosition {
1520 buffer_id: BufferId,
1522 line: u32,
1524 request_id: u64,
1526 },
1527
1528 GetLineEndPosition {
1532 buffer_id: BufferId,
1534 line: u32,
1536 request_id: u64,
1538 },
1539
1540 GetBufferLineCount {
1542 buffer_id: BufferId,
1544 request_id: u64,
1546 },
1547
1548 ScrollToLineCenter {
1551 split_id: SplitId,
1553 buffer_id: BufferId,
1555 line: usize,
1557 },
1558
1559 SetEditorMode {
1562 mode: Option<String>,
1564 },
1565
1566 ShowActionPopup {
1569 popup_id: String,
1571 title: String,
1573 message: String,
1575 actions: Vec<ActionPopupAction>,
1577 },
1578
1579 DisableLspForLanguage {
1581 language: String,
1583 },
1584
1585 RestartLspForLanguage {
1587 language: String,
1589 },
1590
1591 SetLspRootUri {
1595 language: String,
1597 uri: String,
1599 },
1600
1601 CreateScrollSyncGroup {
1605 group_id: u32,
1607 left_split: SplitId,
1609 right_split: SplitId,
1611 },
1612
1613 SetScrollSyncAnchors {
1616 group_id: u32,
1618 anchors: Vec<(usize, usize)>,
1620 },
1621
1622 RemoveScrollSyncGroup {
1624 group_id: u32,
1626 },
1627
1628 SaveBufferToPath {
1631 buffer_id: BufferId,
1633 path: PathBuf,
1635 },
1636
1637 LoadPlugin {
1640 path: PathBuf,
1642 callback_id: JsCallbackId,
1644 },
1645
1646 UnloadPlugin {
1649 name: String,
1651 callback_id: JsCallbackId,
1653 },
1654
1655 ReloadPlugin {
1658 name: String,
1660 callback_id: JsCallbackId,
1662 },
1663
1664 ListPlugins {
1667 callback_id: JsCallbackId,
1669 },
1670
1671 ReloadThemes { apply_theme: Option<String> },
1675
1676 RegisterGrammar {
1679 language: String,
1681 grammar_path: String,
1683 extensions: Vec<String>,
1685 },
1686
1687 RegisterLanguageConfig {
1690 language: String,
1692 config: LanguagePackConfig,
1694 },
1695
1696 RegisterLspServer {
1699 language: String,
1701 config: LspServerPackConfig,
1703 },
1704
1705 ReloadGrammars { callback_id: JsCallbackId },
1709
1710 CreateTerminal {
1714 cwd: Option<String>,
1716 direction: Option<String>,
1718 ratio: Option<f32>,
1720 focus: Option<bool>,
1722 request_id: u64,
1724 },
1725
1726 SendTerminalInput {
1728 terminal_id: TerminalId,
1730 data: String,
1732 },
1733
1734 CloseTerminal {
1736 terminal_id: TerminalId,
1738 },
1739
1740 GrepProject {
1744 pattern: String,
1746 fixed_string: bool,
1748 case_sensitive: bool,
1750 max_results: usize,
1752 whole_words: bool,
1754 callback_id: JsCallbackId,
1756 },
1757
1758 GrepProjectStreaming {
1763 pattern: String,
1765 fixed_string: bool,
1767 case_sensitive: bool,
1769 max_results: usize,
1771 whole_words: bool,
1773 search_id: u64,
1775 callback_id: JsCallbackId,
1777 },
1778
1779 ReplaceInBuffer {
1783 file_path: PathBuf,
1785 matches: Vec<(usize, usize)>,
1787 replacement: String,
1789 callback_id: JsCallbackId,
1791 },
1792}
1793
1794impl PluginCommand {
1795 pub fn debug_variant_name(&self) -> String {
1797 let dbg = format!("{:?}", self);
1798 dbg.split([' ', '{', '(']).next().unwrap_or("?").to_string()
1799 }
1800}
1801
1802#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
1811#[serde(rename_all = "camelCase")]
1812#[ts(export)]
1813pub struct LanguagePackConfig {
1814 #[serde(default)]
1816 pub comment_prefix: Option<String>,
1817
1818 #[serde(default)]
1820 pub block_comment_start: Option<String>,
1821
1822 #[serde(default)]
1824 pub block_comment_end: Option<String>,
1825
1826 #[serde(default)]
1828 pub use_tabs: Option<bool>,
1829
1830 #[serde(default)]
1832 pub tab_size: Option<usize>,
1833
1834 #[serde(default)]
1836 pub auto_indent: Option<bool>,
1837
1838 #[serde(default)]
1841 pub show_whitespace_tabs: Option<bool>,
1842
1843 #[serde(default)]
1845 pub formatter: Option<FormatterPackConfig>,
1846}
1847
1848#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1850#[serde(rename_all = "camelCase")]
1851#[ts(export)]
1852pub struct FormatterPackConfig {
1853 pub command: String,
1855
1856 #[serde(default)]
1858 pub args: Vec<String>,
1859}
1860
1861#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1863#[serde(rename_all = "camelCase")]
1864#[ts(export)]
1865pub struct ProcessLimitsPackConfig {
1866 #[serde(default)]
1868 pub max_memory_percent: Option<u32>,
1869
1870 #[serde(default)]
1872 pub max_cpu_percent: Option<u32>,
1873
1874 #[serde(default)]
1876 pub enabled: Option<bool>,
1877}
1878
1879#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1881#[serde(rename_all = "camelCase")]
1882#[ts(export)]
1883pub struct LspServerPackConfig {
1884 pub command: String,
1886
1887 #[serde(default)]
1889 pub args: Vec<String>,
1890
1891 #[serde(default)]
1893 pub auto_start: Option<bool>,
1894
1895 #[serde(default)]
1897 #[ts(type = "Record<string, unknown> | null")]
1898 pub initialization_options: Option<JsonValue>,
1899
1900 #[serde(default)]
1902 pub process_limits: Option<ProcessLimitsPackConfig>,
1903}
1904
1905#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1907#[ts(export)]
1908pub enum HunkStatus {
1909 Pending,
1910 Staged,
1911 Discarded,
1912}
1913
1914#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1916#[ts(export)]
1917pub struct ReviewHunk {
1918 pub id: String,
1919 pub file: String,
1920 pub context_header: String,
1921 pub status: HunkStatus,
1922 pub base_range: Option<(usize, usize)>,
1924 pub modified_range: Option<(usize, usize)>,
1926}
1927
1928#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1930#[serde(deny_unknown_fields)]
1931#[ts(export, rename = "TsActionPopupAction")]
1932pub struct ActionPopupAction {
1933 pub id: String,
1935 pub label: String,
1937}
1938
1939#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1941#[serde(deny_unknown_fields)]
1942#[ts(export)]
1943pub struct ActionPopupOptions {
1944 pub id: String,
1946 pub title: String,
1948 pub message: String,
1950 pub actions: Vec<ActionPopupAction>,
1952}
1953
1954#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1956#[ts(export)]
1957pub struct TsHighlightSpan {
1958 pub start: u32,
1959 pub end: u32,
1960 #[ts(type = "[number, number, number]")]
1961 pub color: (u8, u8, u8),
1962 pub bold: bool,
1963 pub italic: bool,
1964}
1965
1966#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1968#[ts(export)]
1969pub struct SpawnResult {
1970 pub stdout: String,
1972 pub stderr: String,
1974 pub exit_code: i32,
1976}
1977
1978#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1980#[ts(export)]
1981pub struct BackgroundProcessResult {
1982 #[ts(type = "number")]
1984 pub process_id: u64,
1985 pub exit_code: i32,
1988}
1989
1990#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1992#[serde(rename_all = "camelCase")]
1993#[ts(export, rename_all = "camelCase")]
1994pub struct GrepMatch {
1995 pub file: String,
1997 #[ts(type = "number")]
1999 pub buffer_id: usize,
2000 #[ts(type = "number")]
2002 pub byte_offset: usize,
2003 #[ts(type = "number")]
2005 pub length: usize,
2006 #[ts(type = "number")]
2008 pub line: usize,
2009 #[ts(type = "number")]
2011 pub column: usize,
2012 pub context: String,
2014}
2015
2016#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2018#[serde(rename_all = "camelCase")]
2019#[ts(export, rename_all = "camelCase")]
2020pub struct ReplaceResult {
2021 #[ts(type = "number")]
2023 pub replacements: usize,
2024 #[ts(type = "number")]
2026 pub buffer_id: usize,
2027}
2028
2029#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2031#[serde(deny_unknown_fields, rename_all = "camelCase")]
2032#[ts(export, rename = "TextPropertyEntry", rename_all = "camelCase")]
2033pub struct JsTextPropertyEntry {
2034 pub text: String,
2036 #[serde(default)]
2038 #[ts(optional, type = "Record<string, unknown>")]
2039 pub properties: Option<HashMap<String, JsonValue>>,
2040 #[serde(default)]
2042 #[ts(optional, type = "Partial<OverlayOptions>")]
2043 pub style: Option<OverlayOptions>,
2044 #[serde(default)]
2046 #[ts(optional)]
2047 pub inline_overlays: Option<Vec<crate::text_property::InlineOverlay>>,
2048}
2049
2050#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2052#[ts(export)]
2053pub struct DirEntry {
2054 pub name: String,
2056 pub is_file: bool,
2058 pub is_dir: bool,
2060}
2061
2062#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2064#[ts(export)]
2065pub struct JsPosition {
2066 pub line: u32,
2068 pub character: u32,
2070}
2071
2072#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2074#[ts(export)]
2075pub struct JsRange {
2076 pub start: JsPosition,
2078 pub end: JsPosition,
2080}
2081
2082#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2084#[ts(export)]
2085pub struct JsDiagnostic {
2086 pub uri: String,
2088 pub message: String,
2090 pub severity: Option<u8>,
2092 pub range: JsRange,
2094 #[ts(optional)]
2096 pub source: Option<String>,
2097}
2098
2099#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2101#[serde(deny_unknown_fields)]
2102#[ts(export)]
2103pub struct CreateVirtualBufferOptions {
2104 pub name: String,
2106 #[serde(default)]
2108 #[ts(optional)]
2109 pub mode: Option<String>,
2110 #[serde(default, rename = "readOnly")]
2112 #[ts(optional, rename = "readOnly")]
2113 pub read_only: Option<bool>,
2114 #[serde(default, rename = "showLineNumbers")]
2116 #[ts(optional, rename = "showLineNumbers")]
2117 pub show_line_numbers: Option<bool>,
2118 #[serde(default, rename = "showCursors")]
2120 #[ts(optional, rename = "showCursors")]
2121 pub show_cursors: Option<bool>,
2122 #[serde(default, rename = "editingDisabled")]
2124 #[ts(optional, rename = "editingDisabled")]
2125 pub editing_disabled: Option<bool>,
2126 #[serde(default, rename = "hiddenFromTabs")]
2128 #[ts(optional, rename = "hiddenFromTabs")]
2129 pub hidden_from_tabs: Option<bool>,
2130 #[serde(default)]
2132 #[ts(optional)]
2133 pub entries: Option<Vec<JsTextPropertyEntry>>,
2134}
2135
2136#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2138#[serde(deny_unknown_fields)]
2139#[ts(export)]
2140pub struct CreateVirtualBufferInSplitOptions {
2141 pub name: String,
2143 #[serde(default)]
2145 #[ts(optional)]
2146 pub mode: Option<String>,
2147 #[serde(default, rename = "readOnly")]
2149 #[ts(optional, rename = "readOnly")]
2150 pub read_only: Option<bool>,
2151 #[serde(default)]
2153 #[ts(optional)]
2154 pub ratio: Option<f32>,
2155 #[serde(default)]
2157 #[ts(optional)]
2158 pub direction: Option<String>,
2159 #[serde(default, rename = "panelId")]
2161 #[ts(optional, rename = "panelId")]
2162 pub panel_id: Option<String>,
2163 #[serde(default, rename = "showLineNumbers")]
2165 #[ts(optional, rename = "showLineNumbers")]
2166 pub show_line_numbers: Option<bool>,
2167 #[serde(default, rename = "showCursors")]
2169 #[ts(optional, rename = "showCursors")]
2170 pub show_cursors: Option<bool>,
2171 #[serde(default, rename = "editingDisabled")]
2173 #[ts(optional, rename = "editingDisabled")]
2174 pub editing_disabled: Option<bool>,
2175 #[serde(default, rename = "lineWrap")]
2177 #[ts(optional, rename = "lineWrap")]
2178 pub line_wrap: Option<bool>,
2179 #[serde(default)]
2181 #[ts(optional)]
2182 pub before: Option<bool>,
2183 #[serde(default)]
2185 #[ts(optional)]
2186 pub entries: Option<Vec<JsTextPropertyEntry>>,
2187}
2188
2189#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2191#[serde(deny_unknown_fields)]
2192#[ts(export)]
2193pub struct CreateVirtualBufferInExistingSplitOptions {
2194 pub name: String,
2196 #[serde(rename = "splitId")]
2198 #[ts(rename = "splitId")]
2199 pub split_id: usize,
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 = "lineWrap")]
2222 #[ts(optional, rename = "lineWrap")]
2223 pub line_wrap: 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 CreateTerminalOptions {
2235 #[serde(default)]
2237 #[ts(optional)]
2238 pub cwd: Option<String>,
2239 #[serde(default)]
2241 #[ts(optional)]
2242 pub direction: Option<String>,
2243 #[serde(default)]
2245 #[ts(optional)]
2246 pub ratio: Option<f32>,
2247 #[serde(default)]
2249 #[ts(optional)]
2250 pub focus: Option<bool>,
2251}
2252
2253#[derive(Debug, Clone, Serialize, TS)]
2258#[ts(export, type = "Array<Record<string, unknown>>")]
2259pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
2260
2261#[cfg(feature = "plugins")]
2263mod fromjs_impls {
2264 use super::*;
2265 use rquickjs::{Ctx, FromJs, Value};
2266
2267 impl<'js> FromJs<'js> for JsTextPropertyEntry {
2268 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2269 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2270 from: "object",
2271 to: "JsTextPropertyEntry",
2272 message: Some(e.to_string()),
2273 })
2274 }
2275 }
2276
2277 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
2278 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2279 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2280 from: "object",
2281 to: "CreateVirtualBufferOptions",
2282 message: Some(e.to_string()),
2283 })
2284 }
2285 }
2286
2287 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
2288 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2289 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2290 from: "object",
2291 to: "CreateVirtualBufferInSplitOptions",
2292 message: Some(e.to_string()),
2293 })
2294 }
2295 }
2296
2297 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
2298 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2299 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2300 from: "object",
2301 to: "CreateVirtualBufferInExistingSplitOptions",
2302 message: Some(e.to_string()),
2303 })
2304 }
2305 }
2306
2307 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
2308 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
2309 rquickjs_serde::to_value(ctx.clone(), &self.0)
2310 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
2311 }
2312 }
2313
2314 impl<'js> FromJs<'js> for ActionSpec {
2317 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2318 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2319 from: "object",
2320 to: "ActionSpec",
2321 message: Some(e.to_string()),
2322 })
2323 }
2324 }
2325
2326 impl<'js> FromJs<'js> for ActionPopupAction {
2327 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2328 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2329 from: "object",
2330 to: "ActionPopupAction",
2331 message: Some(e.to_string()),
2332 })
2333 }
2334 }
2335
2336 impl<'js> FromJs<'js> for ActionPopupOptions {
2337 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2338 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2339 from: "object",
2340 to: "ActionPopupOptions",
2341 message: Some(e.to_string()),
2342 })
2343 }
2344 }
2345
2346 impl<'js> FromJs<'js> for ViewTokenWire {
2347 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2348 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2349 from: "object",
2350 to: "ViewTokenWire",
2351 message: Some(e.to_string()),
2352 })
2353 }
2354 }
2355
2356 impl<'js> FromJs<'js> for ViewTokenStyle {
2357 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2358 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2359 from: "object",
2360 to: "ViewTokenStyle",
2361 message: Some(e.to_string()),
2362 })
2363 }
2364 }
2365
2366 impl<'js> FromJs<'js> for LayoutHints {
2367 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2368 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2369 from: "object",
2370 to: "LayoutHints",
2371 message: Some(e.to_string()),
2372 })
2373 }
2374 }
2375
2376 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
2377 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2378 let json: serde_json::Value =
2380 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2381 from: "object",
2382 to: "CreateCompositeBufferOptions (json)",
2383 message: Some(e.to_string()),
2384 })?;
2385 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
2386 from: "json",
2387 to: "CreateCompositeBufferOptions",
2388 message: Some(e.to_string()),
2389 })
2390 }
2391 }
2392
2393 impl<'js> FromJs<'js> for CompositeHunk {
2394 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2395 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2396 from: "object",
2397 to: "CompositeHunk",
2398 message: Some(e.to_string()),
2399 })
2400 }
2401 }
2402
2403 impl<'js> FromJs<'js> for LanguagePackConfig {
2404 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2405 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2406 from: "object",
2407 to: "LanguagePackConfig",
2408 message: Some(e.to_string()),
2409 })
2410 }
2411 }
2412
2413 impl<'js> FromJs<'js> for LspServerPackConfig {
2414 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2415 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2416 from: "object",
2417 to: "LspServerPackConfig",
2418 message: Some(e.to_string()),
2419 })
2420 }
2421 }
2422
2423 impl<'js> FromJs<'js> for ProcessLimitsPackConfig {
2424 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2425 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2426 from: "object",
2427 to: "ProcessLimitsPackConfig",
2428 message: Some(e.to_string()),
2429 })
2430 }
2431 }
2432
2433 impl<'js> FromJs<'js> for CreateTerminalOptions {
2434 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2435 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2436 from: "object",
2437 to: "CreateTerminalOptions",
2438 message: Some(e.to_string()),
2439 })
2440 }
2441 }
2442}
2443
2444pub struct PluginApi {
2446 hooks: Arc<RwLock<HookRegistry>>,
2448
2449 commands: Arc<RwLock<CommandRegistry>>,
2451
2452 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2454
2455 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2457}
2458
2459impl PluginApi {
2460 pub fn new(
2462 hooks: Arc<RwLock<HookRegistry>>,
2463 commands: Arc<RwLock<CommandRegistry>>,
2464 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2465 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2466 ) -> Self {
2467 Self {
2468 hooks,
2469 commands,
2470 command_sender,
2471 state_snapshot,
2472 }
2473 }
2474
2475 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
2477 let mut hooks = self.hooks.write().unwrap();
2478 hooks.add_hook(hook_name, callback);
2479 }
2480
2481 pub fn unregister_hooks(&self, hook_name: &str) {
2483 let mut hooks = self.hooks.write().unwrap();
2484 hooks.remove_hooks(hook_name);
2485 }
2486
2487 pub fn register_command(&self, command: Command) {
2489 let commands = self.commands.read().unwrap();
2490 commands.register(command);
2491 }
2492
2493 pub fn unregister_command(&self, name: &str) {
2495 let commands = self.commands.read().unwrap();
2496 commands.unregister(name);
2497 }
2498
2499 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
2501 self.command_sender
2502 .send(command)
2503 .map_err(|e| format!("Failed to send command: {}", e))
2504 }
2505
2506 pub fn insert_text(
2508 &self,
2509 buffer_id: BufferId,
2510 position: usize,
2511 text: String,
2512 ) -> Result<(), String> {
2513 self.send_command(PluginCommand::InsertText {
2514 buffer_id,
2515 position,
2516 text,
2517 })
2518 }
2519
2520 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
2522 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
2523 }
2524
2525 pub fn add_overlay(
2533 &self,
2534 buffer_id: BufferId,
2535 namespace: Option<String>,
2536 range: Range<usize>,
2537 options: OverlayOptions,
2538 ) -> Result<(), String> {
2539 self.send_command(PluginCommand::AddOverlay {
2540 buffer_id,
2541 namespace: namespace.map(OverlayNamespace::from_string),
2542 range,
2543 options,
2544 })
2545 }
2546
2547 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
2549 self.send_command(PluginCommand::RemoveOverlay {
2550 buffer_id,
2551 handle: OverlayHandle::from_string(handle),
2552 })
2553 }
2554
2555 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
2557 self.send_command(PluginCommand::ClearNamespace {
2558 buffer_id,
2559 namespace: OverlayNamespace::from_string(namespace),
2560 })
2561 }
2562
2563 pub fn clear_overlays_in_range(
2566 &self,
2567 buffer_id: BufferId,
2568 start: usize,
2569 end: usize,
2570 ) -> Result<(), String> {
2571 self.send_command(PluginCommand::ClearOverlaysInRange {
2572 buffer_id,
2573 start,
2574 end,
2575 })
2576 }
2577
2578 pub fn set_status(&self, message: String) -> Result<(), String> {
2580 self.send_command(PluginCommand::SetStatus { message })
2581 }
2582
2583 pub fn open_file_at_location(
2586 &self,
2587 path: PathBuf,
2588 line: Option<usize>,
2589 column: Option<usize>,
2590 ) -> Result<(), String> {
2591 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
2592 }
2593
2594 pub fn open_file_in_split(
2599 &self,
2600 split_id: usize,
2601 path: PathBuf,
2602 line: Option<usize>,
2603 column: Option<usize>,
2604 ) -> Result<(), String> {
2605 self.send_command(PluginCommand::OpenFileInSplit {
2606 split_id,
2607 path,
2608 line,
2609 column,
2610 })
2611 }
2612
2613 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
2616 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
2617 }
2618
2619 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
2622 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
2623 }
2624
2625 pub fn set_prompt_input_sync(&self, sync: bool) -> Result<(), String> {
2627 self.send_command(PluginCommand::SetPromptInputSync { sync })
2628 }
2629
2630 pub fn add_menu_item(
2632 &self,
2633 menu_label: String,
2634 item: MenuItem,
2635 position: MenuPosition,
2636 ) -> Result<(), String> {
2637 self.send_command(PluginCommand::AddMenuItem {
2638 menu_label,
2639 item,
2640 position,
2641 })
2642 }
2643
2644 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
2646 self.send_command(PluginCommand::AddMenu { menu, position })
2647 }
2648
2649 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
2651 self.send_command(PluginCommand::RemoveMenuItem {
2652 menu_label,
2653 item_label,
2654 })
2655 }
2656
2657 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
2659 self.send_command(PluginCommand::RemoveMenu { menu_label })
2660 }
2661
2662 pub fn create_virtual_buffer(
2669 &self,
2670 name: String,
2671 mode: String,
2672 read_only: bool,
2673 ) -> Result<(), String> {
2674 self.send_command(PluginCommand::CreateVirtualBuffer {
2675 name,
2676 mode,
2677 read_only,
2678 })
2679 }
2680
2681 pub fn create_virtual_buffer_with_content(
2687 &self,
2688 name: String,
2689 mode: String,
2690 read_only: bool,
2691 entries: Vec<TextPropertyEntry>,
2692 ) -> Result<(), String> {
2693 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
2694 name,
2695 mode,
2696 read_only,
2697 entries,
2698 show_line_numbers: true,
2699 show_cursors: true,
2700 editing_disabled: false,
2701 hidden_from_tabs: false,
2702 request_id: None,
2703 })
2704 }
2705
2706 pub fn set_virtual_buffer_content(
2710 &self,
2711 buffer_id: BufferId,
2712 entries: Vec<TextPropertyEntry>,
2713 ) -> Result<(), String> {
2714 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
2715 }
2716
2717 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
2721 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
2722 }
2723
2724 pub fn define_mode(
2728 &self,
2729 name: String,
2730 bindings: Vec<(String, String)>,
2731 read_only: bool,
2732 allow_text_input: bool,
2733 ) -> Result<(), String> {
2734 self.send_command(PluginCommand::DefineMode {
2735 name,
2736 bindings,
2737 read_only,
2738 allow_text_input,
2739 plugin_name: None,
2740 })
2741 }
2742
2743 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
2745 self.send_command(PluginCommand::ShowBuffer { buffer_id })
2746 }
2747
2748 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
2750 self.send_command(PluginCommand::SetSplitScroll {
2751 split_id: SplitId(split_id),
2752 top_byte,
2753 })
2754 }
2755
2756 pub fn get_highlights(
2758 &self,
2759 buffer_id: BufferId,
2760 range: Range<usize>,
2761 request_id: u64,
2762 ) -> Result<(), String> {
2763 self.send_command(PluginCommand::RequestHighlights {
2764 buffer_id,
2765 range,
2766 request_id,
2767 })
2768 }
2769
2770 pub fn get_active_buffer_id(&self) -> BufferId {
2774 let snapshot = self.state_snapshot.read().unwrap();
2775 snapshot.active_buffer_id
2776 }
2777
2778 pub fn get_active_split_id(&self) -> usize {
2780 let snapshot = self.state_snapshot.read().unwrap();
2781 snapshot.active_split_id
2782 }
2783
2784 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
2786 let snapshot = self.state_snapshot.read().unwrap();
2787 snapshot.buffers.get(&buffer_id).cloned()
2788 }
2789
2790 pub fn list_buffers(&self) -> Vec<BufferInfo> {
2792 let snapshot = self.state_snapshot.read().unwrap();
2793 snapshot.buffers.values().cloned().collect()
2794 }
2795
2796 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
2798 let snapshot = self.state_snapshot.read().unwrap();
2799 snapshot.primary_cursor.clone()
2800 }
2801
2802 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
2804 let snapshot = self.state_snapshot.read().unwrap();
2805 snapshot.all_cursors.clone()
2806 }
2807
2808 pub fn get_viewport(&self) -> Option<ViewportInfo> {
2810 let snapshot = self.state_snapshot.read().unwrap();
2811 snapshot.viewport.clone()
2812 }
2813
2814 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
2816 Arc::clone(&self.state_snapshot)
2817 }
2818}
2819
2820impl Clone for PluginApi {
2821 fn clone(&self) -> Self {
2822 Self {
2823 hooks: Arc::clone(&self.hooks),
2824 commands: Arc::clone(&self.commands),
2825 command_sender: self.command_sender.clone(),
2826 state_snapshot: Arc::clone(&self.state_snapshot),
2827 }
2828 }
2829}
2830
2831#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2845#[serde(rename_all = "camelCase", deny_unknown_fields)]
2846#[ts(export, rename_all = "camelCase")]
2847pub struct TsCompletionCandidate {
2848 pub label: String,
2850
2851 #[serde(skip_serializing_if = "Option::is_none")]
2853 pub insert_text: Option<String>,
2854
2855 #[serde(skip_serializing_if = "Option::is_none")]
2857 pub detail: Option<String>,
2858
2859 #[serde(skip_serializing_if = "Option::is_none")]
2861 pub icon: Option<String>,
2862
2863 #[serde(default)]
2865 pub score: i64,
2866
2867 #[serde(default)]
2869 pub is_snippet: bool,
2870
2871 #[serde(skip_serializing_if = "Option::is_none")]
2873 pub provider_data: Option<String>,
2874}
2875
2876#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2881#[serde(rename_all = "camelCase")]
2882#[ts(export, rename_all = "camelCase")]
2883pub struct TsCompletionContext {
2884 pub prefix: String,
2886
2887 pub cursor_byte: usize,
2889
2890 pub word_start_byte: usize,
2892
2893 pub buffer_len: usize,
2895
2896 pub is_large_file: bool,
2898
2899 pub text_around_cursor: String,
2902
2903 pub cursor_offset_in_text: usize,
2905
2906 #[serde(skip_serializing_if = "Option::is_none")]
2908 pub language_id: Option<String>,
2909}
2910
2911#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2913#[serde(rename_all = "camelCase", deny_unknown_fields)]
2914#[ts(export, rename_all = "camelCase")]
2915pub struct TsCompletionProviderRegistration {
2916 pub id: String,
2918
2919 pub display_name: String,
2921
2922 #[serde(default = "default_plugin_provider_priority")]
2925 pub priority: u32,
2926
2927 #[serde(default)]
2930 pub language_ids: Vec<String>,
2931}
2932
2933fn default_plugin_provider_priority() -> u32 {
2934 50
2935}
2936
2937#[cfg(test)]
2938mod tests {
2939 use super::*;
2940
2941 #[test]
2942 fn test_plugin_api_creation() {
2943 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2944 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2945 let (tx, _rx) = std::sync::mpsc::channel();
2946 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2947
2948 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2949
2950 let _clone = api.clone();
2952 }
2953
2954 #[test]
2955 fn test_register_hook() {
2956 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2957 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2958 let (tx, _rx) = std::sync::mpsc::channel();
2959 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2960
2961 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
2962
2963 api.register_hook("test-hook", Box::new(|_| true));
2964
2965 let hook_registry = hooks.read().unwrap();
2966 assert_eq!(hook_registry.hook_count("test-hook"), 1);
2967 }
2968
2969 #[test]
2970 fn test_send_command() {
2971 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
2972 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
2973 let (tx, rx) = std::sync::mpsc::channel();
2974 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
2975
2976 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
2977
2978 let result = api.insert_text(BufferId(1), 0, "test".to_string());
2979 assert!(result.is_ok());
2980
2981 let received = rx.try_recv();
2983 assert!(received.is_ok());
2984
2985 match received.unwrap() {
2986 PluginCommand::InsertText {
2987 buffer_id,
2988 position,
2989 text,
2990 } => {
2991 assert_eq!(buffer_id.0, 1);
2992 assert_eq!(position, 0);
2993 assert_eq!(text, "test");
2994 }
2995 _ => panic!("Wrong command type"),
2996 }
2997 }
2998
2999 #[test]
3000 fn test_add_overlay_command() {
3001 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3002 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3003 let (tx, rx) = std::sync::mpsc::channel();
3004 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3005
3006 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3007
3008 let result = api.add_overlay(
3009 BufferId(1),
3010 Some("test-overlay".to_string()),
3011 0..10,
3012 OverlayOptions {
3013 fg: Some(OverlayColorSpec::ThemeKey("ui.status_bar_fg".to_string())),
3014 bg: None,
3015 underline: true,
3016 bold: false,
3017 italic: false,
3018 strikethrough: false,
3019 extend_to_line_end: false,
3020 url: None,
3021 },
3022 );
3023 assert!(result.is_ok());
3024
3025 let received = rx.try_recv().unwrap();
3026 match received {
3027 PluginCommand::AddOverlay {
3028 buffer_id,
3029 namespace,
3030 range,
3031 options,
3032 } => {
3033 assert_eq!(buffer_id.0, 1);
3034 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
3035 assert_eq!(range, 0..10);
3036 assert!(matches!(
3037 options.fg,
3038 Some(OverlayColorSpec::ThemeKey(ref k)) if k == "ui.status_bar_fg"
3039 ));
3040 assert!(options.bg.is_none());
3041 assert!(options.underline);
3042 assert!(!options.bold);
3043 assert!(!options.italic);
3044 assert!(!options.extend_to_line_end);
3045 }
3046 _ => panic!("Wrong command type"),
3047 }
3048 }
3049
3050 #[test]
3051 fn test_set_status_command() {
3052 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3053 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3054 let (tx, rx) = std::sync::mpsc::channel();
3055 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3056
3057 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3058
3059 let result = api.set_status("Test status".to_string());
3060 assert!(result.is_ok());
3061
3062 let received = rx.try_recv().unwrap();
3063 match received {
3064 PluginCommand::SetStatus { message } => {
3065 assert_eq!(message, "Test status");
3066 }
3067 _ => panic!("Wrong command type"),
3068 }
3069 }
3070
3071 #[test]
3072 fn test_get_active_buffer_id() {
3073 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3074 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3075 let (tx, _rx) = std::sync::mpsc::channel();
3076 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3077
3078 {
3080 let mut snapshot = state_snapshot.write().unwrap();
3081 snapshot.active_buffer_id = BufferId(5);
3082 }
3083
3084 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3085
3086 let active_id = api.get_active_buffer_id();
3087 assert_eq!(active_id.0, 5);
3088 }
3089
3090 #[test]
3091 fn test_get_buffer_info() {
3092 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3093 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3094 let (tx, _rx) = std::sync::mpsc::channel();
3095 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3096
3097 {
3099 let mut snapshot = state_snapshot.write().unwrap();
3100 let buffer_info = BufferInfo {
3101 id: BufferId(1),
3102 path: Some(std::path::PathBuf::from("/test/file.txt")),
3103 modified: true,
3104 length: 100,
3105 is_virtual: false,
3106 view_mode: "source".to_string(),
3107 is_composing_in_any_split: false,
3108 compose_width: None,
3109 language: "text".to_string(),
3110 };
3111 snapshot.buffers.insert(BufferId(1), buffer_info);
3112 }
3113
3114 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3115
3116 let info = api.get_buffer_info(BufferId(1));
3117 assert!(info.is_some());
3118 let info = info.unwrap();
3119 assert_eq!(info.id.0, 1);
3120 assert_eq!(
3121 info.path.as_ref().unwrap().to_str().unwrap(),
3122 "/test/file.txt"
3123 );
3124 assert!(info.modified);
3125 assert_eq!(info.length, 100);
3126
3127 let no_info = api.get_buffer_info(BufferId(999));
3129 assert!(no_info.is_none());
3130 }
3131
3132 #[test]
3133 fn test_list_buffers() {
3134 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3135 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3136 let (tx, _rx) = std::sync::mpsc::channel();
3137 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3138
3139 {
3141 let mut snapshot = state_snapshot.write().unwrap();
3142 snapshot.buffers.insert(
3143 BufferId(1),
3144 BufferInfo {
3145 id: BufferId(1),
3146 path: Some(std::path::PathBuf::from("/file1.txt")),
3147 modified: false,
3148 length: 50,
3149 is_virtual: false,
3150 view_mode: "source".to_string(),
3151 is_composing_in_any_split: false,
3152 compose_width: None,
3153 language: "text".to_string(),
3154 },
3155 );
3156 snapshot.buffers.insert(
3157 BufferId(2),
3158 BufferInfo {
3159 id: BufferId(2),
3160 path: Some(std::path::PathBuf::from("/file2.txt")),
3161 modified: true,
3162 length: 100,
3163 is_virtual: false,
3164 view_mode: "source".to_string(),
3165 is_composing_in_any_split: false,
3166 compose_width: None,
3167 language: "text".to_string(),
3168 },
3169 );
3170 snapshot.buffers.insert(
3171 BufferId(3),
3172 BufferInfo {
3173 id: BufferId(3),
3174 path: None,
3175 modified: false,
3176 length: 0,
3177 is_virtual: true,
3178 view_mode: "source".to_string(),
3179 is_composing_in_any_split: false,
3180 compose_width: None,
3181 language: "text".to_string(),
3182 },
3183 );
3184 }
3185
3186 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3187
3188 let buffers = api.list_buffers();
3189 assert_eq!(buffers.len(), 3);
3190
3191 assert!(buffers.iter().any(|b| b.id.0 == 1));
3193 assert!(buffers.iter().any(|b| b.id.0 == 2));
3194 assert!(buffers.iter().any(|b| b.id.0 == 3));
3195 }
3196
3197 #[test]
3198 fn test_get_primary_cursor() {
3199 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3200 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3201 let (tx, _rx) = std::sync::mpsc::channel();
3202 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3203
3204 {
3206 let mut snapshot = state_snapshot.write().unwrap();
3207 snapshot.primary_cursor = Some(CursorInfo {
3208 position: 42,
3209 selection: Some(10..42),
3210 });
3211 }
3212
3213 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3214
3215 let cursor = api.get_primary_cursor();
3216 assert!(cursor.is_some());
3217 let cursor = cursor.unwrap();
3218 assert_eq!(cursor.position, 42);
3219 assert_eq!(cursor.selection, Some(10..42));
3220 }
3221
3222 #[test]
3223 fn test_get_all_cursors() {
3224 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3225 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3226 let (tx, _rx) = std::sync::mpsc::channel();
3227 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3228
3229 {
3231 let mut snapshot = state_snapshot.write().unwrap();
3232 snapshot.all_cursors = vec![
3233 CursorInfo {
3234 position: 10,
3235 selection: None,
3236 },
3237 CursorInfo {
3238 position: 20,
3239 selection: Some(15..20),
3240 },
3241 CursorInfo {
3242 position: 30,
3243 selection: Some(25..30),
3244 },
3245 ];
3246 }
3247
3248 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3249
3250 let cursors = api.get_all_cursors();
3251 assert_eq!(cursors.len(), 3);
3252 assert_eq!(cursors[0].position, 10);
3253 assert_eq!(cursors[0].selection, None);
3254 assert_eq!(cursors[1].position, 20);
3255 assert_eq!(cursors[1].selection, Some(15..20));
3256 assert_eq!(cursors[2].position, 30);
3257 assert_eq!(cursors[2].selection, Some(25..30));
3258 }
3259
3260 #[test]
3261 fn test_get_viewport() {
3262 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3263 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3264 let (tx, _rx) = std::sync::mpsc::channel();
3265 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3266
3267 {
3269 let mut snapshot = state_snapshot.write().unwrap();
3270 snapshot.viewport = Some(ViewportInfo {
3271 top_byte: 100,
3272 top_line: Some(5),
3273 left_column: 5,
3274 width: 80,
3275 height: 24,
3276 });
3277 }
3278
3279 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3280
3281 let viewport = api.get_viewport();
3282 assert!(viewport.is_some());
3283 let viewport = viewport.unwrap();
3284 assert_eq!(viewport.top_byte, 100);
3285 assert_eq!(viewport.left_column, 5);
3286 assert_eq!(viewport.width, 80);
3287 assert_eq!(viewport.height, 24);
3288 }
3289
3290 #[test]
3291 fn test_composite_buffer_options_rejects_unknown_fields() {
3292 let valid_json = r#"{
3294 "name": "test",
3295 "mode": "diff",
3296 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3297 "sources": [{"bufferId": 1, "label": "old"}]
3298 }"#;
3299 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
3300 assert!(
3301 result.is_ok(),
3302 "Valid JSON should parse: {:?}",
3303 result.err()
3304 );
3305
3306 let invalid_json = r#"{
3308 "name": "test",
3309 "mode": "diff",
3310 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3311 "sources": [{"buffer_id": 1, "label": "old"}]
3312 }"#;
3313 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
3314 assert!(
3315 result.is_err(),
3316 "JSON with unknown field should fail to parse"
3317 );
3318 let err = result.unwrap_err().to_string();
3319 assert!(
3320 err.contains("unknown field") || err.contains("buffer_id"),
3321 "Error should mention unknown field: {}",
3322 err
3323 );
3324 }
3325
3326 #[test]
3327 fn test_composite_hunk_rejects_unknown_fields() {
3328 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3330 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
3331 assert!(
3332 result.is_ok(),
3333 "Valid JSON should parse: {:?}",
3334 result.err()
3335 );
3336
3337 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3339 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
3340 assert!(
3341 result.is_err(),
3342 "JSON with unknown field should fail to parse"
3343 );
3344 let err = result.unwrap_err().to_string();
3345 assert!(
3346 err.contains("unknown field") || err.contains("old_start"),
3347 "Error should mention unknown field: {}",
3348 err
3349 );
3350 }
3351
3352 #[test]
3353 fn test_plugin_response_line_end_position() {
3354 let response = PluginResponse::LineEndPosition {
3355 request_id: 42,
3356 position: Some(100),
3357 };
3358 let json = serde_json::to_string(&response).unwrap();
3359 assert!(json.contains("LineEndPosition"));
3360 assert!(json.contains("42"));
3361 assert!(json.contains("100"));
3362
3363 let response_none = PluginResponse::LineEndPosition {
3365 request_id: 1,
3366 position: None,
3367 };
3368 let json_none = serde_json::to_string(&response_none).unwrap();
3369 assert!(json_none.contains("null"));
3370 }
3371
3372 #[test]
3373 fn test_plugin_response_buffer_line_count() {
3374 let response = PluginResponse::BufferLineCount {
3375 request_id: 99,
3376 count: Some(500),
3377 };
3378 let json = serde_json::to_string(&response).unwrap();
3379 assert!(json.contains("BufferLineCount"));
3380 assert!(json.contains("99"));
3381 assert!(json.contains("500"));
3382 }
3383
3384 #[test]
3385 fn test_plugin_command_get_line_end_position() {
3386 let command = PluginCommand::GetLineEndPosition {
3387 buffer_id: BufferId(1),
3388 line: 10,
3389 request_id: 123,
3390 };
3391 let json = serde_json::to_string(&command).unwrap();
3392 assert!(json.contains("GetLineEndPosition"));
3393 assert!(json.contains("10"));
3394 }
3395
3396 #[test]
3397 fn test_plugin_command_get_buffer_line_count() {
3398 let command = PluginCommand::GetBufferLineCount {
3399 buffer_id: BufferId(0),
3400 request_id: 456,
3401 };
3402 let json = serde_json::to_string(&command).unwrap();
3403 assert!(json.contains("GetBufferLineCount"));
3404 assert!(json.contains("456"));
3405 }
3406
3407 #[test]
3408 fn test_plugin_command_scroll_to_line_center() {
3409 let command = PluginCommand::ScrollToLineCenter {
3410 split_id: SplitId(1),
3411 buffer_id: BufferId(2),
3412 line: 50,
3413 };
3414 let json = serde_json::to_string(&command).unwrap();
3415 assert!(json.contains("ScrollToLineCenter"));
3416 assert!(json.contains("50"));
3417 }
3418}