Skip to main content

nvrim/plugins/
truster.rs

1//! Rust test runner helpers integrating with Nvim.
2//!
3//! Exposes a dictionary enabling cursor-aware test execution (`run_test`) by parsing the current buffer
4//! with Tree‑sitter to locate the nearest test function and spawning it inside a WezTerm pane.
5//! All Nvim API failures are reported via [`ytil_noxi::notify::error`].
6
7use std::path::Path;
8
9use nvim_oxi::Dictionary;
10use serde::Deserialize;
11use ytil_noxi::buffer::BufferExt;
12
13/// [`Dictionary`] of Rust tests utilities.
14pub fn dict() -> Dictionary {
15    dict! {
16        "run_test": fn_from!(run_test),
17    }
18}
19
20#[derive(Clone, Copy, Deserialize)]
21enum TargetTerminal {
22    WezTerm,
23    Nvim,
24}
25
26ytil_noxi::impl_nvim_deserializable!(TargetTerminal);
27
28fn run_test(target_terminal: TargetTerminal) -> Option<()> {
29    let file_path = ytil_noxi::buffer::get_absolute_path(Some(&nvim_oxi::api::get_current_buf()))?;
30
31    let test_name = ytil_noxi::tree_sitter::get_enclosing_fn_name_of_position(&file_path)?;
32
33    let test_runner = get_test_runner_for_path(&file_path)
34        .inspect_err(|err| {
35            ytil_noxi::notify::error(format!(
36                "error getting test runner | path={} error={err:#?}",
37                file_path.display()
38            ));
39        })
40        .ok()?;
41
42    match target_terminal {
43        TargetTerminal::WezTerm => run_test_in_wezterm(test_runner, &test_name),
44        TargetTerminal::Nvim => run_test_in_nvim_term(test_runner, &test_name),
45    }
46}
47
48fn run_test_in_wezterm(test_runner: &str, test_name: &str) -> Option<()> {
49    let cur_pane_id = ytil_wezterm::get_current_pane_id()
50        .inspect_err(|err| ytil_noxi::notify::error(format!("error getting current WezTerm pane id | error={err:#?}")))
51        .ok()?;
52
53    let wez_panes = ytil_wezterm::get_all_panes(&[])
54        .inspect_err(|err| {
55            ytil_noxi::notify::error(format!("error getting WezTerm panes | error={err:#?}"));
56        })
57        .ok()?;
58
59    let Some(cur_pane) = wez_panes.iter().find(|p| p.pane_id == cur_pane_id) else {
60        ytil_noxi::notify::error(format!(
61            "error WezTerm pane not found | pane_id={cur_pane_id:#?} panes={wez_panes:#?}"
62        ));
63        return None;
64    };
65
66    let Some(test_runner_pane) = wez_panes.iter().find(|p| p.is_sibling_terminal_pane_of(cur_pane)) else {
67        ytil_noxi::notify::error(format!(
68            "error finding sibling pane to run test | current_pane={cur_pane:#?} panes={wez_panes:#?} test={test_name}"
69        ));
70        return None;
71    };
72
73    let test_run_cmd = format!("'{test_runner} {test_name}'");
74
75    let send_text_to_pane_cmd = ytil_wezterm::send_text_to_pane_cmd(&test_run_cmd, test_runner_pane.pane_id);
76    let submit_pane_cmd = ytil_wezterm::submit_pane_cmd(test_runner_pane.pane_id);
77
78    ytil_cmd::silent_cmd("sh")
79        .args(["-c", &format!("{send_text_to_pane_cmd} && {submit_pane_cmd}")])
80        .spawn()
81        .inspect_err(|err| {
82            ytil_noxi::notify::error(format!(
83                "error executing test run cmd | cmd={test_run_cmd:#?} pane={test_runner_pane:#?} error={err:#?}"
84            ));
85        })
86        .ok()?;
87
88    Some(())
89}
90
91fn run_test_in_nvim_term(test_runner: &str, test_name: &str) -> Option<()> {
92    let Some(terminal_buffer) = nvim_oxi::api::list_bufs().find(BufferExt::is_terminal) else {
93        ytil_noxi::notify::error(format!(
94            "error no terminal buffer found | test_runner={test_runner:?} test_name={test_name:?}",
95        ));
96        return None;
97    };
98
99    terminal_buffer.send_command(&format!("{test_runner} {test_name}\n"));
100
101    Some(())
102}
103
104/// Get the application to use to run the tests based on the presence of a `Makefile.toml`
105/// in the root of a git repository where the supplied [Path] resides.
106///
107/// If the file is found "cargo make test" is used to run the tests.
108/// "cargo test" is used otherwise.
109///
110/// Assumptions:
111/// 1. We're always working in a git repository
112/// 2. no custom config file for cargo-make
113///
114/// # Errors
115/// - A filesystem operation (open/read/write/remove) fails.
116/// - The path is not inside a Git repository.
117fn get_test_runner_for_path(path: &Path) -> rootcause::Result<&'static str> {
118    let git_repo_root = ytil_git::repo::get_root(&ytil_git::repo::discover(path)?);
119
120    if git_repo_root.join("Makefile.toml").exists() {
121        return Ok("cargo make test");
122    }
123
124    Ok("cargo test")
125}