1use crate::command::{Command, Suggestion};
47use crate::file_explorer::FileExplorerDecoration;
48use crate::hooks::{HookCallback, HookRegistry};
49use crate::menu::{Menu, MenuItem};
50use crate::overlay::{OverlayHandle, OverlayNamespace};
51use crate::text_property::{TextProperty, TextPropertyEntry};
52use crate::BufferId;
53use crate::SplitId;
54use crate::TerminalId;
55use lsp_types;
56use serde::{Deserialize, Serialize};
57use serde_json::Value as JsonValue;
58use std::collections::HashMap;
59use std::ops::Range;
60use std::path::PathBuf;
61use std::sync::{Arc, RwLock};
62use ts_rs::TS;
63
64pub struct CommandRegistry {
68 commands: std::sync::RwLock<Vec<Command>>,
69}
70
71impl CommandRegistry {
72 pub fn new() -> Self {
74 Self {
75 commands: std::sync::RwLock::new(Vec::new()),
76 }
77 }
78
79 pub fn register(&self, command: Command) {
81 let mut commands = self.commands.write().unwrap();
82 commands.retain(|c| c.name != command.name);
83 commands.push(command);
84 }
85
86 pub fn unregister(&self, name: &str) {
88 let mut commands = self.commands.write().unwrap();
89 commands.retain(|c| c.name != name);
90 }
91}
92
93impl Default for CommandRegistry {
94 fn default() -> Self {
95 Self::new()
96 }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, TS)]
105#[ts(export)]
106pub struct JsCallbackId(pub u64);
107
108impl JsCallbackId {
109 pub fn new(id: u64) -> Self {
111 Self(id)
112 }
113
114 pub fn as_u64(self) -> u64 {
116 self.0
117 }
118}
119
120impl From<u64> for JsCallbackId {
121 fn from(id: u64) -> Self {
122 Self(id)
123 }
124}
125
126impl From<JsCallbackId> for u64 {
127 fn from(id: JsCallbackId) -> u64 {
128 id.0
129 }
130}
131
132impl std::fmt::Display for JsCallbackId {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 write!(f, "{}", self.0)
135 }
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize, TS)]
140#[serde(rename_all = "camelCase")]
141#[ts(export, rename_all = "camelCase")]
142pub struct TerminalResult {
143 #[ts(type = "number")]
145 pub buffer_id: u64,
146 #[ts(type = "number")]
148 pub terminal_id: u64,
149 #[ts(type = "number | null")]
151 pub split_id: Option<u64>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize, TS)]
156#[serde(rename_all = "camelCase")]
157#[ts(export, rename_all = "camelCase")]
158pub struct VirtualBufferResult {
159 #[ts(type = "number")]
161 pub buffer_id: u64,
162 #[ts(type = "number | null")]
164 pub split_id: Option<u64>,
165}
166
167#[derive(Debug, Clone, Serialize, Deserialize, TS)]
169#[serde(rename_all = "camelCase")]
170#[ts(export, rename_all = "camelCase")]
171pub struct BufferGroupResult {
172 #[ts(type = "number")]
174 pub group_id: u64,
175 #[ts(type = "Record<string, number>")]
177 pub panels: HashMap<String, u64>,
178}
179
180#[derive(Debug, Clone, Serialize, Deserialize, TS)]
182#[ts(export)]
183pub enum PluginResponse {
184 VirtualBufferCreated {
186 request_id: u64,
187 buffer_id: BufferId,
188 split_id: Option<SplitId>,
189 },
190 TerminalCreated {
192 request_id: u64,
193 buffer_id: BufferId,
194 terminal_id: TerminalId,
195 split_id: Option<SplitId>,
196 },
197 LspRequest {
199 request_id: u64,
200 #[ts(type = "any")]
201 result: Result<JsonValue, String>,
202 },
203 HighlightsComputed {
205 request_id: u64,
206 spans: Vec<TsHighlightSpan>,
207 },
208 BufferText {
210 request_id: u64,
211 text: Result<String, String>,
212 },
213 LineStartPosition {
215 request_id: u64,
216 position: Option<usize>,
218 },
219 LineEndPosition {
221 request_id: u64,
222 position: Option<usize>,
224 },
225 BufferLineCount {
227 request_id: u64,
228 count: Option<usize>,
230 },
231 CompositeBufferCreated {
233 request_id: u64,
234 buffer_id: BufferId,
235 },
236 SplitByLabel {
238 request_id: u64,
239 split_id: Option<SplitId>,
240 },
241}
242
243#[derive(Debug, Clone, Serialize, Deserialize, TS)]
245#[ts(export)]
246pub enum PluginAsyncMessage {
247 ProcessOutput {
249 process_id: u64,
251 stdout: String,
253 stderr: String,
255 exit_code: i32,
257 },
258 DelayComplete {
260 callback_id: u64,
262 },
263 ProcessStdout { process_id: u64, data: String },
265 ProcessStderr { process_id: u64, data: String },
267 ProcessExit {
269 process_id: u64,
270 callback_id: u64,
271 exit_code: i32,
272 },
273 LspResponse {
275 language: String,
276 request_id: u64,
277 #[ts(type = "any")]
278 result: Result<JsonValue, String>,
279 },
280 PluginResponse(crate::api::PluginResponse),
282
283 GrepStreamingProgress {
285 search_id: u64,
287 matches_json: String,
289 },
290
291 GrepStreamingComplete {
293 search_id: u64,
295 callback_id: u64,
297 total_matches: usize,
299 truncated: bool,
301 },
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize, TS)]
306#[ts(export)]
307pub struct CursorInfo {
308 pub position: usize,
310 #[cfg_attr(
312 feature = "plugins",
313 ts(type = "{ start: number; end: number } | null")
314 )]
315 pub selection: Option<Range<usize>>,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize, TS)]
320#[serde(deny_unknown_fields)]
321#[ts(export)]
322pub struct ActionSpec {
323 pub action: String,
325 #[serde(default = "default_action_count")]
327 pub count: u32,
328}
329
330fn default_action_count() -> u32 {
331 1
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize, TS)]
336#[ts(export)]
337pub struct BufferInfo {
338 #[ts(type = "number")]
340 pub id: BufferId,
341 #[serde(serialize_with = "serialize_path")]
343 #[ts(type = "string")]
344 pub path: Option<PathBuf>,
345 pub modified: bool,
347 pub length: usize,
349 pub is_virtual: bool,
351 pub view_mode: String,
353 pub is_composing_in_any_split: bool,
358 pub compose_width: Option<u16>,
360 pub language: String,
362}
363
364fn serialize_path<S: serde::Serializer>(path: &Option<PathBuf>, s: S) -> Result<S::Ok, S::Error> {
365 s.serialize_str(
366 &path
367 .as_ref()
368 .map(|p| p.to_string_lossy().to_string())
369 .unwrap_or_default(),
370 )
371}
372
373fn serialize_ranges_as_tuples<S>(ranges: &[Range<usize>], serializer: S) -> Result<S::Ok, S::Error>
375where
376 S: serde::Serializer,
377{
378 use serde::ser::SerializeSeq;
379 let mut seq = serializer.serialize_seq(Some(ranges.len()))?;
380 for range in ranges {
381 seq.serialize_element(&(range.start, range.end))?;
382 }
383 seq.end()
384}
385
386#[derive(Debug, Clone, Serialize, Deserialize, TS)]
388#[ts(export)]
389pub struct BufferSavedDiff {
390 pub equal: bool,
391 #[serde(serialize_with = "serialize_ranges_as_tuples")]
392 #[ts(type = "Array<[number, number]>")]
393 pub byte_ranges: Vec<Range<usize>>,
394}
395
396#[derive(Debug, Clone, Serialize, Deserialize, TS)]
398#[serde(rename_all = "camelCase")]
399#[ts(export, rename_all = "camelCase")]
400pub struct ViewportInfo {
401 pub top_byte: usize,
403 pub top_line: Option<usize>,
405 pub left_column: usize,
407 pub width: u16,
409 pub height: u16,
411}
412
413#[derive(Debug, Clone, Serialize, Deserialize, TS)]
415#[serde(rename_all = "camelCase")]
416#[ts(export, rename_all = "camelCase")]
417pub struct LayoutHints {
418 #[ts(optional)]
420 pub compose_width: Option<u16>,
421 #[ts(optional)]
423 pub column_guides: Option<Vec<u16>>,
424}
425
426#[derive(Debug, Clone, Serialize, Deserialize, TS)]
441#[serde(untagged)]
442#[ts(export)]
443pub enum OverlayColorSpec {
444 #[ts(type = "[number, number, number]")]
446 Rgb(u8, u8, u8),
447 ThemeKey(String),
449}
450
451impl OverlayColorSpec {
452 pub fn rgb(r: u8, g: u8, b: u8) -> Self {
454 Self::Rgb(r, g, b)
455 }
456
457 pub fn theme_key(key: impl Into<String>) -> Self {
459 Self::ThemeKey(key.into())
460 }
461
462 pub fn as_rgb(&self) -> Option<(u8, u8, u8)> {
464 match self {
465 Self::Rgb(r, g, b) => Some((*r, *g, *b)),
466 Self::ThemeKey(_) => None,
467 }
468 }
469
470 pub fn as_theme_key(&self) -> Option<&str> {
472 match self {
473 Self::ThemeKey(key) => Some(key),
474 Self::Rgb(_, _, _) => None,
475 }
476 }
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize, TS)]
484#[serde(deny_unknown_fields, rename_all = "camelCase")]
485#[ts(export, rename_all = "camelCase")]
486#[derive(Default)]
487pub struct OverlayOptions {
488 #[serde(default, skip_serializing_if = "Option::is_none")]
490 pub fg: Option<OverlayColorSpec>,
491
492 #[serde(default, skip_serializing_if = "Option::is_none")]
494 pub bg: Option<OverlayColorSpec>,
495
496 #[serde(default)]
498 pub underline: bool,
499
500 #[serde(default)]
502 pub bold: bool,
503
504 #[serde(default)]
506 pub italic: bool,
507
508 #[serde(default)]
510 pub strikethrough: bool,
511
512 #[serde(default)]
514 pub extend_to_line_end: bool,
515
516 #[serde(default, skip_serializing_if = "Option::is_none")]
520 pub url: Option<String>,
521}
522
523#[derive(Debug, Clone, Serialize, Deserialize, TS)]
529#[serde(deny_unknown_fields)]
530#[ts(export, rename = "TsCompositeLayoutConfig")]
531pub struct CompositeLayoutConfig {
532 #[serde(rename = "type")]
534 #[ts(rename = "type")]
535 pub layout_type: String,
536 #[serde(default)]
538 #[ts(optional)]
539 pub ratios: Option<Vec<f32>>,
540 #[serde(default = "default_true", rename = "showSeparator")]
542 #[ts(rename = "showSeparator")]
543 pub show_separator: bool,
544 #[serde(default)]
546 #[ts(optional)]
547 pub spacing: Option<u16>,
548}
549
550fn default_true() -> bool {
551 true
552}
553
554#[derive(Debug, Clone, Serialize, Deserialize, TS)]
556#[serde(deny_unknown_fields)]
557#[ts(export, rename = "TsCompositeSourceConfig")]
558pub struct CompositeSourceConfig {
559 #[serde(rename = "bufferId")]
561 #[ts(rename = "bufferId")]
562 pub buffer_id: usize,
563 pub label: String,
565 #[serde(default)]
567 pub editable: bool,
568 #[serde(default)]
570 pub style: Option<CompositePaneStyle>,
571}
572
573#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
575#[serde(deny_unknown_fields)]
576#[ts(export, rename = "TsCompositePaneStyle")]
577pub struct CompositePaneStyle {
578 #[serde(default, rename = "addBg")]
581 #[ts(optional, rename = "addBg", type = "[number, number, number]")]
582 pub add_bg: Option<[u8; 3]>,
583 #[serde(default, rename = "removeBg")]
585 #[ts(optional, rename = "removeBg", type = "[number, number, number]")]
586 pub remove_bg: Option<[u8; 3]>,
587 #[serde(default, rename = "modifyBg")]
589 #[ts(optional, rename = "modifyBg", type = "[number, number, number]")]
590 pub modify_bg: Option<[u8; 3]>,
591 #[serde(default, rename = "gutterStyle")]
593 #[ts(optional, rename = "gutterStyle")]
594 pub gutter_style: Option<String>,
595}
596
597#[derive(Debug, Clone, Serialize, Deserialize, TS)]
599#[serde(deny_unknown_fields)]
600#[ts(export, rename = "TsCompositeHunk")]
601pub struct CompositeHunk {
602 #[serde(rename = "oldStart")]
604 #[ts(rename = "oldStart")]
605 pub old_start: usize,
606 #[serde(rename = "oldCount")]
608 #[ts(rename = "oldCount")]
609 pub old_count: usize,
610 #[serde(rename = "newStart")]
612 #[ts(rename = "newStart")]
613 pub new_start: usize,
614 #[serde(rename = "newCount")]
616 #[ts(rename = "newCount")]
617 pub new_count: usize,
618}
619
620#[derive(Debug, Clone, Serialize, Deserialize, TS)]
622#[serde(deny_unknown_fields)]
623#[ts(export, rename = "TsCreateCompositeBufferOptions")]
624pub struct CreateCompositeBufferOptions {
625 #[serde(default)]
627 pub name: String,
628 #[serde(default)]
630 pub mode: String,
631 pub layout: CompositeLayoutConfig,
633 pub sources: Vec<CompositeSourceConfig>,
635 #[serde(default)]
637 pub hunks: Option<Vec<CompositeHunk>>,
638 #[serde(default, rename = "initialFocusHunk")]
642 #[ts(optional, rename = "initialFocusHunk")]
643 pub initial_focus_hunk: Option<usize>,
644}
645
646#[derive(Debug, Clone, Serialize, Deserialize, TS)]
648#[ts(export)]
649pub enum ViewTokenWireKind {
650 Text(String),
651 Newline,
652 Space,
653 Break,
656 BinaryByte(u8),
660}
661
662#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
668#[serde(deny_unknown_fields)]
669#[ts(export)]
670pub struct ViewTokenStyle {
671 #[serde(default)]
673 #[ts(type = "[number, number, number] | null")]
674 pub fg: Option<(u8, u8, u8)>,
675 #[serde(default)]
677 #[ts(type = "[number, number, number] | null")]
678 pub bg: Option<(u8, u8, u8)>,
679 #[serde(default)]
681 pub bold: bool,
682 #[serde(default)]
684 pub italic: bool,
685}
686
687#[derive(Debug, Clone, Serialize, Deserialize, TS)]
689#[serde(deny_unknown_fields)]
690#[ts(export)]
691pub struct ViewTokenWire {
692 #[ts(type = "number | null")]
694 pub source_offset: Option<usize>,
695 pub kind: ViewTokenWireKind,
697 #[serde(default)]
699 #[ts(optional)]
700 pub style: Option<ViewTokenStyle>,
701}
702
703#[derive(Debug, Clone, Serialize, Deserialize, TS)]
705#[ts(export)]
706pub struct ViewTransformPayload {
707 pub range: Range<usize>,
709 pub tokens: Vec<ViewTokenWire>,
711 pub layout_hints: Option<LayoutHints>,
713}
714
715#[derive(Debug, Clone, Serialize, Deserialize, TS)]
718#[ts(export)]
719pub struct EditorStateSnapshot {
720 pub active_buffer_id: BufferId,
722 pub active_split_id: usize,
724 pub buffers: HashMap<BufferId, BufferInfo>,
726 pub buffer_saved_diffs: HashMap<BufferId, BufferSavedDiff>,
728 pub primary_cursor: Option<CursorInfo>,
730 pub all_cursors: Vec<CursorInfo>,
732 pub viewport: Option<ViewportInfo>,
734 pub buffer_cursor_positions: HashMap<BufferId, usize>,
736 pub buffer_text_properties: HashMap<BufferId, Vec<TextProperty>>,
738 pub selected_text: Option<String>,
741 pub clipboard: String,
743 pub working_dir: PathBuf,
745 #[ts(type = "any")]
748 pub diagnostics: HashMap<String, Vec<lsp_types::Diagnostic>>,
749 #[ts(type = "any")]
752 pub folding_ranges: HashMap<String, Vec<lsp_types::FoldingRange>>,
753 #[ts(type = "any")]
756 pub config: serde_json::Value,
757 #[ts(type = "any")]
760 pub user_config: serde_json::Value,
761 #[ts(type = "GrammarInfo[]")]
763 pub available_grammars: Vec<GrammarInfoSnapshot>,
764 pub editor_mode: Option<String>,
767
768 #[ts(type = "any")]
772 pub plugin_view_states: HashMap<BufferId, HashMap<String, serde_json::Value>>,
773
774 #[serde(skip)]
777 #[ts(skip)]
778 pub plugin_view_states_split: usize,
779
780 #[serde(skip)]
783 #[ts(skip)]
784 pub keybinding_labels: HashMap<String, String>,
785
786 #[ts(type = "any")]
793 pub plugin_global_states: HashMap<String, HashMap<String, serde_json::Value>>,
794}
795
796impl EditorStateSnapshot {
797 pub fn new() -> Self {
798 Self {
799 active_buffer_id: BufferId(0),
800 active_split_id: 0,
801 buffers: HashMap::new(),
802 buffer_saved_diffs: HashMap::new(),
803 primary_cursor: None,
804 all_cursors: Vec::new(),
805 viewport: None,
806 buffer_cursor_positions: HashMap::new(),
807 buffer_text_properties: HashMap::new(),
808 selected_text: None,
809 clipboard: String::new(),
810 working_dir: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
811 diagnostics: HashMap::new(),
812 folding_ranges: HashMap::new(),
813 config: serde_json::Value::Null,
814 user_config: serde_json::Value::Null,
815 available_grammars: Vec::new(),
816 editor_mode: None,
817 plugin_view_states: HashMap::new(),
818 plugin_view_states_split: 0,
819 keybinding_labels: HashMap::new(),
820 plugin_global_states: HashMap::new(),
821 }
822 }
823}
824
825impl Default for EditorStateSnapshot {
826 fn default() -> Self {
827 Self::new()
828 }
829}
830
831#[derive(Debug, Clone, Serialize, Deserialize, TS)]
833#[ts(export)]
834pub struct GrammarInfoSnapshot {
835 pub name: String,
837 pub source: String,
839 pub file_extensions: Vec<String>,
841 pub short_name: Option<String>,
843}
844
845#[derive(Debug, Clone, Serialize, Deserialize, TS)]
847#[ts(export)]
848pub enum MenuPosition {
849 Top,
851 Bottom,
853 Before(String),
855 After(String),
857}
858
859#[derive(Debug, Clone, Serialize, Deserialize, TS)]
861#[ts(export)]
862pub enum PluginCommand {
863 InsertText {
865 buffer_id: BufferId,
866 position: usize,
867 text: String,
868 },
869
870 DeleteRange {
872 buffer_id: BufferId,
873 range: Range<usize>,
874 },
875
876 AddOverlay {
881 buffer_id: BufferId,
882 namespace: Option<OverlayNamespace>,
883 range: Range<usize>,
884 options: OverlayOptions,
886 },
887
888 RemoveOverlay {
890 buffer_id: BufferId,
891 handle: OverlayHandle,
892 },
893
894 SetStatus { message: String },
896
897 ApplyTheme { theme_name: String },
899
900 ReloadConfig,
903
904 RegisterCommand { command: Command },
906
907 UnregisterCommand { name: String },
909
910 OpenFileInBackground { path: PathBuf },
912
913 InsertAtCursor { text: String },
915
916 SpawnProcess {
918 command: String,
919 args: Vec<String>,
920 cwd: Option<String>,
921 callback_id: JsCallbackId,
922 },
923
924 Delay {
926 callback_id: JsCallbackId,
927 duration_ms: u64,
928 },
929
930 SpawnBackgroundProcess {
934 process_id: u64,
936 command: String,
938 args: Vec<String>,
940 cwd: Option<String>,
942 callback_id: JsCallbackId,
944 },
945
946 KillBackgroundProcess { process_id: u64 },
948
949 SpawnProcessWait {
952 process_id: u64,
954 callback_id: JsCallbackId,
956 },
957
958 SetLayoutHints {
960 buffer_id: BufferId,
961 split_id: Option<SplitId>,
962 range: Range<usize>,
963 hints: LayoutHints,
964 },
965
966 SetLineNumbers { buffer_id: BufferId, enabled: bool },
968
969 SetViewMode { buffer_id: BufferId, mode: String },
971
972 SetLineWrap {
974 buffer_id: BufferId,
975 split_id: Option<SplitId>,
976 enabled: bool,
977 },
978
979 SubmitViewTransform {
981 buffer_id: BufferId,
982 split_id: Option<SplitId>,
983 payload: ViewTransformPayload,
984 },
985
986 ClearViewTransform {
988 buffer_id: BufferId,
989 split_id: Option<SplitId>,
990 },
991
992 SetViewState {
995 buffer_id: BufferId,
996 key: String,
997 #[ts(type = "any")]
998 value: Option<serde_json::Value>,
999 },
1000
1001 SetGlobalState {
1005 plugin_name: String,
1006 key: String,
1007 #[ts(type = "any")]
1008 value: Option<serde_json::Value>,
1009 },
1010
1011 ClearAllOverlays { buffer_id: BufferId },
1013
1014 ClearNamespace {
1016 buffer_id: BufferId,
1017 namespace: OverlayNamespace,
1018 },
1019
1020 ClearOverlaysInRange {
1023 buffer_id: BufferId,
1024 start: usize,
1025 end: usize,
1026 },
1027
1028 AddVirtualText {
1031 buffer_id: BufferId,
1032 virtual_text_id: String,
1033 position: usize,
1034 text: String,
1035 color: (u8, u8, u8),
1036 use_bg: bool, before: bool, },
1039
1040 RemoveVirtualText {
1042 buffer_id: BufferId,
1043 virtual_text_id: String,
1044 },
1045
1046 RemoveVirtualTextsByPrefix { buffer_id: BufferId, prefix: String },
1048
1049 ClearVirtualTexts { buffer_id: BufferId },
1051
1052 AddVirtualLine {
1056 buffer_id: BufferId,
1057 position: usize,
1059 text: String,
1061 fg_color: (u8, u8, u8),
1063 bg_color: Option<(u8, u8, u8)>,
1065 above: bool,
1067 namespace: String,
1069 priority: i32,
1071 },
1072
1073 ClearVirtualTextNamespace {
1076 buffer_id: BufferId,
1077 namespace: String,
1078 },
1079
1080 AddConceal {
1083 buffer_id: BufferId,
1084 namespace: OverlayNamespace,
1086 start: usize,
1088 end: usize,
1089 replacement: Option<String>,
1091 },
1092
1093 ClearConcealNamespace {
1095 buffer_id: BufferId,
1096 namespace: OverlayNamespace,
1097 },
1098
1099 ClearConcealsInRange {
1102 buffer_id: BufferId,
1103 start: usize,
1104 end: usize,
1105 },
1106
1107 AddSoftBreak {
1111 buffer_id: BufferId,
1112 namespace: OverlayNamespace,
1114 position: usize,
1116 indent: u16,
1118 },
1119
1120 ClearSoftBreakNamespace {
1122 buffer_id: BufferId,
1123 namespace: OverlayNamespace,
1124 },
1125
1126 ClearSoftBreaksInRange {
1128 buffer_id: BufferId,
1129 start: usize,
1130 end: usize,
1131 },
1132
1133 RefreshLines { buffer_id: BufferId },
1135
1136 RefreshAllLines,
1140
1141 HookCompleted { hook_name: String },
1145
1146 SetLineIndicator {
1149 buffer_id: BufferId,
1150 line: usize,
1152 namespace: String,
1154 symbol: String,
1156 color: (u8, u8, u8),
1158 priority: i32,
1160 },
1161
1162 SetLineIndicators {
1165 buffer_id: BufferId,
1166 lines: Vec<usize>,
1168 namespace: String,
1170 symbol: String,
1172 color: (u8, u8, u8),
1174 priority: i32,
1176 },
1177
1178 ClearLineIndicators {
1180 buffer_id: BufferId,
1181 namespace: String,
1183 },
1184
1185 SetFileExplorerDecorations {
1187 namespace: String,
1189 decorations: Vec<FileExplorerDecoration>,
1191 },
1192
1193 ClearFileExplorerDecorations {
1195 namespace: String,
1197 },
1198
1199 OpenFileAtLocation {
1202 path: PathBuf,
1203 line: Option<usize>, column: Option<usize>, },
1206
1207 OpenFileInSplit {
1210 split_id: usize,
1211 path: PathBuf,
1212 line: Option<usize>, column: Option<usize>, },
1215
1216 StartPrompt {
1219 label: String,
1220 prompt_type: String, },
1222
1223 StartPromptWithInitial {
1225 label: String,
1226 prompt_type: String,
1227 initial_value: String,
1228 },
1229
1230 StartPromptAsync {
1233 label: String,
1234 initial_value: String,
1235 callback_id: JsCallbackId,
1236 },
1237
1238 SetPromptSuggestions { suggestions: Vec<Suggestion> },
1241
1242 SetPromptInputSync { sync: bool },
1244
1245 AddMenuItem {
1248 menu_label: String,
1249 item: MenuItem,
1250 position: MenuPosition,
1251 },
1252
1253 AddMenu { menu: Menu, position: MenuPosition },
1255
1256 RemoveMenuItem {
1258 menu_label: String,
1259 item_label: String,
1260 },
1261
1262 RemoveMenu { menu_label: String },
1264
1265 CreateVirtualBuffer {
1267 name: String,
1269 mode: String,
1271 read_only: bool,
1273 },
1274
1275 CreateVirtualBufferWithContent {
1279 name: String,
1281 mode: String,
1283 read_only: bool,
1285 entries: Vec<TextPropertyEntry>,
1287 show_line_numbers: bool,
1289 show_cursors: bool,
1291 editing_disabled: bool,
1293 hidden_from_tabs: bool,
1295 request_id: Option<u64>,
1297 },
1298
1299 CreateVirtualBufferInSplit {
1302 name: String,
1304 mode: String,
1306 read_only: bool,
1308 entries: Vec<TextPropertyEntry>,
1310 ratio: f32,
1312 direction: Option<String>,
1314 panel_id: Option<String>,
1316 show_line_numbers: bool,
1318 show_cursors: bool,
1320 editing_disabled: bool,
1322 line_wrap: Option<bool>,
1324 before: bool,
1326 request_id: Option<u64>,
1328 },
1329
1330 SetVirtualBufferContent {
1332 buffer_id: BufferId,
1333 entries: Vec<TextPropertyEntry>,
1335 },
1336
1337 GetTextPropertiesAtCursor { buffer_id: BufferId },
1339
1340 CreateBufferGroup {
1343 name: String,
1345 mode: String,
1347 layout_json: String,
1349 request_id: Option<u64>,
1351 },
1352
1353 SetPanelContent {
1355 group_id: usize,
1357 panel_name: String,
1359 entries: Vec<TextPropertyEntry>,
1361 },
1362
1363 CloseBufferGroup { group_id: usize },
1365
1366 FocusPanel { group_id: usize, panel_name: String },
1368
1369 DefineMode {
1371 name: String,
1372 bindings: Vec<(String, String)>, read_only: bool,
1374 allow_text_input: bool,
1376 plugin_name: Option<String>,
1378 },
1379
1380 ShowBuffer { buffer_id: BufferId },
1382
1383 CreateVirtualBufferInExistingSplit {
1385 name: String,
1387 mode: String,
1389 read_only: bool,
1391 entries: Vec<TextPropertyEntry>,
1393 split_id: SplitId,
1395 show_line_numbers: bool,
1397 show_cursors: bool,
1399 editing_disabled: bool,
1401 line_wrap: Option<bool>,
1403 request_id: Option<u64>,
1405 },
1406
1407 CloseBuffer { buffer_id: BufferId },
1409
1410 CreateCompositeBuffer {
1413 name: String,
1415 mode: String,
1417 layout: CompositeLayoutConfig,
1419 sources: Vec<CompositeSourceConfig>,
1421 hunks: Option<Vec<CompositeHunk>>,
1423 initial_focus_hunk: Option<usize>,
1425 request_id: Option<u64>,
1427 },
1428
1429 UpdateCompositeAlignment {
1431 buffer_id: BufferId,
1432 hunks: Vec<CompositeHunk>,
1433 },
1434
1435 CloseCompositeBuffer { buffer_id: BufferId },
1437
1438 FlushLayout,
1445
1446 CompositeNextHunk { buffer_id: BufferId },
1448
1449 CompositePrevHunk { buffer_id: BufferId },
1451
1452 FocusSplit { split_id: SplitId },
1454
1455 SetSplitBuffer {
1457 split_id: SplitId,
1458 buffer_id: BufferId,
1459 },
1460
1461 SetSplitScroll { split_id: SplitId, top_byte: usize },
1463
1464 RequestHighlights {
1466 buffer_id: BufferId,
1467 range: Range<usize>,
1468 request_id: u64,
1469 },
1470
1471 CloseSplit { split_id: SplitId },
1473
1474 SetSplitRatio {
1476 split_id: SplitId,
1477 ratio: f32,
1479 },
1480
1481 SetSplitLabel { split_id: SplitId, label: String },
1483
1484 ClearSplitLabel { split_id: SplitId },
1486
1487 GetSplitByLabel { label: String, request_id: u64 },
1489
1490 DistributeSplitsEvenly {
1492 split_ids: Vec<SplitId>,
1494 },
1495
1496 SetBufferCursor {
1498 buffer_id: BufferId,
1499 position: usize,
1501 },
1502
1503 SetBufferShowCursors { buffer_id: BufferId, show: bool },
1511
1512 SendLspRequest {
1514 language: String,
1515 method: String,
1516 #[ts(type = "any")]
1517 params: Option<JsonValue>,
1518 request_id: u64,
1519 },
1520
1521 SetClipboard { text: String },
1523
1524 DeleteSelection,
1527
1528 SetContext {
1532 name: String,
1534 active: bool,
1536 },
1537
1538 SetReviewDiffHunks { hunks: Vec<ReviewHunk> },
1540
1541 ExecuteAction {
1544 action_name: String,
1546 },
1547
1548 ExecuteActions {
1552 actions: Vec<ActionSpec>,
1554 },
1555
1556 GetBufferText {
1558 buffer_id: BufferId,
1560 start: usize,
1562 end: usize,
1564 request_id: u64,
1566 },
1567
1568 GetLineStartPosition {
1571 buffer_id: BufferId,
1573 line: u32,
1575 request_id: u64,
1577 },
1578
1579 GetLineEndPosition {
1583 buffer_id: BufferId,
1585 line: u32,
1587 request_id: u64,
1589 },
1590
1591 GetBufferLineCount {
1593 buffer_id: BufferId,
1595 request_id: u64,
1597 },
1598
1599 ScrollToLineCenter {
1602 split_id: SplitId,
1604 buffer_id: BufferId,
1606 line: usize,
1608 },
1609
1610 ScrollBufferToLine {
1616 buffer_id: BufferId,
1618 line: usize,
1620 },
1621
1622 SetEditorMode {
1625 mode: Option<String>,
1627 },
1628
1629 ShowActionPopup {
1632 popup_id: String,
1634 title: String,
1636 message: String,
1638 actions: Vec<ActionPopupAction>,
1640 },
1641
1642 DisableLspForLanguage {
1644 language: String,
1646 },
1647
1648 RestartLspForLanguage {
1650 language: String,
1652 },
1653
1654 SetLspRootUri {
1658 language: String,
1660 uri: String,
1662 },
1663
1664 CreateScrollSyncGroup {
1668 group_id: u32,
1670 left_split: SplitId,
1672 right_split: SplitId,
1674 },
1675
1676 SetScrollSyncAnchors {
1679 group_id: u32,
1681 anchors: Vec<(usize, usize)>,
1683 },
1684
1685 RemoveScrollSyncGroup {
1687 group_id: u32,
1689 },
1690
1691 SaveBufferToPath {
1694 buffer_id: BufferId,
1696 path: PathBuf,
1698 },
1699
1700 LoadPlugin {
1703 path: PathBuf,
1705 callback_id: JsCallbackId,
1707 },
1708
1709 UnloadPlugin {
1712 name: String,
1714 callback_id: JsCallbackId,
1716 },
1717
1718 ReloadPlugin {
1721 name: String,
1723 callback_id: JsCallbackId,
1725 },
1726
1727 ListPlugins {
1730 callback_id: JsCallbackId,
1732 },
1733
1734 ReloadThemes { apply_theme: Option<String> },
1738
1739 RegisterGrammar {
1742 language: String,
1744 grammar_path: String,
1746 extensions: Vec<String>,
1748 },
1749
1750 RegisterLanguageConfig {
1753 language: String,
1755 config: LanguagePackConfig,
1757 },
1758
1759 RegisterLspServer {
1762 language: String,
1764 config: LspServerPackConfig,
1766 },
1767
1768 ReloadGrammars { callback_id: JsCallbackId },
1772
1773 CreateTerminal {
1777 cwd: Option<String>,
1779 direction: Option<String>,
1781 ratio: Option<f32>,
1783 focus: Option<bool>,
1785 request_id: u64,
1787 },
1788
1789 SendTerminalInput {
1791 terminal_id: TerminalId,
1793 data: String,
1795 },
1796
1797 CloseTerminal {
1799 terminal_id: TerminalId,
1801 },
1802
1803 GrepProject {
1807 pattern: String,
1809 fixed_string: bool,
1811 case_sensitive: bool,
1813 max_results: usize,
1815 whole_words: bool,
1817 callback_id: JsCallbackId,
1819 },
1820
1821 GrepProjectStreaming {
1826 pattern: String,
1828 fixed_string: bool,
1830 case_sensitive: bool,
1832 max_results: usize,
1834 whole_words: bool,
1836 search_id: u64,
1838 callback_id: JsCallbackId,
1840 },
1841
1842 ReplaceInBuffer {
1846 file_path: PathBuf,
1848 matches: Vec<(usize, usize)>,
1850 replacement: String,
1852 callback_id: JsCallbackId,
1854 },
1855}
1856
1857impl PluginCommand {
1858 pub fn debug_variant_name(&self) -> String {
1860 let dbg = format!("{:?}", self);
1861 dbg.split([' ', '{', '(']).next().unwrap_or("?").to_string()
1862 }
1863}
1864
1865#[derive(Debug, Clone, Serialize, Deserialize, Default, TS)]
1874#[serde(rename_all = "camelCase")]
1875#[ts(export)]
1876pub struct LanguagePackConfig {
1877 #[serde(default)]
1879 pub comment_prefix: Option<String>,
1880
1881 #[serde(default)]
1883 pub block_comment_start: Option<String>,
1884
1885 #[serde(default)]
1887 pub block_comment_end: Option<String>,
1888
1889 #[serde(default)]
1891 pub use_tabs: Option<bool>,
1892
1893 #[serde(default)]
1895 pub tab_size: Option<usize>,
1896
1897 #[serde(default)]
1899 pub auto_indent: Option<bool>,
1900
1901 #[serde(default)]
1904 pub show_whitespace_tabs: Option<bool>,
1905
1906 #[serde(default)]
1908 pub formatter: Option<FormatterPackConfig>,
1909}
1910
1911#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1913#[serde(rename_all = "camelCase")]
1914#[ts(export)]
1915pub struct FormatterPackConfig {
1916 pub command: String,
1918
1919 #[serde(default)]
1921 pub args: Vec<String>,
1922}
1923
1924#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1926#[serde(rename_all = "camelCase")]
1927#[ts(export)]
1928pub struct ProcessLimitsPackConfig {
1929 #[serde(default)]
1931 pub max_memory_percent: Option<u32>,
1932
1933 #[serde(default)]
1935 pub max_cpu_percent: Option<u32>,
1936
1937 #[serde(default)]
1939 pub enabled: Option<bool>,
1940}
1941
1942#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1944#[serde(rename_all = "camelCase")]
1945#[ts(export)]
1946pub struct LspServerPackConfig {
1947 pub command: String,
1949
1950 #[serde(default)]
1952 pub args: Vec<String>,
1953
1954 #[serde(default)]
1956 pub auto_start: Option<bool>,
1957
1958 #[serde(default)]
1960 #[ts(type = "Record<string, unknown> | null")]
1961 pub initialization_options: Option<JsonValue>,
1962
1963 #[serde(default)]
1965 pub process_limits: Option<ProcessLimitsPackConfig>,
1966}
1967
1968#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, TS)]
1970#[ts(export)]
1971pub enum HunkStatus {
1972 Pending,
1973 Staged,
1974 Discarded,
1975}
1976
1977#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1979#[ts(export)]
1980pub struct ReviewHunk {
1981 pub id: String,
1982 pub file: String,
1983 pub context_header: String,
1984 pub status: HunkStatus,
1985 pub base_range: Option<(usize, usize)>,
1987 pub modified_range: Option<(usize, usize)>,
1989}
1990
1991#[derive(Debug, Clone, Serialize, Deserialize, TS)]
1993#[serde(deny_unknown_fields)]
1994#[ts(export, rename = "TsActionPopupAction")]
1995pub struct ActionPopupAction {
1996 pub id: String,
1998 pub label: String,
2000}
2001
2002#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2004#[serde(deny_unknown_fields)]
2005#[ts(export)]
2006pub struct ActionPopupOptions {
2007 pub id: String,
2009 pub title: String,
2011 pub message: String,
2013 pub actions: Vec<ActionPopupAction>,
2015}
2016
2017#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2019#[ts(export)]
2020pub struct TsHighlightSpan {
2021 pub start: u32,
2022 pub end: u32,
2023 #[ts(type = "[number, number, number]")]
2024 pub color: (u8, u8, u8),
2025 pub bold: bool,
2026 pub italic: bool,
2027}
2028
2029#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2031#[ts(export)]
2032pub struct SpawnResult {
2033 pub stdout: String,
2035 pub stderr: String,
2037 pub exit_code: i32,
2039}
2040
2041#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2043#[ts(export)]
2044pub struct BackgroundProcessResult {
2045 #[ts(type = "number")]
2047 pub process_id: u64,
2048 pub exit_code: i32,
2051}
2052
2053#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2055#[serde(rename_all = "camelCase")]
2056#[ts(export, rename_all = "camelCase")]
2057pub struct GrepMatch {
2058 pub file: String,
2060 #[ts(type = "number")]
2062 pub buffer_id: usize,
2063 #[ts(type = "number")]
2065 pub byte_offset: usize,
2066 #[ts(type = "number")]
2068 pub length: usize,
2069 #[ts(type = "number")]
2071 pub line: usize,
2072 #[ts(type = "number")]
2074 pub column: usize,
2075 pub context: String,
2077}
2078
2079#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2081#[serde(rename_all = "camelCase")]
2082#[ts(export, rename_all = "camelCase")]
2083pub struct ReplaceResult {
2084 #[ts(type = "number")]
2086 pub replacements: usize,
2087 #[ts(type = "number")]
2089 pub buffer_id: usize,
2090}
2091
2092#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2094#[serde(deny_unknown_fields, rename_all = "camelCase")]
2095#[ts(export, rename = "TextPropertyEntry", rename_all = "camelCase")]
2096pub struct JsTextPropertyEntry {
2097 pub text: String,
2099 #[serde(default)]
2101 #[ts(optional, type = "Record<string, unknown>")]
2102 pub properties: Option<HashMap<String, JsonValue>>,
2103 #[serde(default)]
2105 #[ts(optional, type = "Partial<OverlayOptions>")]
2106 pub style: Option<OverlayOptions>,
2107 #[serde(default)]
2109 #[ts(optional)]
2110 pub inline_overlays: Option<Vec<crate::text_property::InlineOverlay>>,
2111}
2112
2113#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2115#[ts(export)]
2116pub struct DirEntry {
2117 pub name: String,
2119 pub is_file: bool,
2121 pub is_dir: bool,
2123}
2124
2125#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2127#[ts(export)]
2128pub struct JsPosition {
2129 pub line: u32,
2131 pub character: u32,
2133}
2134
2135#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2137#[ts(export)]
2138pub struct JsRange {
2139 pub start: JsPosition,
2141 pub end: JsPosition,
2143}
2144
2145#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2147#[ts(export)]
2148pub struct JsDiagnostic {
2149 pub uri: String,
2151 pub message: String,
2153 pub severity: Option<u8>,
2155 pub range: JsRange,
2157 #[ts(optional)]
2159 pub source: Option<String>,
2160}
2161
2162#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2164#[serde(deny_unknown_fields)]
2165#[ts(export)]
2166pub struct CreateVirtualBufferOptions {
2167 pub name: String,
2169 #[serde(default)]
2171 #[ts(optional)]
2172 pub mode: Option<String>,
2173 #[serde(default, rename = "readOnly")]
2175 #[ts(optional, rename = "readOnly")]
2176 pub read_only: Option<bool>,
2177 #[serde(default, rename = "showLineNumbers")]
2179 #[ts(optional, rename = "showLineNumbers")]
2180 pub show_line_numbers: Option<bool>,
2181 #[serde(default, rename = "showCursors")]
2183 #[ts(optional, rename = "showCursors")]
2184 pub show_cursors: Option<bool>,
2185 #[serde(default, rename = "editingDisabled")]
2187 #[ts(optional, rename = "editingDisabled")]
2188 pub editing_disabled: Option<bool>,
2189 #[serde(default, rename = "hiddenFromTabs")]
2191 #[ts(optional, rename = "hiddenFromTabs")]
2192 pub hidden_from_tabs: Option<bool>,
2193 #[serde(default)]
2195 #[ts(optional)]
2196 pub entries: Option<Vec<JsTextPropertyEntry>>,
2197}
2198
2199#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2201#[serde(deny_unknown_fields)]
2202#[ts(export)]
2203pub struct CreateVirtualBufferInSplitOptions {
2204 pub name: String,
2206 #[serde(default)]
2208 #[ts(optional)]
2209 pub mode: Option<String>,
2210 #[serde(default, rename = "readOnly")]
2212 #[ts(optional, rename = "readOnly")]
2213 pub read_only: Option<bool>,
2214 #[serde(default)]
2216 #[ts(optional)]
2217 pub ratio: Option<f32>,
2218 #[serde(default)]
2220 #[ts(optional)]
2221 pub direction: Option<String>,
2222 #[serde(default, rename = "panelId")]
2224 #[ts(optional, rename = "panelId")]
2225 pub panel_id: Option<String>,
2226 #[serde(default, rename = "showLineNumbers")]
2228 #[ts(optional, rename = "showLineNumbers")]
2229 pub show_line_numbers: Option<bool>,
2230 #[serde(default, rename = "showCursors")]
2232 #[ts(optional, rename = "showCursors")]
2233 pub show_cursors: Option<bool>,
2234 #[serde(default, rename = "editingDisabled")]
2236 #[ts(optional, rename = "editingDisabled")]
2237 pub editing_disabled: Option<bool>,
2238 #[serde(default, rename = "lineWrap")]
2240 #[ts(optional, rename = "lineWrap")]
2241 pub line_wrap: Option<bool>,
2242 #[serde(default)]
2244 #[ts(optional)]
2245 pub before: Option<bool>,
2246 #[serde(default)]
2248 #[ts(optional)]
2249 pub entries: Option<Vec<JsTextPropertyEntry>>,
2250}
2251
2252#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2254#[serde(deny_unknown_fields)]
2255#[ts(export)]
2256pub struct CreateVirtualBufferInExistingSplitOptions {
2257 pub name: String,
2259 #[serde(rename = "splitId")]
2261 #[ts(rename = "splitId")]
2262 pub split_id: usize,
2263 #[serde(default)]
2265 #[ts(optional)]
2266 pub mode: Option<String>,
2267 #[serde(default, rename = "readOnly")]
2269 #[ts(optional, rename = "readOnly")]
2270 pub read_only: Option<bool>,
2271 #[serde(default, rename = "showLineNumbers")]
2273 #[ts(optional, rename = "showLineNumbers")]
2274 pub show_line_numbers: Option<bool>,
2275 #[serde(default, rename = "showCursors")]
2277 #[ts(optional, rename = "showCursors")]
2278 pub show_cursors: Option<bool>,
2279 #[serde(default, rename = "editingDisabled")]
2281 #[ts(optional, rename = "editingDisabled")]
2282 pub editing_disabled: Option<bool>,
2283 #[serde(default, rename = "lineWrap")]
2285 #[ts(optional, rename = "lineWrap")]
2286 pub line_wrap: Option<bool>,
2287 #[serde(default)]
2289 #[ts(optional)]
2290 pub entries: Option<Vec<JsTextPropertyEntry>>,
2291}
2292
2293#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2295#[serde(deny_unknown_fields)]
2296#[ts(export)]
2297pub struct CreateTerminalOptions {
2298 #[serde(default)]
2300 #[ts(optional)]
2301 pub cwd: Option<String>,
2302 #[serde(default)]
2304 #[ts(optional)]
2305 pub direction: Option<String>,
2306 #[serde(default)]
2308 #[ts(optional)]
2309 pub ratio: Option<f32>,
2310 #[serde(default)]
2312 #[ts(optional)]
2313 pub focus: Option<bool>,
2314}
2315
2316#[derive(Debug, Clone, Serialize, TS)]
2321#[ts(export, type = "Array<Record<string, unknown>>")]
2322pub struct TextPropertiesAtCursor(pub Vec<HashMap<String, JsonValue>>);
2323
2324#[cfg(feature = "plugins")]
2326mod fromjs_impls {
2327 use super::*;
2328 use rquickjs::{Ctx, FromJs, Value};
2329
2330 impl<'js> FromJs<'js> for JsTextPropertyEntry {
2331 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2332 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2333 from: "object",
2334 to: "JsTextPropertyEntry",
2335 message: Some(e.to_string()),
2336 })
2337 }
2338 }
2339
2340 impl<'js> FromJs<'js> for CreateVirtualBufferOptions {
2341 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2342 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2343 from: "object",
2344 to: "CreateVirtualBufferOptions",
2345 message: Some(e.to_string()),
2346 })
2347 }
2348 }
2349
2350 impl<'js> FromJs<'js> for CreateVirtualBufferInSplitOptions {
2351 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2352 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2353 from: "object",
2354 to: "CreateVirtualBufferInSplitOptions",
2355 message: Some(e.to_string()),
2356 })
2357 }
2358 }
2359
2360 impl<'js> FromJs<'js> for CreateVirtualBufferInExistingSplitOptions {
2361 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2362 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2363 from: "object",
2364 to: "CreateVirtualBufferInExistingSplitOptions",
2365 message: Some(e.to_string()),
2366 })
2367 }
2368 }
2369
2370 impl<'js> rquickjs::IntoJs<'js> for TextPropertiesAtCursor {
2371 fn into_js(self, ctx: &Ctx<'js>) -> rquickjs::Result<Value<'js>> {
2372 rquickjs_serde::to_value(ctx.clone(), &self.0)
2373 .map_err(|e| rquickjs::Error::new_from_js_message("serialize", "", &e.to_string()))
2374 }
2375 }
2376
2377 impl<'js> FromJs<'js> for ActionSpec {
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: "ActionSpec",
2384 message: Some(e.to_string()),
2385 })
2386 }
2387 }
2388
2389 impl<'js> FromJs<'js> for ActionPopupAction {
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: "ActionPopupAction",
2394 message: Some(e.to_string()),
2395 })
2396 }
2397 }
2398
2399 impl<'js> FromJs<'js> for ActionPopupOptions {
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: "ActionPopupOptions",
2404 message: Some(e.to_string()),
2405 })
2406 }
2407 }
2408
2409 impl<'js> FromJs<'js> for ViewTokenWire {
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: "ViewTokenWire",
2414 message: Some(e.to_string()),
2415 })
2416 }
2417 }
2418
2419 impl<'js> FromJs<'js> for ViewTokenStyle {
2420 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2421 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2422 from: "object",
2423 to: "ViewTokenStyle",
2424 message: Some(e.to_string()),
2425 })
2426 }
2427 }
2428
2429 impl<'js> FromJs<'js> for LayoutHints {
2430 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2431 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2432 from: "object",
2433 to: "LayoutHints",
2434 message: Some(e.to_string()),
2435 })
2436 }
2437 }
2438
2439 impl<'js> FromJs<'js> for CreateCompositeBufferOptions {
2440 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2441 let json: serde_json::Value =
2443 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2444 from: "object",
2445 to: "CreateCompositeBufferOptions (json)",
2446 message: Some(e.to_string()),
2447 })?;
2448 serde_json::from_value(json).map_err(|e| rquickjs::Error::FromJs {
2449 from: "json",
2450 to: "CreateCompositeBufferOptions",
2451 message: Some(e.to_string()),
2452 })
2453 }
2454 }
2455
2456 impl<'js> FromJs<'js> for CompositeHunk {
2457 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2458 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2459 from: "object",
2460 to: "CompositeHunk",
2461 message: Some(e.to_string()),
2462 })
2463 }
2464 }
2465
2466 impl<'js> FromJs<'js> for LanguagePackConfig {
2467 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2468 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2469 from: "object",
2470 to: "LanguagePackConfig",
2471 message: Some(e.to_string()),
2472 })
2473 }
2474 }
2475
2476 impl<'js> FromJs<'js> for LspServerPackConfig {
2477 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2478 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2479 from: "object",
2480 to: "LspServerPackConfig",
2481 message: Some(e.to_string()),
2482 })
2483 }
2484 }
2485
2486 impl<'js> FromJs<'js> for ProcessLimitsPackConfig {
2487 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2488 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2489 from: "object",
2490 to: "ProcessLimitsPackConfig",
2491 message: Some(e.to_string()),
2492 })
2493 }
2494 }
2495
2496 impl<'js> FromJs<'js> for CreateTerminalOptions {
2497 fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> rquickjs::Result<Self> {
2498 rquickjs_serde::from_value(value).map_err(|e| rquickjs::Error::FromJs {
2499 from: "object",
2500 to: "CreateTerminalOptions",
2501 message: Some(e.to_string()),
2502 })
2503 }
2504 }
2505}
2506
2507pub struct PluginApi {
2509 hooks: Arc<RwLock<HookRegistry>>,
2511
2512 commands: Arc<RwLock<CommandRegistry>>,
2514
2515 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2517
2518 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2520}
2521
2522impl PluginApi {
2523 pub fn new(
2525 hooks: Arc<RwLock<HookRegistry>>,
2526 commands: Arc<RwLock<CommandRegistry>>,
2527 command_sender: std::sync::mpsc::Sender<PluginCommand>,
2528 state_snapshot: Arc<RwLock<EditorStateSnapshot>>,
2529 ) -> Self {
2530 Self {
2531 hooks,
2532 commands,
2533 command_sender,
2534 state_snapshot,
2535 }
2536 }
2537
2538 pub fn register_hook(&self, hook_name: &str, callback: HookCallback) {
2540 let mut hooks = self.hooks.write().unwrap();
2541 hooks.add_hook(hook_name, callback);
2542 }
2543
2544 pub fn unregister_hooks(&self, hook_name: &str) {
2546 let mut hooks = self.hooks.write().unwrap();
2547 hooks.remove_hooks(hook_name);
2548 }
2549
2550 pub fn register_command(&self, command: Command) {
2552 let commands = self.commands.read().unwrap();
2553 commands.register(command);
2554 }
2555
2556 pub fn unregister_command(&self, name: &str) {
2558 let commands = self.commands.read().unwrap();
2559 commands.unregister(name);
2560 }
2561
2562 pub fn send_command(&self, command: PluginCommand) -> Result<(), String> {
2564 self.command_sender
2565 .send(command)
2566 .map_err(|e| format!("Failed to send command: {}", e))
2567 }
2568
2569 pub fn insert_text(
2571 &self,
2572 buffer_id: BufferId,
2573 position: usize,
2574 text: String,
2575 ) -> Result<(), String> {
2576 self.send_command(PluginCommand::InsertText {
2577 buffer_id,
2578 position,
2579 text,
2580 })
2581 }
2582
2583 pub fn delete_range(&self, buffer_id: BufferId, range: Range<usize>) -> Result<(), String> {
2585 self.send_command(PluginCommand::DeleteRange { buffer_id, range })
2586 }
2587
2588 pub fn add_overlay(
2596 &self,
2597 buffer_id: BufferId,
2598 namespace: Option<String>,
2599 range: Range<usize>,
2600 options: OverlayOptions,
2601 ) -> Result<(), String> {
2602 self.send_command(PluginCommand::AddOverlay {
2603 buffer_id,
2604 namespace: namespace.map(OverlayNamespace::from_string),
2605 range,
2606 options,
2607 })
2608 }
2609
2610 pub fn remove_overlay(&self, buffer_id: BufferId, handle: String) -> Result<(), String> {
2612 self.send_command(PluginCommand::RemoveOverlay {
2613 buffer_id,
2614 handle: OverlayHandle::from_string(handle),
2615 })
2616 }
2617
2618 pub fn clear_namespace(&self, buffer_id: BufferId, namespace: String) -> Result<(), String> {
2620 self.send_command(PluginCommand::ClearNamespace {
2621 buffer_id,
2622 namespace: OverlayNamespace::from_string(namespace),
2623 })
2624 }
2625
2626 pub fn clear_overlays_in_range(
2629 &self,
2630 buffer_id: BufferId,
2631 start: usize,
2632 end: usize,
2633 ) -> Result<(), String> {
2634 self.send_command(PluginCommand::ClearOverlaysInRange {
2635 buffer_id,
2636 start,
2637 end,
2638 })
2639 }
2640
2641 pub fn set_status(&self, message: String) -> Result<(), String> {
2643 self.send_command(PluginCommand::SetStatus { message })
2644 }
2645
2646 pub fn open_file_at_location(
2649 &self,
2650 path: PathBuf,
2651 line: Option<usize>,
2652 column: Option<usize>,
2653 ) -> Result<(), String> {
2654 self.send_command(PluginCommand::OpenFileAtLocation { path, line, column })
2655 }
2656
2657 pub fn open_file_in_split(
2662 &self,
2663 split_id: usize,
2664 path: PathBuf,
2665 line: Option<usize>,
2666 column: Option<usize>,
2667 ) -> Result<(), String> {
2668 self.send_command(PluginCommand::OpenFileInSplit {
2669 split_id,
2670 path,
2671 line,
2672 column,
2673 })
2674 }
2675
2676 pub fn start_prompt(&self, label: String, prompt_type: String) -> Result<(), String> {
2679 self.send_command(PluginCommand::StartPrompt { label, prompt_type })
2680 }
2681
2682 pub fn set_prompt_suggestions(&self, suggestions: Vec<Suggestion>) -> Result<(), String> {
2685 self.send_command(PluginCommand::SetPromptSuggestions { suggestions })
2686 }
2687
2688 pub fn set_prompt_input_sync(&self, sync: bool) -> Result<(), String> {
2690 self.send_command(PluginCommand::SetPromptInputSync { sync })
2691 }
2692
2693 pub fn add_menu_item(
2695 &self,
2696 menu_label: String,
2697 item: MenuItem,
2698 position: MenuPosition,
2699 ) -> Result<(), String> {
2700 self.send_command(PluginCommand::AddMenuItem {
2701 menu_label,
2702 item,
2703 position,
2704 })
2705 }
2706
2707 pub fn add_menu(&self, menu: Menu, position: MenuPosition) -> Result<(), String> {
2709 self.send_command(PluginCommand::AddMenu { menu, position })
2710 }
2711
2712 pub fn remove_menu_item(&self, menu_label: String, item_label: String) -> Result<(), String> {
2714 self.send_command(PluginCommand::RemoveMenuItem {
2715 menu_label,
2716 item_label,
2717 })
2718 }
2719
2720 pub fn remove_menu(&self, menu_label: String) -> Result<(), String> {
2722 self.send_command(PluginCommand::RemoveMenu { menu_label })
2723 }
2724
2725 pub fn create_virtual_buffer(
2732 &self,
2733 name: String,
2734 mode: String,
2735 read_only: bool,
2736 ) -> Result<(), String> {
2737 self.send_command(PluginCommand::CreateVirtualBuffer {
2738 name,
2739 mode,
2740 read_only,
2741 })
2742 }
2743
2744 pub fn create_virtual_buffer_with_content(
2750 &self,
2751 name: String,
2752 mode: String,
2753 read_only: bool,
2754 entries: Vec<TextPropertyEntry>,
2755 ) -> Result<(), String> {
2756 self.send_command(PluginCommand::CreateVirtualBufferWithContent {
2757 name,
2758 mode,
2759 read_only,
2760 entries,
2761 show_line_numbers: true,
2762 show_cursors: true,
2763 editing_disabled: false,
2764 hidden_from_tabs: false,
2765 request_id: None,
2766 })
2767 }
2768
2769 pub fn set_virtual_buffer_content(
2773 &self,
2774 buffer_id: BufferId,
2775 entries: Vec<TextPropertyEntry>,
2776 ) -> Result<(), String> {
2777 self.send_command(PluginCommand::SetVirtualBufferContent { buffer_id, entries })
2778 }
2779
2780 pub fn get_text_properties_at_cursor(&self, buffer_id: BufferId) -> Result<(), String> {
2784 self.send_command(PluginCommand::GetTextPropertiesAtCursor { buffer_id })
2785 }
2786
2787 pub fn define_mode(
2791 &self,
2792 name: String,
2793 bindings: Vec<(String, String)>,
2794 read_only: bool,
2795 allow_text_input: bool,
2796 ) -> Result<(), String> {
2797 self.send_command(PluginCommand::DefineMode {
2798 name,
2799 bindings,
2800 read_only,
2801 allow_text_input,
2802 plugin_name: None,
2803 })
2804 }
2805
2806 pub fn show_buffer(&self, buffer_id: BufferId) -> Result<(), String> {
2808 self.send_command(PluginCommand::ShowBuffer { buffer_id })
2809 }
2810
2811 pub fn set_split_scroll(&self, split_id: usize, top_byte: usize) -> Result<(), String> {
2813 self.send_command(PluginCommand::SetSplitScroll {
2814 split_id: SplitId(split_id),
2815 top_byte,
2816 })
2817 }
2818
2819 pub fn get_highlights(
2821 &self,
2822 buffer_id: BufferId,
2823 range: Range<usize>,
2824 request_id: u64,
2825 ) -> Result<(), String> {
2826 self.send_command(PluginCommand::RequestHighlights {
2827 buffer_id,
2828 range,
2829 request_id,
2830 })
2831 }
2832
2833 pub fn get_active_buffer_id(&self) -> BufferId {
2837 let snapshot = self.state_snapshot.read().unwrap();
2838 snapshot.active_buffer_id
2839 }
2840
2841 pub fn get_active_split_id(&self) -> usize {
2843 let snapshot = self.state_snapshot.read().unwrap();
2844 snapshot.active_split_id
2845 }
2846
2847 pub fn get_buffer_info(&self, buffer_id: BufferId) -> Option<BufferInfo> {
2849 let snapshot = self.state_snapshot.read().unwrap();
2850 snapshot.buffers.get(&buffer_id).cloned()
2851 }
2852
2853 pub fn list_buffers(&self) -> Vec<BufferInfo> {
2855 let snapshot = self.state_snapshot.read().unwrap();
2856 snapshot.buffers.values().cloned().collect()
2857 }
2858
2859 pub fn get_primary_cursor(&self) -> Option<CursorInfo> {
2861 let snapshot = self.state_snapshot.read().unwrap();
2862 snapshot.primary_cursor.clone()
2863 }
2864
2865 pub fn get_all_cursors(&self) -> Vec<CursorInfo> {
2867 let snapshot = self.state_snapshot.read().unwrap();
2868 snapshot.all_cursors.clone()
2869 }
2870
2871 pub fn get_viewport(&self) -> Option<ViewportInfo> {
2873 let snapshot = self.state_snapshot.read().unwrap();
2874 snapshot.viewport.clone()
2875 }
2876
2877 pub fn state_snapshot_handle(&self) -> Arc<RwLock<EditorStateSnapshot>> {
2879 Arc::clone(&self.state_snapshot)
2880 }
2881}
2882
2883impl Clone for PluginApi {
2884 fn clone(&self) -> Self {
2885 Self {
2886 hooks: Arc::clone(&self.hooks),
2887 commands: Arc::clone(&self.commands),
2888 command_sender: self.command_sender.clone(),
2889 state_snapshot: Arc::clone(&self.state_snapshot),
2890 }
2891 }
2892}
2893
2894#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2908#[serde(rename_all = "camelCase", deny_unknown_fields)]
2909#[ts(export, rename_all = "camelCase")]
2910pub struct TsCompletionCandidate {
2911 pub label: String,
2913
2914 #[serde(skip_serializing_if = "Option::is_none")]
2916 pub insert_text: Option<String>,
2917
2918 #[serde(skip_serializing_if = "Option::is_none")]
2920 pub detail: Option<String>,
2921
2922 #[serde(skip_serializing_if = "Option::is_none")]
2924 pub icon: Option<String>,
2925
2926 #[serde(default)]
2928 pub score: i64,
2929
2930 #[serde(default)]
2932 pub is_snippet: bool,
2933
2934 #[serde(skip_serializing_if = "Option::is_none")]
2936 pub provider_data: Option<String>,
2937}
2938
2939#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2944#[serde(rename_all = "camelCase")]
2945#[ts(export, rename_all = "camelCase")]
2946pub struct TsCompletionContext {
2947 pub prefix: String,
2949
2950 pub cursor_byte: usize,
2952
2953 pub word_start_byte: usize,
2955
2956 pub buffer_len: usize,
2958
2959 pub is_large_file: bool,
2961
2962 pub text_around_cursor: String,
2965
2966 pub cursor_offset_in_text: usize,
2968
2969 #[serde(skip_serializing_if = "Option::is_none")]
2971 pub language_id: Option<String>,
2972}
2973
2974#[derive(Debug, Clone, Serialize, Deserialize, TS)]
2976#[serde(rename_all = "camelCase", deny_unknown_fields)]
2977#[ts(export, rename_all = "camelCase")]
2978pub struct TsCompletionProviderRegistration {
2979 pub id: String,
2981
2982 pub display_name: String,
2984
2985 #[serde(default = "default_plugin_provider_priority")]
2988 pub priority: u32,
2989
2990 #[serde(default)]
2993 pub language_ids: Vec<String>,
2994}
2995
2996fn default_plugin_provider_priority() -> u32 {
2997 50
2998}
2999
3000#[cfg(test)]
3001mod tests {
3002 use super::*;
3003
3004 #[test]
3005 fn test_plugin_api_creation() {
3006 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3007 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3008 let (tx, _rx) = std::sync::mpsc::channel();
3009 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3010
3011 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3012
3013 let _clone = api.clone();
3015 }
3016
3017 #[test]
3018 fn test_register_hook() {
3019 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3020 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3021 let (tx, _rx) = std::sync::mpsc::channel();
3022 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3023
3024 let api = PluginApi::new(hooks.clone(), commands, tx, state_snapshot);
3025
3026 api.register_hook("test-hook", Box::new(|_| true));
3027
3028 let hook_registry = hooks.read().unwrap();
3029 assert_eq!(hook_registry.hook_count("test-hook"), 1);
3030 }
3031
3032 #[test]
3033 fn test_send_command() {
3034 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3035 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3036 let (tx, rx) = std::sync::mpsc::channel();
3037 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3038
3039 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3040
3041 let result = api.insert_text(BufferId(1), 0, "test".to_string());
3042 assert!(result.is_ok());
3043
3044 let received = rx.try_recv();
3046 assert!(received.is_ok());
3047
3048 match received.unwrap() {
3049 PluginCommand::InsertText {
3050 buffer_id,
3051 position,
3052 text,
3053 } => {
3054 assert_eq!(buffer_id.0, 1);
3055 assert_eq!(position, 0);
3056 assert_eq!(text, "test");
3057 }
3058 _ => panic!("Wrong command type"),
3059 }
3060 }
3061
3062 #[test]
3063 fn test_add_overlay_command() {
3064 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3065 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3066 let (tx, rx) = std::sync::mpsc::channel();
3067 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3068
3069 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3070
3071 let result = api.add_overlay(
3072 BufferId(1),
3073 Some("test-overlay".to_string()),
3074 0..10,
3075 OverlayOptions {
3076 fg: Some(OverlayColorSpec::ThemeKey("ui.status_bar_fg".to_string())),
3077 bg: None,
3078 underline: true,
3079 bold: false,
3080 italic: false,
3081 strikethrough: false,
3082 extend_to_line_end: false,
3083 url: None,
3084 },
3085 );
3086 assert!(result.is_ok());
3087
3088 let received = rx.try_recv().unwrap();
3089 match received {
3090 PluginCommand::AddOverlay {
3091 buffer_id,
3092 namespace,
3093 range,
3094 options,
3095 } => {
3096 assert_eq!(buffer_id.0, 1);
3097 assert_eq!(namespace.as_ref().map(|n| n.as_str()), Some("test-overlay"));
3098 assert_eq!(range, 0..10);
3099 assert!(matches!(
3100 options.fg,
3101 Some(OverlayColorSpec::ThemeKey(ref k)) if k == "ui.status_bar_fg"
3102 ));
3103 assert!(options.bg.is_none());
3104 assert!(options.underline);
3105 assert!(!options.bold);
3106 assert!(!options.italic);
3107 assert!(!options.extend_to_line_end);
3108 }
3109 _ => panic!("Wrong command type"),
3110 }
3111 }
3112
3113 #[test]
3114 fn test_set_status_command() {
3115 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3116 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3117 let (tx, rx) = std::sync::mpsc::channel();
3118 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3119
3120 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3121
3122 let result = api.set_status("Test status".to_string());
3123 assert!(result.is_ok());
3124
3125 let received = rx.try_recv().unwrap();
3126 match received {
3127 PluginCommand::SetStatus { message } => {
3128 assert_eq!(message, "Test status");
3129 }
3130 _ => panic!("Wrong command type"),
3131 }
3132 }
3133
3134 #[test]
3135 fn test_get_active_buffer_id() {
3136 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3137 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3138 let (tx, _rx) = std::sync::mpsc::channel();
3139 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3140
3141 {
3143 let mut snapshot = state_snapshot.write().unwrap();
3144 snapshot.active_buffer_id = BufferId(5);
3145 }
3146
3147 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3148
3149 let active_id = api.get_active_buffer_id();
3150 assert_eq!(active_id.0, 5);
3151 }
3152
3153 #[test]
3154 fn test_get_buffer_info() {
3155 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3156 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3157 let (tx, _rx) = std::sync::mpsc::channel();
3158 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3159
3160 {
3162 let mut snapshot = state_snapshot.write().unwrap();
3163 let buffer_info = BufferInfo {
3164 id: BufferId(1),
3165 path: Some(std::path::PathBuf::from("/test/file.txt")),
3166 modified: true,
3167 length: 100,
3168 is_virtual: false,
3169 view_mode: "source".to_string(),
3170 is_composing_in_any_split: false,
3171 compose_width: None,
3172 language: "text".to_string(),
3173 };
3174 snapshot.buffers.insert(BufferId(1), buffer_info);
3175 }
3176
3177 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3178
3179 let info = api.get_buffer_info(BufferId(1));
3180 assert!(info.is_some());
3181 let info = info.unwrap();
3182 assert_eq!(info.id.0, 1);
3183 assert_eq!(
3184 info.path.as_ref().unwrap().to_str().unwrap(),
3185 "/test/file.txt"
3186 );
3187 assert!(info.modified);
3188 assert_eq!(info.length, 100);
3189
3190 let no_info = api.get_buffer_info(BufferId(999));
3192 assert!(no_info.is_none());
3193 }
3194
3195 #[test]
3196 fn test_list_buffers() {
3197 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3198 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3199 let (tx, _rx) = std::sync::mpsc::channel();
3200 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3201
3202 {
3204 let mut snapshot = state_snapshot.write().unwrap();
3205 snapshot.buffers.insert(
3206 BufferId(1),
3207 BufferInfo {
3208 id: BufferId(1),
3209 path: Some(std::path::PathBuf::from("/file1.txt")),
3210 modified: false,
3211 length: 50,
3212 is_virtual: false,
3213 view_mode: "source".to_string(),
3214 is_composing_in_any_split: false,
3215 compose_width: None,
3216 language: "text".to_string(),
3217 },
3218 );
3219 snapshot.buffers.insert(
3220 BufferId(2),
3221 BufferInfo {
3222 id: BufferId(2),
3223 path: Some(std::path::PathBuf::from("/file2.txt")),
3224 modified: true,
3225 length: 100,
3226 is_virtual: false,
3227 view_mode: "source".to_string(),
3228 is_composing_in_any_split: false,
3229 compose_width: None,
3230 language: "text".to_string(),
3231 },
3232 );
3233 snapshot.buffers.insert(
3234 BufferId(3),
3235 BufferInfo {
3236 id: BufferId(3),
3237 path: None,
3238 modified: false,
3239 length: 0,
3240 is_virtual: true,
3241 view_mode: "source".to_string(),
3242 is_composing_in_any_split: false,
3243 compose_width: None,
3244 language: "text".to_string(),
3245 },
3246 );
3247 }
3248
3249 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3250
3251 let buffers = api.list_buffers();
3252 assert_eq!(buffers.len(), 3);
3253
3254 assert!(buffers.iter().any(|b| b.id.0 == 1));
3256 assert!(buffers.iter().any(|b| b.id.0 == 2));
3257 assert!(buffers.iter().any(|b| b.id.0 == 3));
3258 }
3259
3260 #[test]
3261 fn test_get_primary_cursor() {
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.primary_cursor = Some(CursorInfo {
3271 position: 42,
3272 selection: Some(10..42),
3273 });
3274 }
3275
3276 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3277
3278 let cursor = api.get_primary_cursor();
3279 assert!(cursor.is_some());
3280 let cursor = cursor.unwrap();
3281 assert_eq!(cursor.position, 42);
3282 assert_eq!(cursor.selection, Some(10..42));
3283 }
3284
3285 #[test]
3286 fn test_get_all_cursors() {
3287 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3288 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3289 let (tx, _rx) = std::sync::mpsc::channel();
3290 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3291
3292 {
3294 let mut snapshot = state_snapshot.write().unwrap();
3295 snapshot.all_cursors = vec![
3296 CursorInfo {
3297 position: 10,
3298 selection: None,
3299 },
3300 CursorInfo {
3301 position: 20,
3302 selection: Some(15..20),
3303 },
3304 CursorInfo {
3305 position: 30,
3306 selection: Some(25..30),
3307 },
3308 ];
3309 }
3310
3311 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3312
3313 let cursors = api.get_all_cursors();
3314 assert_eq!(cursors.len(), 3);
3315 assert_eq!(cursors[0].position, 10);
3316 assert_eq!(cursors[0].selection, None);
3317 assert_eq!(cursors[1].position, 20);
3318 assert_eq!(cursors[1].selection, Some(15..20));
3319 assert_eq!(cursors[2].position, 30);
3320 assert_eq!(cursors[2].selection, Some(25..30));
3321 }
3322
3323 #[test]
3324 fn test_get_viewport() {
3325 let hooks = Arc::new(RwLock::new(HookRegistry::new()));
3326 let commands = Arc::new(RwLock::new(CommandRegistry::new()));
3327 let (tx, _rx) = std::sync::mpsc::channel();
3328 let state_snapshot = Arc::new(RwLock::new(EditorStateSnapshot::new()));
3329
3330 {
3332 let mut snapshot = state_snapshot.write().unwrap();
3333 snapshot.viewport = Some(ViewportInfo {
3334 top_byte: 100,
3335 top_line: Some(5),
3336 left_column: 5,
3337 width: 80,
3338 height: 24,
3339 });
3340 }
3341
3342 let api = PluginApi::new(hooks, commands, tx, state_snapshot);
3343
3344 let viewport = api.get_viewport();
3345 assert!(viewport.is_some());
3346 let viewport = viewport.unwrap();
3347 assert_eq!(viewport.top_byte, 100);
3348 assert_eq!(viewport.left_column, 5);
3349 assert_eq!(viewport.width, 80);
3350 assert_eq!(viewport.height, 24);
3351 }
3352
3353 #[test]
3354 fn test_composite_buffer_options_rejects_unknown_fields() {
3355 let valid_json = r#"{
3357 "name": "test",
3358 "mode": "diff",
3359 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3360 "sources": [{"bufferId": 1, "label": "old"}]
3361 }"#;
3362 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(valid_json);
3363 assert!(
3364 result.is_ok(),
3365 "Valid JSON should parse: {:?}",
3366 result.err()
3367 );
3368
3369 let invalid_json = r#"{
3371 "name": "test",
3372 "mode": "diff",
3373 "layout": {"type": "side-by-side", "ratios": [0.5, 0.5], "showSeparator": true},
3374 "sources": [{"buffer_id": 1, "label": "old"}]
3375 }"#;
3376 let result: Result<CreateCompositeBufferOptions, _> = serde_json::from_str(invalid_json);
3377 assert!(
3378 result.is_err(),
3379 "JSON with unknown field should fail to parse"
3380 );
3381 let err = result.unwrap_err().to_string();
3382 assert!(
3383 err.contains("unknown field") || err.contains("buffer_id"),
3384 "Error should mention unknown field: {}",
3385 err
3386 );
3387 }
3388
3389 #[test]
3390 fn test_composite_hunk_rejects_unknown_fields() {
3391 let valid_json = r#"{"oldStart": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3393 let result: Result<CompositeHunk, _> = serde_json::from_str(valid_json);
3394 assert!(
3395 result.is_ok(),
3396 "Valid JSON should parse: {:?}",
3397 result.err()
3398 );
3399
3400 let invalid_json = r#"{"old_start": 0, "oldCount": 5, "newStart": 0, "newCount": 7}"#;
3402 let result: Result<CompositeHunk, _> = serde_json::from_str(invalid_json);
3403 assert!(
3404 result.is_err(),
3405 "JSON with unknown field should fail to parse"
3406 );
3407 let err = result.unwrap_err().to_string();
3408 assert!(
3409 err.contains("unknown field") || err.contains("old_start"),
3410 "Error should mention unknown field: {}",
3411 err
3412 );
3413 }
3414
3415 #[test]
3416 fn test_plugin_response_line_end_position() {
3417 let response = PluginResponse::LineEndPosition {
3418 request_id: 42,
3419 position: Some(100),
3420 };
3421 let json = serde_json::to_string(&response).unwrap();
3422 assert!(json.contains("LineEndPosition"));
3423 assert!(json.contains("42"));
3424 assert!(json.contains("100"));
3425
3426 let response_none = PluginResponse::LineEndPosition {
3428 request_id: 1,
3429 position: None,
3430 };
3431 let json_none = serde_json::to_string(&response_none).unwrap();
3432 assert!(json_none.contains("null"));
3433 }
3434
3435 #[test]
3436 fn test_plugin_response_buffer_line_count() {
3437 let response = PluginResponse::BufferLineCount {
3438 request_id: 99,
3439 count: Some(500),
3440 };
3441 let json = serde_json::to_string(&response).unwrap();
3442 assert!(json.contains("BufferLineCount"));
3443 assert!(json.contains("99"));
3444 assert!(json.contains("500"));
3445 }
3446
3447 #[test]
3448 fn test_plugin_command_get_line_end_position() {
3449 let command = PluginCommand::GetLineEndPosition {
3450 buffer_id: BufferId(1),
3451 line: 10,
3452 request_id: 123,
3453 };
3454 let json = serde_json::to_string(&command).unwrap();
3455 assert!(json.contains("GetLineEndPosition"));
3456 assert!(json.contains("10"));
3457 }
3458
3459 #[test]
3460 fn test_plugin_command_get_buffer_line_count() {
3461 let command = PluginCommand::GetBufferLineCount {
3462 buffer_id: BufferId(0),
3463 request_id: 456,
3464 };
3465 let json = serde_json::to_string(&command).unwrap();
3466 assert!(json.contains("GetBufferLineCount"));
3467 assert!(json.contains("456"));
3468 }
3469
3470 #[test]
3471 fn test_plugin_command_scroll_to_line_center() {
3472 let command = PluginCommand::ScrollToLineCenter {
3473 split_id: SplitId(1),
3474 buffer_id: BufferId(2),
3475 line: 50,
3476 };
3477 let json = serde_json::to_string(&command).unwrap();
3478 assert!(json.contains("ScrollToLineCenter"));
3479 assert!(json.contains("50"));
3480 }
3481
3482 #[test]
3485 fn js_callback_id_conversions_and_display() {
3486 for raw in [0u64, 1, 42, u64::MAX] {
3487 let id = JsCallbackId::new(raw);
3488 assert_eq!(id.as_u64(), raw);
3489 assert_eq!(u64::from(id), raw);
3490 assert_eq!(JsCallbackId::from(raw), id);
3491 assert_eq!(id.to_string(), raw.to_string());
3492 }
3493 }
3494
3495 #[test]
3499 fn serde_defaults_fire_when_fields_are_omitted() {
3500 let spec: ActionSpec = serde_json::from_str(r#"{"action": "move_left"}"#).unwrap();
3502 assert_eq!(spec.count, 1);
3503 let spec: ActionSpec =
3504 serde_json::from_str(r#"{"action": "move_left", "count": 5}"#).unwrap();
3505 assert_eq!(spec.count, 5);
3506
3507 let layout: CompositeLayoutConfig =
3509 serde_json::from_str(r#"{"type": "side-by-side"}"#).unwrap();
3510 assert!(layout.show_separator);
3511 let layout: CompositeLayoutConfig =
3512 serde_json::from_str(r#"{"type": "side-by-side", "showSeparator": false}"#).unwrap();
3513 assert!(!layout.show_separator);
3514
3515 let reg: TsCompletionProviderRegistration =
3517 serde_json::from_str(r#"{"id": "p", "displayName": "P"}"#).unwrap();
3518 assert_eq!(reg.priority, 50);
3519 let reg: TsCompletionProviderRegistration =
3520 serde_json::from_str(r#"{"id": "p", "displayName": "P", "priority": 3}"#).unwrap();
3521 assert_eq!(reg.priority, 3);
3522 }
3523}