Skip to main content

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