1use core::str::FromStr;
7use std::path::Path;
8
9use color_eyre::eyre::WrapErr;
10use color_eyre::eyre::bail;
11use color_eyre::eyre::eyre;
12use ytil_wezterm::WeztermPane;
13
14pub enum Editor {
16 Hx,
18 Nvim,
20}
21
22impl Editor {
23 pub fn open_file_cmd(&self, file_to_open: &FileToOpen) -> String {
25 let path = file_to_open.path.as_str();
26 let line_nbr = file_to_open.line_nbr;
27 let column = file_to_open.column;
28
29 match self {
30 Self::Hx => format!("':o {path}:{line_nbr}'"),
31 Self::Nvim => format!(":e {path} | :call cursor({line_nbr}, {column})"),
32 }
33 }
34
35 pub const fn pane_titles(&self) -> &[&str] {
37 match self {
38 Self::Hx => &["hx"],
39 Self::Nvim => &["nvim", "nv"],
40 }
41 }
42}
43
44impl FromStr for Editor {
46 type Err = color_eyre::eyre::Error;
47
48 fn from_str(value: &str) -> Result<Self, Self::Err> {
49 match value {
50 "hx" => Ok(Self::Hx),
51 "nvim" | "nv" => Ok(Self::Nvim),
52 unknown => Err(eyre!("unknown editor | value={unknown}")),
53 }
54 }
55}
56
57#[derive(Debug, Eq, PartialEq)]
59pub struct FileToOpen {
60 pub column: i64,
62 pub line_nbr: i64,
64 pub path: String,
66}
67
68impl TryFrom<(&str, i64, &[WeztermPane])> for FileToOpen {
70 type Error = color_eyre::eyre::Error;
71
72 fn try_from((file_to_open, pane_id, panes): (&str, i64, &[WeztermPane])) -> Result<Self, Self::Error> {
73 if Path::new(file_to_open).is_absolute() {
74 return Self::from_str(file_to_open);
75 }
76
77 let mut source_pane_absolute_cwd = panes
78 .iter()
79 .find(|pane| pane.pane_id == pane_id)
80 .ok_or_else(|| eyre!("missing pane | pane_id={pane_id} panes={panes:#?}"))?
81 .absolute_cwd();
82
83 source_pane_absolute_cwd.push(file_to_open);
84
85 Self::from_str(
86 source_pane_absolute_cwd
87 .to_str()
88 .ok_or_else(|| eyre!("cannot get path str | path={source_pane_absolute_cwd:#?}"))?,
89 )
90 .wrap_err_with(|| eyre!("error parsing file to open | file_to_open={file_to_open} pane_id={pane_id}"))
91 }
92}
93
94impl FromStr for FileToOpen {
96 type Err = color_eyre::eyre::Error;
97
98 fn from_str(s: &str) -> Result<Self, Self::Err> {
99 let mut parts = s.split(':');
100 let path = parts.next().ok_or_else(|| eyre!("file path missing | str={s}"))?;
101 let line_nbr = parts
102 .next()
103 .map(str::parse::<i64>)
104 .transpose()
105 .wrap_err_with(|| eyre!("invalid line number | str={s:?}"))?
106 .unwrap_or_default();
107 let column = parts
108 .next()
109 .map(str::parse::<i64>)
110 .transpose()
111 .wrap_err_with(|| eyre!("invalid column number | str={s:?}"))?
112 .unwrap_or_default();
113 if !Path::new(path)
114 .try_exists()
115 .wrap_err_with(|| eyre!("error checking if file exists | path={path:?}"))?
116 {
117 bail!("file missing | path={path}")
118 }
119
120 Ok(Self {
121 path: path.into(),
122 line_nbr,
123 column,
124 })
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use std::path::PathBuf;
131
132 use ytil_wezterm::WeztermPane;
133 use ytil_wezterm::WeztermPaneSize;
134
135 use super::*;
136
137 #[test]
138 fn open_file_cmd_returns_the_expected_cmd_string() {
139 let file = FileToOpen {
140 path: "src/main.rs".into(),
141 line_nbr: 12,
142 column: 5,
143 };
144 assert_eq!(Editor::Hx.open_file_cmd(&file), "':o src/main.rs:12'");
145 assert_eq!(
146 Editor::Nvim.open_file_cmd(&file),
147 ":e src/main.rs | :call cursor(12, 5)"
148 );
149 }
150
151 #[test]
152 fn pane_titles_are_the_expected_ones() {
153 assert_eq!(Editor::Hx.pane_titles(), &["hx"]);
154 assert_eq!(Editor::Nvim.pane_titles(), &["nvim", "nv"]);
155 }
156
157 #[test]
158 fn editor_from_str_works_as_expected() {
159 assert2::let_assert!(Ok(Editor::Hx) = Editor::from_str("hx"));
160 assert2::let_assert!(Ok(Editor::Nvim) = Editor::from_str("nvim"));
161 assert2::let_assert!(Ok(Editor::Nvim) = Editor::from_str("nv"));
162 assert2::let_assert!(Err(err) = Editor::from_str("unknown"));
163 assert!(err.to_string().contains("unknown editor"));
164 }
165
166 #[test]
167 fn file_to_open_from_str_works_as_expected() {
168 let root_dir = std::env::current_dir().unwrap();
169 let dummy_path = root_dir.join("Cargo.toml").to_string_lossy().to_string();
171
172 assert2::let_assert!(Ok(f0) = FileToOpen::from_str(&dummy_path));
173 let expected = FileToOpen {
174 path: dummy_path.clone(),
175 line_nbr: 0,
176 column: 0,
177 };
178 assert_eq!(f0, expected);
179
180 assert2::let_assert!(Ok(f1) = FileToOpen::from_str(&format!("{dummy_path}:3")));
181 let expected = FileToOpen {
182 path: dummy_path.clone(),
183 line_nbr: 3,
184 column: 0,
185 };
186 assert_eq!(f1, expected);
187
188 assert2::let_assert!(Ok(f2) = FileToOpen::from_str(&format!("{dummy_path}:3:7")));
189 let expected = FileToOpen {
190 path: dummy_path,
191 line_nbr: 3,
192 column: 7,
193 };
194 assert_eq!(f2, expected);
195 }
196
197 #[test]
198 fn try_from_errors_when_pane_is_missing() {
199 let panes: Vec<WeztermPane> = vec![];
200 assert2::let_assert!(Err(err) = FileToOpen::try_from(("README.md", 999, panes.as_slice())));
201 assert!(err.to_string().contains("missing pane"));
202 }
203
204 #[test]
205 fn try_from_errors_when_relative_file_is_missing() {
206 let dir = std::env::current_dir().unwrap();
207 let panes = vec![pane_with(1, 1, &dir)];
208 assert2::let_assert!(
209 Err(err) = FileToOpen::try_from(("definitely_missing_12345__file.rs", 1, panes.as_slice()))
210 );
211 assert!(err.to_string().contains("error parsing file to open"));
212 }
213
214 #[test]
215 fn try_from_resolves_relative_existing_file() {
216 let dir = std::env::current_dir().unwrap();
217 let panes = vec![pane_with(7, 1, &dir)];
218 assert2::let_assert!(Ok(file) = FileToOpen::try_from(("Cargo.toml", 7, panes.as_slice())));
219 let expected = FileToOpen {
220 path: dir.join("Cargo.toml").to_string_lossy().to_string(),
221 line_nbr: 0,
222 column: 0,
223 };
224 assert_eq!(file, expected);
225 }
226
227 fn pane_with(pane_id: i64, tab_id: i64, cwd_fs: &std::path::Path) -> WeztermPane {
228 WeztermPane {
229 cursor_shape: "Block".into(),
230 cursor_visibility: "Visible".into(),
231 cursor_x: 0,
232 cursor_y: 0,
233 cwd: PathBuf::from(format!("file://host{}", cwd_fs.display())),
236 is_active: true,
237 is_zoomed: false,
238 left_col: 0,
239 pane_id,
240 size: WeztermPaneSize {
241 cols: 80,
242 dpi: 96,
243 pixel_height: 800,
244 pixel_width: 600,
245 rows: 24,
246 },
247 tab_id,
248 tab_title: "tab".into(),
249 title: "hx".into(),
250 top_row: 0,
251 tty_name: "tty".into(),
252 window_id: 1,
253 window_title: "win".into(),
254 workspace: "default".into(),
255 }
256 }
257}