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 },
88
89 BufferActivated { buffer_id: BufferId },
91
92 BufferDeactivated { buffer_id: BufferId },
94
95 DiagnosticsUpdated {
97 uri: String,
99 count: usize,
101 },
102
103 PreCommand { action: Action },
105
106 PostCommand { action: Action },
108
109 Idle { milliseconds: u64 },
111
112 EditorInitialized,
114
115 RenderStart { buffer_id: BufferId },
117
118 RenderLine {
120 buffer_id: BufferId,
121 line_number: usize,
122 byte_start: usize,
123 byte_end: usize,
124 content: String,
125 },
126
127 LinesChanged {
129 buffer_id: BufferId,
130 lines: Vec<LineInfo>,
131 },
132
133 PromptChanged { prompt_type: String, input: String },
135
136 PromptConfirmed {
138 prompt_type: String,
139 input: String,
140 selected_index: Option<usize>,
141 },
142
143 PromptCancelled { prompt_type: String, input: String },
145
146 PromptSelectionChanged {
148 prompt_type: String,
149 selected_index: usize,
150 },
151
152 KeyboardShortcuts { bindings: Vec<(String, String)> },
154
155 LspReferences {
157 symbol: String,
159 locations: Vec<LspLocation>,
161 },
162
163 ViewTransformRequest {
165 buffer_id: BufferId,
166 split_id: SplitId,
167 viewport_start: usize,
169 viewport_end: usize,
171 tokens: Vec<ViewTokenWire>,
173 },
174
175 MouseClick {
177 column: u16,
179 row: u16,
181 button: String,
183 modifiers: String,
185 content_x: u16,
187 content_y: u16,
189 },
190
191 MouseMove {
193 column: u16,
195 row: u16,
197 content_x: u16,
199 content_y: u16,
201 },
202
203 LspServerRequest {
205 language: String,
207 method: String,
209 server_command: String,
211 params: Option<String>,
213 },
214
215 ViewportChanged {
217 split_id: SplitId,
218 buffer_id: BufferId,
219 top_byte: usize,
220 width: u16,
221 height: u16,
222 },
223
224 LspServerError {
226 language: String,
228 server_command: String,
230 error_type: String,
232 message: String,
234 },
235
236 LspStatusClicked {
238 language: String,
240 has_error: bool,
242 },
243
244 ActionPopupResult {
246 popup_id: String,
248 action_id: String,
250 },
251
252 ProcessOutput {
254 process_id: u64,
256 data: String,
258 },
259}
260
261#[derive(Debug, Clone, serde::Serialize)]
263pub struct LineInfo {
264 pub line_number: usize,
266 pub byte_start: usize,
268 pub byte_end: usize,
270 pub content: String,
272}
273
274#[derive(Debug, Clone, serde::Serialize)]
276pub struct LspLocation {
277 pub file: String,
279 pub line: u32,
281 pub column: u32,
283}
284
285pub type HookCallback = Box<dyn Fn(&HookArgs) -> bool + Send + Sync>;
287
288pub struct HookRegistry {
290 hooks: HashMap<String, Vec<HookCallback>>,
292}
293
294impl HookRegistry {
295 pub fn new() -> Self {
297 Self {
298 hooks: HashMap::new(),
299 }
300 }
301
302 pub fn add_hook(&mut self, name: &str, callback: HookCallback) {
304 self.hooks
305 .entry(name.to_string())
306 .or_default()
307 .push(callback);
308 }
309
310 pub fn remove_hooks(&mut self, name: &str) {
312 self.hooks.remove(name);
313 }
314
315 pub fn run_hooks(&self, name: &str, args: &HookArgs) -> bool {
317 if let Some(hooks) = self.hooks.get(name) {
318 for callback in hooks {
319 if !callback(args) {
320 return false;
321 }
322 }
323 }
324 true
325 }
326
327 pub fn hook_count(&self, name: &str) -> usize {
329 self.hooks.get(name).map(|v| v.len()).unwrap_or(0)
330 }
331
332 pub fn hook_names(&self) -> Vec<String> {
334 self.hooks.keys().cloned().collect()
335 }
336}
337
338impl Default for HookRegistry {
339 fn default() -> Self {
340 Self::new()
341 }
342}
343
344pub fn hook_args_to_json(args: &HookArgs) -> Result<String> {
346 let json_value = match args {
347 HookArgs::RenderStart { buffer_id } => {
348 serde_json::json!({
349 "buffer_id": buffer_id.0,
350 })
351 }
352 HookArgs::RenderLine {
353 buffer_id,
354 line_number,
355 byte_start,
356 byte_end,
357 content,
358 } => {
359 serde_json::json!({
360 "buffer_id": buffer_id.0,
361 "line_number": line_number,
362 "byte_start": byte_start,
363 "byte_end": byte_end,
364 "content": content,
365 })
366 }
367 HookArgs::BufferActivated { buffer_id } => {
368 serde_json::json!({ "buffer_id": buffer_id.0 })
369 }
370 HookArgs::BufferDeactivated { buffer_id } => {
371 serde_json::json!({ "buffer_id": buffer_id.0 })
372 }
373 HookArgs::DiagnosticsUpdated { uri, count } => {
374 serde_json::json!({
375 "uri": uri,
376 "count": count,
377 })
378 }
379 HookArgs::BufferClosed { buffer_id } => {
380 serde_json::json!({ "buffer_id": buffer_id.0 })
381 }
382 HookArgs::CursorMoved {
383 buffer_id,
384 cursor_id,
385 old_position,
386 new_position,
387 line,
388 } => {
389 serde_json::json!({
390 "buffer_id": buffer_id.0,
391 "cursor_id": cursor_id.0,
392 "old_position": old_position,
393 "new_position": new_position,
394 "line": line,
395 })
396 }
397 HookArgs::BeforeInsert {
398 buffer_id,
399 position,
400 text,
401 } => {
402 serde_json::json!({
403 "buffer_id": buffer_id.0,
404 "position": position,
405 "text": text,
406 })
407 }
408 HookArgs::AfterInsert {
409 buffer_id,
410 position,
411 text,
412 affected_start,
413 affected_end,
414 start_line,
415 end_line,
416 lines_added,
417 } => {
418 serde_json::json!({
419 "buffer_id": buffer_id.0,
420 "position": position,
421 "text": text,
422 "affected_start": affected_start,
423 "affected_end": affected_end,
424 "start_line": start_line,
425 "end_line": end_line,
426 "lines_added": lines_added,
427 })
428 }
429 HookArgs::BeforeDelete { buffer_id, range } => {
430 serde_json::json!({
431 "buffer_id": buffer_id.0,
432 "start": range.start,
433 "end": range.end,
434 })
435 }
436 HookArgs::AfterDelete {
437 buffer_id,
438 range,
439 deleted_text,
440 affected_start,
441 deleted_len,
442 start_line,
443 end_line,
444 lines_removed,
445 } => {
446 serde_json::json!({
447 "buffer_id": buffer_id.0,
448 "start": range.start,
449 "end": range.end,
450 "deleted_text": deleted_text,
451 "affected_start": affected_start,
452 "deleted_len": deleted_len,
453 "start_line": start_line,
454 "end_line": end_line,
455 "lines_removed": lines_removed,
456 })
457 }
458 HookArgs::BeforeFileOpen { path } => {
459 serde_json::json!({ "path": path.to_string_lossy() })
460 }
461 HookArgs::AfterFileOpen { path, buffer_id } => {
462 serde_json::json!({
463 "path": path.to_string_lossy(),
464 "buffer_id": buffer_id.0,
465 })
466 }
467 HookArgs::BeforeFileSave { path, buffer_id } => {
468 serde_json::json!({
469 "path": path.to_string_lossy(),
470 "buffer_id": buffer_id.0,
471 })
472 }
473 HookArgs::AfterFileSave { path, buffer_id } => {
474 serde_json::json!({
475 "path": path.to_string_lossy(),
476 "buffer_id": buffer_id.0,
477 })
478 }
479 HookArgs::PreCommand { action } => {
480 serde_json::json!({ "action": format!("{:?}", action) })
481 }
482 HookArgs::PostCommand { action } => {
483 serde_json::json!({ "action": format!("{:?}", action) })
484 }
485 HookArgs::Idle { milliseconds } => {
486 serde_json::json!({ "milliseconds": milliseconds })
487 }
488 HookArgs::EditorInitialized => {
489 serde_json::json!({})
490 }
491 HookArgs::PromptChanged { prompt_type, input } => {
492 serde_json::json!({
493 "prompt_type": prompt_type,
494 "input": input,
495 })
496 }
497 HookArgs::PromptConfirmed {
498 prompt_type,
499 input,
500 selected_index,
501 } => {
502 serde_json::json!({
503 "prompt_type": prompt_type,
504 "input": input,
505 "selected_index": selected_index,
506 })
507 }
508 HookArgs::PromptCancelled { prompt_type, input } => {
509 serde_json::json!({
510 "prompt_type": prompt_type,
511 "input": input,
512 })
513 }
514 HookArgs::PromptSelectionChanged {
515 prompt_type,
516 selected_index,
517 } => {
518 serde_json::json!({
519 "prompt_type": prompt_type,
520 "selected_index": selected_index,
521 })
522 }
523 HookArgs::KeyboardShortcuts { bindings } => {
524 let entries: Vec<serde_json::Value> = bindings
525 .iter()
526 .map(|(key, action)| serde_json::json!({ "key": key, "action": action }))
527 .collect();
528 serde_json::json!({ "bindings": entries })
529 }
530 HookArgs::LspReferences { symbol, locations } => {
531 let locs: Vec<serde_json::Value> = locations
532 .iter()
533 .map(|loc| {
534 serde_json::json!({
535 "file": loc.file,
536 "line": loc.line,
537 "column": loc.column,
538 })
539 })
540 .collect();
541 serde_json::json!({ "symbol": symbol, "locations": locs })
542 }
543 HookArgs::LinesChanged { buffer_id, lines } => {
544 let lines_json: Vec<serde_json::Value> = lines
545 .iter()
546 .map(|line| {
547 serde_json::json!({
548 "line_number": line.line_number,
549 "byte_start": line.byte_start,
550 "byte_end": line.byte_end,
551 "content": line.content,
552 })
553 })
554 .collect();
555 serde_json::json!({
556 "buffer_id": buffer_id.0,
557 "lines": lines_json,
558 })
559 }
560 HookArgs::ViewTransformRequest {
561 buffer_id,
562 split_id,
563 viewport_start,
564 viewport_end,
565 tokens,
566 } => {
567 let tokens_json: Vec<serde_json::Value> = tokens
568 .iter()
569 .map(|token| {
570 let kind_json = match &token.kind {
571 ViewTokenWireKind::Text(s) => serde_json::json!({ "Text": s }),
572 ViewTokenWireKind::Newline => serde_json::json!("Newline"),
573 ViewTokenWireKind::Space => serde_json::json!("Space"),
574 ViewTokenWireKind::Break => serde_json::json!("Break"),
575 ViewTokenWireKind::BinaryByte(b) => serde_json::json!({ "BinaryByte": b }),
576 };
577 serde_json::json!({
578 "source_offset": token.source_offset,
579 "kind": kind_json,
580 })
581 })
582 .collect();
583 serde_json::json!({
584 "buffer_id": buffer_id.0,
585 "split_id": split_id.0,
586 "viewport_start": viewport_start,
587 "viewport_end": viewport_end,
588 "tokens": tokens_json,
589 })
590 }
591 HookArgs::MouseClick {
592 column,
593 row,
594 button,
595 modifiers,
596 content_x,
597 content_y,
598 } => {
599 serde_json::json!({
600 "column": column,
601 "row": row,
602 "button": button,
603 "modifiers": modifiers,
604 "content_x": content_x,
605 "content_y": content_y,
606 })
607 }
608 HookArgs::MouseMove {
609 column,
610 row,
611 content_x,
612 content_y,
613 } => {
614 serde_json::json!({
615 "column": column,
616 "row": row,
617 "content_x": content_x,
618 "content_y": content_y,
619 })
620 }
621 HookArgs::LspServerRequest {
622 language,
623 method,
624 server_command,
625 params,
626 } => {
627 serde_json::json!({
628 "language": language,
629 "method": method,
630 "server_command": server_command,
631 "params": params,
632 })
633 }
634 HookArgs::ViewportChanged {
635 split_id,
636 buffer_id,
637 top_byte,
638 width,
639 height,
640 } => {
641 serde_json::json!({
642 "split_id": split_id.0,
643 "buffer_id": buffer_id.0,
644 "top_byte": top_byte,
645 "width": width,
646 "height": height,
647 })
648 }
649 HookArgs::LspServerError {
650 language,
651 server_command,
652 error_type,
653 message,
654 } => {
655 serde_json::json!({
656 "language": language,
657 "server_command": server_command,
658 "error_type": error_type,
659 "message": message,
660 })
661 }
662 HookArgs::LspStatusClicked {
663 language,
664 has_error,
665 } => {
666 serde_json::json!({
667 "language": language,
668 "has_error": has_error,
669 })
670 }
671 HookArgs::ActionPopupResult {
672 popup_id,
673 action_id,
674 } => {
675 serde_json::json!({
676 "popup_id": popup_id,
677 "action_id": action_id,
678 })
679 }
680 HookArgs::ProcessOutput { process_id, data } => {
681 serde_json::json!({
682 "process_id": process_id,
683 "data": data,
684 })
685 }
686 };
687
688 serde_json::to_string(&json_value)
689 .map_err(|e| anyhow::anyhow!("Failed to serialize hook args: {}", e))
690}