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 nvim_oxi::Object;
11use nvim_oxi::conversion::FromObject;
12use nvim_oxi::lua::Poppable;
13use nvim_oxi::lua::ffi::State;
14use nvim_oxi::serde::Deserializer;
15use serde::Deserialize;
16use ytil_noxi::buffer::BufferExt;
17
18/// [`Dictionary`] of Rust tests utilities.
19pub fn dict() -> Dictionary {
20    dict! {
21        "run_test": fn_from!(run_test),
22    }
23}
24
25#[derive(Clone, Copy, Deserialize)]
26enum TargetTerminal {
27    WezTerm,
28    Nvim,
29}
30
31impl FromObject for TargetTerminal {
32    fn from_object(obj: Object) -> Result<Self, nvim_oxi::conversion::Error> {
33        Self::deserialize(Deserializer::new(obj)).map_err(Into::into)
34    }
35}
36
37impl Poppable for TargetTerminal {
38    unsafe fn pop(lstate: *mut State) -> Result<Self, nvim_oxi::lua::Error> {
39        unsafe {
40            let obj = Object::pop(lstate)?;
41            Self::from_object(obj).map_err(nvim_oxi::lua::Error::pop_error_from_err::<Self, _>)
42        }
43    }
44}
45
46fn run_test(target_terminal: TargetTerminal) -> Option<()> {
47    let file_path = ytil_noxi::buffer::get_absolute_path(Some(&nvim_oxi::api::get_current_buf()))?;
48
49    let test_name = ytil_noxi::tree_sitter::get_enclosing_fn_name_of_position(&file_path)?;
50
51    let test_runner = get_test_runner_for_path(&file_path)
52        .inspect_err(|err| {
53            ytil_noxi::notify::error(format!(
54                "error getting test runner | path={} error={err:#?}",
55                file_path.display()
56            ));
57        })
58        .ok()?;
59
60    match target_terminal {
61        TargetTerminal::WezTerm => run_test_in_wezterm(test_runner, &test_name),
62        TargetTerminal::Nvim => run_test_in_nvim_term(test_runner, &test_name),
63    }
64}
65
66fn run_test_in_wezterm(test_runner: &str, test_name: &str) -> Option<()> {
67    let cur_pane_id = ytil_wezterm::get_current_pane_id()
68        .inspect_err(|err| ytil_noxi::notify::error(format!("error getting current WezTerm pane id | error={err:#?}")))
69        .ok()?;
70
71    let wez_panes = ytil_wezterm::get_all_panes(&[])
72        .inspect_err(|err| {
73            ytil_noxi::notify::error(format!("error getting WezTerm panes | error={err:#?}"));
74        })
75        .ok()?;
76
77    let Some(cur_pane) = wez_panes.iter().find(|p| p.pane_id == cur_pane_id) else {
78        ytil_noxi::notify::error(format!(
79            "error WezTerm pane not found | pane_id={cur_pane_id:#?} panes={wez_panes:#?}"
80        ));
81        return None;
82    };
83
84    let Some(test_runner_pane) = wez_panes.iter().find(|p| p.is_sibling_terminal_pane_of(cur_pane)) else {
85        ytil_noxi::notify::error(format!(
86            "error finding sibling pane to run test | current_pane={cur_pane:#?} panes={wez_panes:#?} test={test_name}"
87        ));
88        return None;
89    };
90
91    let test_run_cmd = format!("'{test_runner} {test_name}'");
92
93    let send_text_to_pane_cmd = ytil_wezterm::send_text_to_pane_cmd(&test_run_cmd, test_runner_pane.pane_id);
94    let submit_pane_cmd = ytil_wezterm::submit_pane_cmd(test_runner_pane.pane_id);
95
96    ytil_cmd::silent_cmd("sh")
97        .args(["-c", &format!("{send_text_to_pane_cmd} && {submit_pane_cmd}")])
98        .spawn()
99        .inspect_err(|err| {
100            ytil_noxi::notify::error(format!(
101                "error executing test run cmd | cmd={test_run_cmd:#?} pane={test_runner_pane:#?} error={err:#?}"
102            ));
103        })
104        .ok()?;
105
106    Some(())
107}
108
109fn run_test_in_nvim_term(test_runner: &str, test_name: &str) -> Option<()> {
110    let Some(terminal_buffer) = nvim_oxi::api::list_bufs().find(BufferExt::is_terminal) else {
111        ytil_noxi::notify::error(format!(
112            "error no terminal buffer found | test_runner={test_runner:?} test_name={test_name:?}",
113        ));
114        return None;
115    };
116
117    terminal_buffer.send_command(&format!("{test_runner} {test_name}\n"));
118
119    Some(())
120}
121
122/// Get the application to use to run the tests based on the presence of a `Makefile.toml`
123/// in the root of a git repository where the supplied [Path] resides.
124///
125/// If the file is found "cargo make test" is used to run the tests.
126/// "cargo test" is used otherwise.
127///
128/// Assumptions:
129/// 1. We're always working in a git repository
130/// 2. no custom config file for cargo-make
131///
132/// # Errors
133/// - A filesystem operation (open/read/write/remove) fails.
134/// - The path is not inside a Git repository.
135fn get_test_runner_for_path(path: &Path) -> color_eyre::Result<&'static str> {
136    let git_repo_root = ytil_git::repo::get_root(&ytil_git::repo::discover(path)?);
137
138    if std::fs::read_dir(git_repo_root)?.any(|res| {
139        res.as_ref()
140            .map(|de| de.file_name() == "Makefile.toml")
141            .unwrap_or(false)
142    }) {
143        return Ok("cargo make test");
144    }
145
146    Ok("cargo test")
147}