1use anyhow::Result;
6use std::collections::HashMap;
7use std::ops::Range;
8use std::path::PathBuf;
9
10use crate::action::Action;
11use crate::api::{ViewTokenWire, ViewTokenWireKind};
12use crate::{BufferId, CursorId, SplitId};
13
14#[derive(Debug, Clone, serde::Serialize)]
16pub enum HookArgs {
17 BeforeFileOpen { path: PathBuf },
19
20 AfterFileOpen { buffer_id: BufferId, path: PathBuf },
22
23 BeforeFileSave { buffer_id: BufferId, path: PathBuf },
25
26 AfterFileSave { buffer_id: BufferId, path: PathBuf },
28
29 BufferClosed { buffer_id: BufferId },
31
32 BeforeInsert {
34 buffer_id: BufferId,
35 position: usize,
36 text: String,
37 },
38
39 AfterInsert {
41 buffer_id: BufferId,
42 position: usize,
43 text: String,
44 affected_start: usize,
46 affected_end: usize,
48 start_line: usize,
50 end_line: usize,
52 lines_added: usize,
54 },
55
56 BeforeDelete {
58 buffer_id: BufferId,
59 range: Range<usize>,
60 },
61
62 AfterDelete {
64 buffer_id: BufferId,
65 range: Range<usize>,
66 deleted_text: String,
67 affected_start: usize,
69 deleted_len: usize,
71 start_line: usize,
73 end_line: usize,
75 lines_removed: usize,
77 },
78
79 CursorMoved {
81 buffer_id: BufferId,
82 cursor_id: CursorId,
83 old_position: usize,
84 new_position: usize,
85 line: usize,
87 text_properties: Vec<std::collections::HashMap<String, serde_json::Value>>,
89 },
90
91 BufferActivated { buffer_id: BufferId },
93
94 BufferDeactivated { buffer_id: BufferId },
96
97 DiagnosticsUpdated {
99 uri: String,
101 count: usize,
103 },
104
105 PreCommand { action: Action },
107
108 PostCommand { action: Action },
110
111 Idle { milliseconds: u64 },
113
114 EditorInitialized,
116
117 PluginsLoaded,
122
123 Ready,
126
127 RenderStart { buffer_id: BufferId },
129
130 RenderLine {
132 buffer_id: BufferId,
133 line_number: usize,
134 byte_start: usize,
135 byte_end: usize,
136 content: String,
137 },
138
139 LinesChanged {
141 buffer_id: BufferId,
142 lines: Vec<LineInfo>,
143 },
144
145 PromptChanged { prompt_type: String, input: String },
147
148 PromptConfirmed {
150 prompt_type: String,
151 input: String,
152 selected_index: Option<usize>,
153 },
154
155 PromptCancelled { prompt_type: String, input: String },
157
158 PromptSelectionChanged {
160 prompt_type: String,
161 selected_index: usize,
162 },
163
164 KeyboardShortcuts { bindings: Vec<(String, String)> },
166
167 LspReferences {
169 symbol: String,
171 locations: Vec<LspLocation>,
173 },
174
175 ViewTransformRequest {
177 buffer_id: BufferId,
178 split_id: SplitId,
179 viewport_start: usize,
181 viewport_end: usize,
183 tokens: Vec<ViewTokenWire>,
185 cursor_positions: Vec<usize>,
187 },
188
189 MouseClick {
191 column: u16,
193 row: u16,
195 button: String,
197 modifiers: String,
199 content_x: u16,
201 content_y: u16,
203 buffer_id: Option<u64>,
206 buffer_row: Option<u32>,
209 buffer_col: Option<u32>,
212 },
213
214 MouseMove {
216 column: u16,
218 row: u16,
220 content_x: u16,
222 content_y: u16,
224 },
225
226 LspServerRequest {
228 language: String,
230 method: String,
232 server_command: String,
234 params: Option<String>,
236 },
237
238 ViewportChanged {
240 split_id: SplitId,
241 buffer_id: BufferId,
242 top_byte: usize,
243 top_line: Option<usize>,
244 width: u16,
245 height: u16,
246 },
247
248 LspServerError {
250 language: String,
252 server_command: String,
254 error_type: String,
256 message: String,
258 },
259
260 LspStatusClicked {
262 language: String,
264 has_error: bool,
266 missing_servers: Vec<String>,
271 user_dismissed: bool,
276 },
277
278 ActionPopupResult {
280 popup_id: String,
282 action_id: String,
284 },
285
286 ProcessOutput {
288 process_id: u64,
290 data: String,
292 },
293
294 LanguageChanged {
296 buffer_id: BufferId,
297 language: String,
299 },
300
301 ThemeInspectKey {
303 theme_name: String,
305 key: String,
307 },
308
309 MouseScroll {
311 buffer_id: BufferId,
312 delta: i32,
314 col: u16,
316 row: u16,
318 },
319
320 Resize { width: u16, height: u16 },
322
323 FocusGained,
325}
326
327#[derive(Debug, Clone, serde::Serialize)]
329pub struct LineInfo {
330 pub line_number: usize,
332 pub byte_start: usize,
334 pub byte_end: usize,
336 pub content: String,
338}
339
340#[derive(Debug, Clone, serde::Serialize)]
342pub struct LspLocation {
343 pub file: String,
345 pub line: u32,
347 pub column: u32,
349}
350
351pub type HookCallback = Box<dyn Fn(&HookArgs) -> bool + Send + Sync>;
353
354pub struct HookRegistry {
356 hooks: HashMap<String, Vec<HookCallback>>,
358}
359
360impl HookRegistry {
361 pub fn new() -> Self {
363 Self {
364 hooks: HashMap::new(),
365 }
366 }
367
368 pub fn add_hook(&mut self, name: &str, callback: HookCallback) {
370 self.hooks
371 .entry(name.to_string())
372 .or_default()
373 .push(callback);
374 }
375
376 pub fn remove_hooks(&mut self, name: &str) {
378 self.hooks.remove(name);
379 }
380
381 pub fn run_hooks(&self, name: &str, args: &HookArgs) -> bool {
383 if let Some(hooks) = self.hooks.get(name) {
384 for callback in hooks {
385 if !callback(args) {
386 return false;
387 }
388 }
389 }
390 true
391 }
392
393 pub fn hook_count(&self, name: &str) -> usize {
395 self.hooks.get(name).map(|v| v.len()).unwrap_or(0)
396 }
397
398 pub fn hook_names(&self) -> Vec<String> {
400 self.hooks.keys().cloned().collect()
401 }
402}
403
404impl Default for HookRegistry {
405 fn default() -> Self {
406 Self::new()
407 }
408}
409
410pub fn hook_args_to_json(args: &HookArgs) -> Result<serde_json::Value> {
412 let json_value = match args {
413 HookArgs::RenderStart { buffer_id } => {
414 serde_json::json!({
415 "buffer_id": buffer_id.0,
416 })
417 }
418 HookArgs::RenderLine {
419 buffer_id,
420 line_number,
421 byte_start,
422 byte_end,
423 content,
424 } => {
425 serde_json::json!({
426 "buffer_id": buffer_id.0,
427 "line_number": line_number,
428 "byte_start": byte_start,
429 "byte_end": byte_end,
430 "content": content,
431 })
432 }
433 HookArgs::BufferActivated { buffer_id } => {
434 serde_json::json!({ "buffer_id": buffer_id.0 })
435 }
436 HookArgs::BufferDeactivated { buffer_id } => {
437 serde_json::json!({ "buffer_id": buffer_id.0 })
438 }
439 HookArgs::DiagnosticsUpdated { uri, count } => {
440 serde_json::json!({
441 "uri": uri,
442 "count": count,
443 })
444 }
445 HookArgs::BufferClosed { buffer_id } => {
446 serde_json::json!({ "buffer_id": buffer_id.0 })
447 }
448 HookArgs::CursorMoved {
449 buffer_id,
450 cursor_id,
451 old_position,
452 new_position,
453 line,
454 text_properties,
455 } => {
456 serde_json::json!({
457 "buffer_id": buffer_id.0,
458 "cursor_id": cursor_id.0,
459 "old_position": old_position,
460 "new_position": new_position,
461 "line": line,
462 "text_properties": text_properties,
463 })
464 }
465 HookArgs::BeforeInsert {
466 buffer_id,
467 position,
468 text,
469 } => {
470 serde_json::json!({
471 "buffer_id": buffer_id.0,
472 "position": position,
473 "text": text,
474 })
475 }
476 HookArgs::AfterInsert {
477 buffer_id,
478 position,
479 text,
480 affected_start,
481 affected_end,
482 start_line,
483 end_line,
484 lines_added,
485 } => {
486 serde_json::json!({
487 "buffer_id": buffer_id.0,
488 "position": position,
489 "text": text,
490 "affected_start": affected_start,
491 "affected_end": affected_end,
492 "start_line": start_line,
493 "end_line": end_line,
494 "lines_added": lines_added,
495 })
496 }
497 HookArgs::BeforeDelete { buffer_id, range } => {
498 serde_json::json!({
499 "buffer_id": buffer_id.0,
500 "start": range.start,
501 "end": range.end,
502 })
503 }
504 HookArgs::AfterDelete {
505 buffer_id,
506 range,
507 deleted_text,
508 affected_start,
509 deleted_len,
510 start_line,
511 end_line,
512 lines_removed,
513 } => {
514 serde_json::json!({
515 "buffer_id": buffer_id.0,
516 "start": range.start,
517 "end": range.end,
518 "deleted_text": deleted_text,
519 "affected_start": affected_start,
520 "deleted_len": deleted_len,
521 "start_line": start_line,
522 "end_line": end_line,
523 "lines_removed": lines_removed,
524 })
525 }
526 HookArgs::BeforeFileOpen { path } => {
527 serde_json::json!({ "path": path.to_string_lossy() })
528 }
529 HookArgs::AfterFileOpen { path, buffer_id } => {
530 serde_json::json!({
531 "path": path.to_string_lossy(),
532 "buffer_id": buffer_id.0,
533 })
534 }
535 HookArgs::BeforeFileSave { path, buffer_id } => {
536 serde_json::json!({
537 "path": path.to_string_lossy(),
538 "buffer_id": buffer_id.0,
539 })
540 }
541 HookArgs::AfterFileSave { path, buffer_id } => {
542 serde_json::json!({
543 "path": path.to_string_lossy(),
544 "buffer_id": buffer_id.0,
545 })
546 }
547 HookArgs::PreCommand { action } => {
548 serde_json::json!({ "action": format!("{:?}", action) })
549 }
550 HookArgs::PostCommand { action } => {
551 serde_json::json!({ "action": format!("{:?}", action) })
552 }
553 HookArgs::Idle { milliseconds } => {
554 serde_json::json!({ "milliseconds": milliseconds })
555 }
556 HookArgs::EditorInitialized => {
557 serde_json::json!({})
558 }
559 HookArgs::PluginsLoaded => {
560 serde_json::json!({})
561 }
562 HookArgs::Ready => {
563 serde_json::json!({})
564 }
565 HookArgs::PromptChanged { prompt_type, input } => {
566 serde_json::json!({
567 "prompt_type": prompt_type,
568 "input": input,
569 })
570 }
571 HookArgs::PromptConfirmed {
572 prompt_type,
573 input,
574 selected_index,
575 } => {
576 serde_json::json!({
577 "prompt_type": prompt_type,
578 "input": input,
579 "selected_index": selected_index,
580 })
581 }
582 HookArgs::PromptCancelled { prompt_type, input } => {
583 serde_json::json!({
584 "prompt_type": prompt_type,
585 "input": input,
586 })
587 }
588 HookArgs::PromptSelectionChanged {
589 prompt_type,
590 selected_index,
591 } => {
592 serde_json::json!({
593 "prompt_type": prompt_type,
594 "selected_index": selected_index,
595 })
596 }
597 HookArgs::KeyboardShortcuts { bindings } => {
598 let entries: Vec<serde_json::Value> = bindings
599 .iter()
600 .map(|(key, action)| serde_json::json!({ "key": key, "action": action }))
601 .collect();
602 serde_json::json!({ "bindings": entries })
603 }
604 HookArgs::LspReferences { symbol, locations } => {
605 let locs: Vec<serde_json::Value> = locations
606 .iter()
607 .map(|loc| {
608 serde_json::json!({
609 "file": loc.file,
610 "line": loc.line,
611 "column": loc.column,
612 })
613 })
614 .collect();
615 serde_json::json!({ "symbol": symbol, "locations": locs })
616 }
617 HookArgs::LinesChanged { buffer_id, lines } => {
618 let lines_json: Vec<serde_json::Value> = lines
619 .iter()
620 .map(|line| {
621 serde_json::json!({
622 "line_number": line.line_number,
623 "byte_start": line.byte_start,
624 "byte_end": line.byte_end,
625 "content": line.content,
626 })
627 })
628 .collect();
629 serde_json::json!({
630 "buffer_id": buffer_id.0,
631 "lines": lines_json,
632 })
633 }
634 HookArgs::ViewTransformRequest {
635 buffer_id,
636 split_id,
637 viewport_start,
638 viewport_end,
639 tokens,
640 cursor_positions,
641 } => {
642 let tokens_json: Vec<serde_json::Value> = tokens
643 .iter()
644 .map(|token| {
645 let kind_json = match &token.kind {
646 ViewTokenWireKind::Text(s) => serde_json::json!({ "Text": s }),
647 ViewTokenWireKind::Newline => serde_json::json!("Newline"),
648 ViewTokenWireKind::Space => serde_json::json!("Space"),
649 ViewTokenWireKind::Break => serde_json::json!("Break"),
650 ViewTokenWireKind::BinaryByte(b) => serde_json::json!({ "BinaryByte": b }),
651 };
652 serde_json::json!({
653 "source_offset": token.source_offset,
654 "kind": kind_json,
655 })
656 })
657 .collect();
658 serde_json::json!({
659 "buffer_id": buffer_id.0,
660 "split_id": split_id.0,
661 "viewport_start": viewport_start,
662 "viewport_end": viewport_end,
663 "tokens": tokens_json,
664 "cursor_positions": cursor_positions,
665 })
666 }
667 HookArgs::MouseClick {
668 column,
669 row,
670 button,
671 modifiers,
672 content_x,
673 content_y,
674 buffer_id,
675 buffer_row,
676 buffer_col,
677 } => {
678 serde_json::json!({
679 "column": column,
680 "row": row,
681 "button": button,
682 "modifiers": modifiers,
683 "content_x": content_x,
684 "content_y": content_y,
685 "buffer_id": buffer_id,
686 "buffer_row": buffer_row,
687 "buffer_col": buffer_col,
688 })
689 }
690 HookArgs::MouseMove {
691 column,
692 row,
693 content_x,
694 content_y,
695 } => {
696 serde_json::json!({
697 "column": column,
698 "row": row,
699 "content_x": content_x,
700 "content_y": content_y,
701 })
702 }
703 HookArgs::LspServerRequest {
704 language,
705 method,
706 server_command,
707 params,
708 } => {
709 serde_json::json!({
710 "language": language,
711 "method": method,
712 "server_command": server_command,
713 "params": params,
714 })
715 }
716 HookArgs::ViewportChanged {
717 split_id,
718 buffer_id,
719 top_byte,
720 top_line,
721 width,
722 height,
723 } => {
724 serde_json::json!({
725 "split_id": split_id.0,
726 "buffer_id": buffer_id.0,
727 "top_byte": top_byte,
728 "top_line": top_line,
729 "width": width,
730 "height": height,
731 })
732 }
733 HookArgs::LspServerError {
734 language,
735 server_command,
736 error_type,
737 message,
738 } => {
739 serde_json::json!({
740 "language": language,
741 "server_command": server_command,
742 "error_type": error_type,
743 "message": message,
744 })
745 }
746 HookArgs::LspStatusClicked {
747 language,
748 has_error,
749 missing_servers,
750 user_dismissed,
751 } => {
752 serde_json::json!({
753 "language": language,
754 "has_error": has_error,
755 "missing_servers": missing_servers,
756 "user_dismissed": user_dismissed,
757 })
758 }
759 HookArgs::ActionPopupResult {
760 popup_id,
761 action_id,
762 } => {
763 serde_json::json!({
764 "popup_id": popup_id,
765 "action_id": action_id,
766 })
767 }
768 HookArgs::ProcessOutput { process_id, data } => {
769 serde_json::json!({
770 "process_id": process_id,
771 "data": data,
772 })
773 }
774 HookArgs::LanguageChanged {
775 buffer_id,
776 language,
777 } => {
778 serde_json::json!({
779 "buffer_id": buffer_id.0,
780 "language": language,
781 })
782 }
783 HookArgs::ThemeInspectKey { theme_name, key } => {
784 serde_json::json!({
785 "theme_name": theme_name,
786 "key": key,
787 })
788 }
789 HookArgs::MouseScroll {
790 buffer_id,
791 delta,
792 col,
793 row,
794 } => {
795 serde_json::json!({
796 "buffer_id": buffer_id.0,
797 "delta": delta,
798 "col": col,
799 "row": row,
800 })
801 }
802 HookArgs::Resize { width, height } => {
803 serde_json::json!({
804 "width": width,
805 "height": height,
806 })
807 }
808 HookArgs::FocusGained => {
809 serde_json::json!({})
810 }
811 };
812
813 Ok(json_value)
814}
815
816#[cfg(test)]
817mod tests {
818 use super::*;
819 use std::sync::atomic::{AtomicUsize, Ordering};
820 use std::sync::Arc;
821
822 fn noop_true() -> HookCallback {
823 Box::new(|_| true)
824 }
825
826 #[test]
830 fn add_count_list_remove_round_trip() {
831 let mut reg = HookRegistry::new();
832 assert_eq!(reg.hook_count("a"), 0);
833 assert!(reg.hook_names().is_empty());
834
835 reg.add_hook("a", noop_true());
836 reg.add_hook("a", noop_true());
837 reg.add_hook("b", noop_true());
838
839 assert_eq!(reg.hook_count("a"), 2);
840 assert_eq!(reg.hook_count("b"), 1);
841 assert_eq!(reg.hook_count("missing"), 0);
842
843 let mut names = reg.hook_names();
844 names.sort();
845 assert_eq!(names, vec!["a".to_string(), "b".to_string()]);
846
847 reg.remove_hooks("a");
848 assert_eq!(reg.hook_count("a"), 0);
849 assert_eq!(reg.hook_count("b"), 1);
850 assert_eq!(reg.hook_names(), vec!["b".to_string()]);
851 }
852
853 #[test]
856 fn run_hooks_all_true_and_short_circuits_on_false() {
857 let mut reg = HookRegistry::new();
858 let args = HookArgs::EditorInitialized;
859
860 assert!(reg.run_hooks("unknown", &args));
862
863 let calls = Arc::new(AtomicUsize::new(0));
865 for _ in 0..3 {
866 let c = calls.clone();
867 reg.add_hook(
868 "all_true",
869 Box::new(move |_| {
870 c.fetch_add(1, Ordering::SeqCst);
871 true
872 }),
873 );
874 }
875 assert!(reg.run_hooks("all_true", &args));
876 assert_eq!(calls.load(Ordering::SeqCst), 3);
877
878 let calls = Arc::new(AtomicUsize::new(0));
880 let c1 = calls.clone();
881 reg.add_hook(
882 "short",
883 Box::new(move |_| {
884 c1.fetch_add(1, Ordering::SeqCst);
885 false
886 }),
887 );
888 let c2 = calls.clone();
889 reg.add_hook(
890 "short",
891 Box::new(move |_| {
892 c2.fetch_add(1, Ordering::SeqCst);
893 true
894 }),
895 );
896 assert!(!reg.run_hooks("short", &args));
897 assert_eq!(calls.load(Ordering::SeqCst), 1);
898 }
899
900 #[test]
904 fn hook_args_to_json_serializes_payload_fields() {
905 let json = hook_args_to_json(&HookArgs::DiagnosticsUpdated {
906 uri: "file:///x.rs".into(),
907 count: 7,
908 })
909 .unwrap();
910 assert_eq!(json["uri"], "file:///x.rs");
911 assert_eq!(json["count"], 7);
912 }
913}