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 RenderStart { buffer_id: BufferId },
119
120 RenderLine {
122 buffer_id: BufferId,
123 line_number: usize,
124 byte_start: usize,
125 byte_end: usize,
126 content: String,
127 },
128
129 LinesChanged {
131 buffer_id: BufferId,
132 lines: Vec<LineInfo>,
133 },
134
135 PromptChanged { prompt_type: String, input: String },
137
138 PromptConfirmed {
140 prompt_type: String,
141 input: String,
142 selected_index: Option<usize>,
143 },
144
145 PromptCancelled { prompt_type: String, input: String },
147
148 PromptSelectionChanged {
150 prompt_type: String,
151 selected_index: usize,
152 },
153
154 KeyboardShortcuts { bindings: Vec<(String, String)> },
156
157 LspReferences {
159 symbol: String,
161 locations: Vec<LspLocation>,
163 },
164
165 ViewTransformRequest {
167 buffer_id: BufferId,
168 split_id: SplitId,
169 viewport_start: usize,
171 viewport_end: usize,
173 tokens: Vec<ViewTokenWire>,
175 cursor_positions: Vec<usize>,
177 },
178
179 MouseClick {
181 column: u16,
183 row: u16,
185 button: String,
187 modifiers: String,
189 content_x: u16,
191 content_y: u16,
193 buffer_id: Option<u64>,
196 buffer_row: Option<u32>,
199 buffer_col: Option<u32>,
202 },
203
204 MouseMove {
206 column: u16,
208 row: u16,
210 content_x: u16,
212 content_y: u16,
214 },
215
216 LspServerRequest {
218 language: String,
220 method: String,
222 server_command: String,
224 params: Option<String>,
226 },
227
228 ViewportChanged {
230 split_id: SplitId,
231 buffer_id: BufferId,
232 top_byte: usize,
233 top_line: Option<usize>,
234 width: u16,
235 height: u16,
236 },
237
238 LspServerError {
240 language: String,
242 server_command: String,
244 error_type: String,
246 message: String,
248 },
249
250 LspStatusClicked {
252 language: String,
254 has_error: bool,
256 missing_servers: Vec<String>,
261 user_dismissed: bool,
266 },
267
268 ActionPopupResult {
270 popup_id: String,
272 action_id: String,
274 },
275
276 ProcessOutput {
278 process_id: u64,
280 data: String,
282 },
283
284 LanguageChanged {
286 buffer_id: BufferId,
287 language: String,
289 },
290
291 ThemeInspectKey {
293 theme_name: String,
295 key: String,
297 },
298
299 MouseScroll {
301 buffer_id: BufferId,
302 delta: i32,
304 col: u16,
306 row: u16,
308 },
309
310 Resize { width: u16, height: u16 },
312
313 FocusGained,
315}
316
317#[derive(Debug, Clone, serde::Serialize)]
319pub struct LineInfo {
320 pub line_number: usize,
322 pub byte_start: usize,
324 pub byte_end: usize,
326 pub content: String,
328}
329
330#[derive(Debug, Clone, serde::Serialize)]
332pub struct LspLocation {
333 pub file: String,
335 pub line: u32,
337 pub column: u32,
339}
340
341pub type HookCallback = Box<dyn Fn(&HookArgs) -> bool + Send + Sync>;
343
344pub struct HookRegistry {
346 hooks: HashMap<String, Vec<HookCallback>>,
348}
349
350impl HookRegistry {
351 pub fn new() -> Self {
353 Self {
354 hooks: HashMap::new(),
355 }
356 }
357
358 pub fn add_hook(&mut self, name: &str, callback: HookCallback) {
360 self.hooks
361 .entry(name.to_string())
362 .or_default()
363 .push(callback);
364 }
365
366 pub fn remove_hooks(&mut self, name: &str) {
368 self.hooks.remove(name);
369 }
370
371 pub fn run_hooks(&self, name: &str, args: &HookArgs) -> bool {
373 if let Some(hooks) = self.hooks.get(name) {
374 for callback in hooks {
375 if !callback(args) {
376 return false;
377 }
378 }
379 }
380 true
381 }
382
383 pub fn hook_count(&self, name: &str) -> usize {
385 self.hooks.get(name).map(|v| v.len()).unwrap_or(0)
386 }
387
388 pub fn hook_names(&self) -> Vec<String> {
390 self.hooks.keys().cloned().collect()
391 }
392}
393
394impl Default for HookRegistry {
395 fn default() -> Self {
396 Self::new()
397 }
398}
399
400pub fn hook_args_to_json(args: &HookArgs) -> Result<serde_json::Value> {
402 let json_value = match args {
403 HookArgs::RenderStart { buffer_id } => {
404 serde_json::json!({
405 "buffer_id": buffer_id.0,
406 })
407 }
408 HookArgs::RenderLine {
409 buffer_id,
410 line_number,
411 byte_start,
412 byte_end,
413 content,
414 } => {
415 serde_json::json!({
416 "buffer_id": buffer_id.0,
417 "line_number": line_number,
418 "byte_start": byte_start,
419 "byte_end": byte_end,
420 "content": content,
421 })
422 }
423 HookArgs::BufferActivated { buffer_id } => {
424 serde_json::json!({ "buffer_id": buffer_id.0 })
425 }
426 HookArgs::BufferDeactivated { buffer_id } => {
427 serde_json::json!({ "buffer_id": buffer_id.0 })
428 }
429 HookArgs::DiagnosticsUpdated { uri, count } => {
430 serde_json::json!({
431 "uri": uri,
432 "count": count,
433 })
434 }
435 HookArgs::BufferClosed { buffer_id } => {
436 serde_json::json!({ "buffer_id": buffer_id.0 })
437 }
438 HookArgs::CursorMoved {
439 buffer_id,
440 cursor_id,
441 old_position,
442 new_position,
443 line,
444 text_properties,
445 } => {
446 serde_json::json!({
447 "buffer_id": buffer_id.0,
448 "cursor_id": cursor_id.0,
449 "old_position": old_position,
450 "new_position": new_position,
451 "line": line,
452 "text_properties": text_properties,
453 })
454 }
455 HookArgs::BeforeInsert {
456 buffer_id,
457 position,
458 text,
459 } => {
460 serde_json::json!({
461 "buffer_id": buffer_id.0,
462 "position": position,
463 "text": text,
464 })
465 }
466 HookArgs::AfterInsert {
467 buffer_id,
468 position,
469 text,
470 affected_start,
471 affected_end,
472 start_line,
473 end_line,
474 lines_added,
475 } => {
476 serde_json::json!({
477 "buffer_id": buffer_id.0,
478 "position": position,
479 "text": text,
480 "affected_start": affected_start,
481 "affected_end": affected_end,
482 "start_line": start_line,
483 "end_line": end_line,
484 "lines_added": lines_added,
485 })
486 }
487 HookArgs::BeforeDelete { buffer_id, range } => {
488 serde_json::json!({
489 "buffer_id": buffer_id.0,
490 "start": range.start,
491 "end": range.end,
492 })
493 }
494 HookArgs::AfterDelete {
495 buffer_id,
496 range,
497 deleted_text,
498 affected_start,
499 deleted_len,
500 start_line,
501 end_line,
502 lines_removed,
503 } => {
504 serde_json::json!({
505 "buffer_id": buffer_id.0,
506 "start": range.start,
507 "end": range.end,
508 "deleted_text": deleted_text,
509 "affected_start": affected_start,
510 "deleted_len": deleted_len,
511 "start_line": start_line,
512 "end_line": end_line,
513 "lines_removed": lines_removed,
514 })
515 }
516 HookArgs::BeforeFileOpen { path } => {
517 serde_json::json!({ "path": path.to_string_lossy() })
518 }
519 HookArgs::AfterFileOpen { path, buffer_id } => {
520 serde_json::json!({
521 "path": path.to_string_lossy(),
522 "buffer_id": buffer_id.0,
523 })
524 }
525 HookArgs::BeforeFileSave { path, buffer_id } => {
526 serde_json::json!({
527 "path": path.to_string_lossy(),
528 "buffer_id": buffer_id.0,
529 })
530 }
531 HookArgs::AfterFileSave { path, buffer_id } => {
532 serde_json::json!({
533 "path": path.to_string_lossy(),
534 "buffer_id": buffer_id.0,
535 })
536 }
537 HookArgs::PreCommand { action } => {
538 serde_json::json!({ "action": format!("{:?}", action) })
539 }
540 HookArgs::PostCommand { action } => {
541 serde_json::json!({ "action": format!("{:?}", action) })
542 }
543 HookArgs::Idle { milliseconds } => {
544 serde_json::json!({ "milliseconds": milliseconds })
545 }
546 HookArgs::EditorInitialized => {
547 serde_json::json!({})
548 }
549 HookArgs::PromptChanged { prompt_type, input } => {
550 serde_json::json!({
551 "prompt_type": prompt_type,
552 "input": input,
553 })
554 }
555 HookArgs::PromptConfirmed {
556 prompt_type,
557 input,
558 selected_index,
559 } => {
560 serde_json::json!({
561 "prompt_type": prompt_type,
562 "input": input,
563 "selected_index": selected_index,
564 })
565 }
566 HookArgs::PromptCancelled { prompt_type, input } => {
567 serde_json::json!({
568 "prompt_type": prompt_type,
569 "input": input,
570 })
571 }
572 HookArgs::PromptSelectionChanged {
573 prompt_type,
574 selected_index,
575 } => {
576 serde_json::json!({
577 "prompt_type": prompt_type,
578 "selected_index": selected_index,
579 })
580 }
581 HookArgs::KeyboardShortcuts { bindings } => {
582 let entries: Vec<serde_json::Value> = bindings
583 .iter()
584 .map(|(key, action)| serde_json::json!({ "key": key, "action": action }))
585 .collect();
586 serde_json::json!({ "bindings": entries })
587 }
588 HookArgs::LspReferences { symbol, locations } => {
589 let locs: Vec<serde_json::Value> = locations
590 .iter()
591 .map(|loc| {
592 serde_json::json!({
593 "file": loc.file,
594 "line": loc.line,
595 "column": loc.column,
596 })
597 })
598 .collect();
599 serde_json::json!({ "symbol": symbol, "locations": locs })
600 }
601 HookArgs::LinesChanged { buffer_id, lines } => {
602 let lines_json: Vec<serde_json::Value> = lines
603 .iter()
604 .map(|line| {
605 serde_json::json!({
606 "line_number": line.line_number,
607 "byte_start": line.byte_start,
608 "byte_end": line.byte_end,
609 "content": line.content,
610 })
611 })
612 .collect();
613 serde_json::json!({
614 "buffer_id": buffer_id.0,
615 "lines": lines_json,
616 })
617 }
618 HookArgs::ViewTransformRequest {
619 buffer_id,
620 split_id,
621 viewport_start,
622 viewport_end,
623 tokens,
624 cursor_positions,
625 } => {
626 let tokens_json: Vec<serde_json::Value> = tokens
627 .iter()
628 .map(|token| {
629 let kind_json = match &token.kind {
630 ViewTokenWireKind::Text(s) => serde_json::json!({ "Text": s }),
631 ViewTokenWireKind::Newline => serde_json::json!("Newline"),
632 ViewTokenWireKind::Space => serde_json::json!("Space"),
633 ViewTokenWireKind::Break => serde_json::json!("Break"),
634 ViewTokenWireKind::BinaryByte(b) => serde_json::json!({ "BinaryByte": b }),
635 };
636 serde_json::json!({
637 "source_offset": token.source_offset,
638 "kind": kind_json,
639 })
640 })
641 .collect();
642 serde_json::json!({
643 "buffer_id": buffer_id.0,
644 "split_id": split_id.0,
645 "viewport_start": viewport_start,
646 "viewport_end": viewport_end,
647 "tokens": tokens_json,
648 "cursor_positions": cursor_positions,
649 })
650 }
651 HookArgs::MouseClick {
652 column,
653 row,
654 button,
655 modifiers,
656 content_x,
657 content_y,
658 buffer_id,
659 buffer_row,
660 buffer_col,
661 } => {
662 serde_json::json!({
663 "column": column,
664 "row": row,
665 "button": button,
666 "modifiers": modifiers,
667 "content_x": content_x,
668 "content_y": content_y,
669 "buffer_id": buffer_id,
670 "buffer_row": buffer_row,
671 "buffer_col": buffer_col,
672 })
673 }
674 HookArgs::MouseMove {
675 column,
676 row,
677 content_x,
678 content_y,
679 } => {
680 serde_json::json!({
681 "column": column,
682 "row": row,
683 "content_x": content_x,
684 "content_y": content_y,
685 })
686 }
687 HookArgs::LspServerRequest {
688 language,
689 method,
690 server_command,
691 params,
692 } => {
693 serde_json::json!({
694 "language": language,
695 "method": method,
696 "server_command": server_command,
697 "params": params,
698 })
699 }
700 HookArgs::ViewportChanged {
701 split_id,
702 buffer_id,
703 top_byte,
704 top_line,
705 width,
706 height,
707 } => {
708 serde_json::json!({
709 "split_id": split_id.0,
710 "buffer_id": buffer_id.0,
711 "top_byte": top_byte,
712 "top_line": top_line,
713 "width": width,
714 "height": height,
715 })
716 }
717 HookArgs::LspServerError {
718 language,
719 server_command,
720 error_type,
721 message,
722 } => {
723 serde_json::json!({
724 "language": language,
725 "server_command": server_command,
726 "error_type": error_type,
727 "message": message,
728 })
729 }
730 HookArgs::LspStatusClicked {
731 language,
732 has_error,
733 missing_servers,
734 user_dismissed,
735 } => {
736 serde_json::json!({
737 "language": language,
738 "has_error": has_error,
739 "missing_servers": missing_servers,
740 "user_dismissed": user_dismissed,
741 })
742 }
743 HookArgs::ActionPopupResult {
744 popup_id,
745 action_id,
746 } => {
747 serde_json::json!({
748 "popup_id": popup_id,
749 "action_id": action_id,
750 })
751 }
752 HookArgs::ProcessOutput { process_id, data } => {
753 serde_json::json!({
754 "process_id": process_id,
755 "data": data,
756 })
757 }
758 HookArgs::LanguageChanged {
759 buffer_id,
760 language,
761 } => {
762 serde_json::json!({
763 "buffer_id": buffer_id.0,
764 "language": language,
765 })
766 }
767 HookArgs::ThemeInspectKey { theme_name, key } => {
768 serde_json::json!({
769 "theme_name": theme_name,
770 "key": key,
771 })
772 }
773 HookArgs::MouseScroll {
774 buffer_id,
775 delta,
776 col,
777 row,
778 } => {
779 serde_json::json!({
780 "buffer_id": buffer_id.0,
781 "delta": delta,
782 "col": col,
783 "row": row,
784 })
785 }
786 HookArgs::Resize { width, height } => {
787 serde_json::json!({
788 "width": width,
789 "height": height,
790 })
791 }
792 HookArgs::FocusGained => {
793 serde_json::json!({})
794 }
795 };
796
797 Ok(json_value)
798}
799
800#[cfg(test)]
801mod tests {
802 use super::*;
803 use std::sync::atomic::{AtomicUsize, Ordering};
804 use std::sync::Arc;
805
806 fn noop_true() -> HookCallback {
807 Box::new(|_| true)
808 }
809
810 #[test]
814 fn add_count_list_remove_round_trip() {
815 let mut reg = HookRegistry::new();
816 assert_eq!(reg.hook_count("a"), 0);
817 assert!(reg.hook_names().is_empty());
818
819 reg.add_hook("a", noop_true());
820 reg.add_hook("a", noop_true());
821 reg.add_hook("b", noop_true());
822
823 assert_eq!(reg.hook_count("a"), 2);
824 assert_eq!(reg.hook_count("b"), 1);
825 assert_eq!(reg.hook_count("missing"), 0);
826
827 let mut names = reg.hook_names();
828 names.sort();
829 assert_eq!(names, vec!["a".to_string(), "b".to_string()]);
830
831 reg.remove_hooks("a");
832 assert_eq!(reg.hook_count("a"), 0);
833 assert_eq!(reg.hook_count("b"), 1);
834 assert_eq!(reg.hook_names(), vec!["b".to_string()]);
835 }
836
837 #[test]
840 fn run_hooks_all_true_and_short_circuits_on_false() {
841 let mut reg = HookRegistry::new();
842 let args = HookArgs::EditorInitialized;
843
844 assert!(reg.run_hooks("unknown", &args));
846
847 let calls = Arc::new(AtomicUsize::new(0));
849 for _ in 0..3 {
850 let c = calls.clone();
851 reg.add_hook(
852 "all_true",
853 Box::new(move |_| {
854 c.fetch_add(1, Ordering::SeqCst);
855 true
856 }),
857 );
858 }
859 assert!(reg.run_hooks("all_true", &args));
860 assert_eq!(calls.load(Ordering::SeqCst), 3);
861
862 let calls = Arc::new(AtomicUsize::new(0));
864 let c1 = calls.clone();
865 reg.add_hook(
866 "short",
867 Box::new(move |_| {
868 c1.fetch_add(1, Ordering::SeqCst);
869 false
870 }),
871 );
872 let c2 = calls.clone();
873 reg.add_hook(
874 "short",
875 Box::new(move |_| {
876 c2.fetch_add(1, Ordering::SeqCst);
877 true
878 }),
879 );
880 assert!(!reg.run_hooks("short", &args));
881 assert_eq!(calls.load(Ordering::SeqCst), 1);
882 }
883
884 #[test]
888 fn hook_args_to_json_serializes_payload_fields() {
889 let json = hook_args_to_json(&HookArgs::DiagnosticsUpdated {
890 uri: "file:///x.rs".into(),
891 count: 7,
892 })
893 .unwrap();
894 assert_eq!(json["uri"], "file:///x.rs");
895 assert_eq!(json["count"], 7);
896 }
897}