ytil_hx/
lib.rs

1//! Parse Helix (hx) status line output into structured types: [`HxStatusLine`] and [`HxCursorPosition`].
2
3use core::str::FromStr;
4use std::path::PathBuf;
5
6use color_eyre::eyre;
7use color_eyre::eyre::WrapErr;
8use color_eyre::eyre::bail;
9use color_eyre::eyre::eyre;
10
11/// Represents the parsed status line from Helix editor, containing filepath and cursor position.
12#[derive(Debug, Eq, PartialEq)]
13#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
14pub struct HxStatusLine {
15    /// The filepath currently open in the editor.
16    pub file_path: PathBuf,
17    /// The current cursor position in the file.
18    pub position: HxCursorPosition,
19}
20
21/// Parses a [`HxStatusLine`] from a Helix editor status line string.
22impl FromStr for HxStatusLine {
23    type Err = eyre::Error;
24
25    fn from_str(hx_status_line: &str) -> Result<Self, Self::Err> {
26        let hx_status_line = hx_status_line.trim();
27
28        let elements: Vec<&str> = hx_status_line.split_ascii_whitespace().collect();
29
30        let path_left_separator_idx = elements
31            .iter()
32            .position(|x| x == &"`")
33            .ok_or_else(|| eyre!("error missing left path separator | elements={elements:#?}"))?;
34        let path_right_separator_idx = elements
35            .iter()
36            .rposition(|x| x == &"`")
37            .ok_or_else(|| eyre!("error missing right path separator | elements={elements:#?}"))?;
38
39        let path_slice_range = path_left_separator_idx..path_right_separator_idx;
40        let path_slice = elements
41            .get(path_slice_range.clone())
42            .ok_or_else(|| eyre!("error invalid path slice indices | range={path_slice_range:#?}"))?;
43        let ["`", path] = path_slice else {
44            bail!("missing path | elements={elements:#?}");
45        };
46
47        Ok(Self {
48            file_path: path.into(),
49            position: HxCursorPosition::from_str(
50                elements
51                    .last()
52                    .ok_or_else(|| eyre!("error missing last element | elements={elements:#?}"))?,
53            )?,
54        })
55    }
56}
57
58/// Represents a cursor position in a text file with line and column coordinates.
59#[derive(Debug, Eq, PartialEq)]
60#[cfg_attr(any(test, feature = "fake"), derive(fake::Dummy))]
61pub struct HxCursorPosition {
62    /// The column number (1-based).
63    pub column: usize,
64    /// The line number (1-based).
65    pub line: usize,
66}
67
68/// Parses a [`HxCursorPosition`] from a string in the format "line:column".
69impl FromStr for HxCursorPosition {
70    type Err = eyre::Error;
71
72    fn from_str(s: &str) -> Result<Self, Self::Err> {
73        let (line, column) = s
74            .split_once(':')
75            .ok_or_else(|| eyre!("error missing line column delimiter | input={s}"))?;
76
77        Ok(Self {
78            line: line
79                .parse()
80                .wrap_err_with(|| eyre!("invalid line number | input={s:?}"))?,
81            column: column
82                .parse()
83                .wrap_err_with(|| eyre!("invalid column number | input={s:?}"))?,
84        })
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use super::*;
91
92    #[test]
93    fn hx_cursor_from_str_works_as_expected_with_a_file_path_pointing_to_an_existent_file_in_normal_mode() {
94        let result = HxStatusLine::from_str(
95            "      ● 1 ` src/utils.rs `                                                                  1 sel  1 char  W ● 1  42:33 ",
96        );
97        let expected = HxStatusLine {
98            file_path: "src/utils.rs".into(),
99            position: HxCursorPosition { line: 42, column: 33 },
100        };
101
102        assert_eq!(result.unwrap(), expected);
103    }
104
105    #[test]
106    fn hx_cursor_from_str_works_as_expected_with_a_file_path_pointing_to_an_existent_file_and_a_spinner() {
107        let result = HxStatusLine::from_str(
108            "⣷      ` src/utils.rs `                                                                  1 sel  1 char  W ● 1  33:42 ",
109        );
110        let expected = HxStatusLine {
111            file_path: "src/utils.rs".into(),
112            position: HxCursorPosition { line: 33, column: 42 },
113        };
114
115        assert_eq!(result.unwrap(), expected);
116    }
117}