1use core::str::FromStr;
7use std::path::Path;
8
9use rootcause::prelude::ResultExt;
10use rootcause::report;
11use ytil_wezterm::WeztermPane;
12
13pub enum Editor {
15 Hx,
17 Nvim,
19}
20
21impl Editor {
22 pub fn open_file_cmd(&self, file_to_open: &FileToOpen) -> String {
24 let path = file_to_open.path.as_str();
25 let line_nbr = file_to_open.line_nbr;
26 let column = file_to_open.column;
27
28 match self {
29 Self::Hx => format!("':o {path}:{line_nbr}'"),
30 Self::Nvim => format!(":e {path} | :call cursor({line_nbr}, {column})"),
31 }
32 }
33
34 pub const fn pane_titles(&self) -> &[&str] {
36 match self {
37 Self::Hx => &["hx"],
38 Self::Nvim => &["nvim", "nv"],
39 }
40 }
41}
42
43impl FromStr for Editor {
45 type Err = rootcause::Report;
46
47 fn from_str(value: &str) -> Result<Self, Self::Err> {
48 match value {
49 "hx" => Ok(Self::Hx),
50 "nvim" | "nv" => Ok(Self::Nvim),
51 unknown => Err(report!("unknown editor").attach(format!("value={unknown}"))),
52 }
53 }
54}
55
56#[derive(Debug, Eq, PartialEq)]
58pub struct FileToOpen {
59 pub column: i64,
61 pub line_nbr: i64,
63 pub path: String,
65}
66
67impl TryFrom<(&str, i64, &[WeztermPane])> for FileToOpen {
69 type Error = rootcause::Report;
70
71 fn try_from((file_to_open, pane_id, panes): (&str, i64, &[WeztermPane])) -> Result<Self, Self::Error> {
72 if Path::new(file_to_open).is_absolute() {
73 return Self::from_str(file_to_open);
74 }
75
76 let mut source_pane_absolute_cwd = panes
77 .iter()
78 .find(|pane| pane.pane_id == pane_id)
79 .ok_or_else(|| report!("missing pane"))
80 .attach_with(|| format!("pane_id={pane_id} panes={panes:#?}"))?
81 .absolute_cwd();
82
83 source_pane_absolute_cwd.push(file_to_open);
84
85 Ok(Self::from_str(
86 source_pane_absolute_cwd
87 .to_str()
88 .ok_or_else(|| report!("cannot get path str"))
89 .attach_with(|| format!("path={}", source_pane_absolute_cwd.display()))?,
90 )
91 .context("error parsing file to open")
92 .attach_with(|| format!("file_to_open={file_to_open} pane_id={pane_id}"))?)
93 }
94}
95
96impl FromStr for FileToOpen {
98 type Err = rootcause::Report;
99
100 fn from_str(s: &str) -> Result<Self, Self::Err> {
101 let mut parts = s.split(':');
102 let path = parts
103 .next()
104 .ok_or_else(|| report!("file path missing"))
105 .attach_with(|| format!("str={s}"))?;
106 let line_nbr = parts
107 .next()
108 .map(str::parse::<i64>)
109 .transpose()
110 .context("invalid line number")
111 .attach_with(|| format!("str={s:?}"))?
112 .unwrap_or_default();
113 let column = parts
114 .next()
115 .map(str::parse::<i64>)
116 .transpose()
117 .context("invalid column number")
118 .attach_with(|| format!("str={s:?}"))?
119 .unwrap_or_default();
120 if !Path::new(path)
121 .try_exists()
122 .context("error checking if file exists")
123 .attach_with(|| format!("path={path:?}"))?
124 {
125 Err(report!("file missing")).attach_with(|| format!("path={path}"))?;
126 }
127
128 Ok(Self {
129 path: path.into(),
130 line_nbr,
131 column,
132 })
133 }
134}
135
136#[cfg(test)]
137mod tests {
138 use std::path::PathBuf;
139
140 use test_that::prelude::*;
141 use ytil_wezterm::WeztermPane;
142 use ytil_wezterm::WeztermPaneSize;
143
144 use super::*;
145
146 #[test]
147 fn test_open_file_cmd_returns_the_expected_cmd_string() {
148 let file = FileToOpen {
149 path: "src/main.rs".into(),
150 line_nbr: 12,
151 column: 5,
152 };
153 assert_eq!(Editor::Hx.open_file_cmd(&file), "':o src/main.rs:12'");
154 assert_eq!(
155 Editor::Nvim.open_file_cmd(&file),
156 ":e src/main.rs | :call cursor(12, 5)"
157 );
158 }
159
160 #[test]
161 fn test_pane_titles_are_the_expected_ones() {
162 assert_eq!(Editor::Hx.pane_titles(), &["hx"]);
163 assert_eq!(Editor::Nvim.pane_titles(), &["nvim", "nv"]);
164 }
165
166 #[test]
167 fn test_editor_from_str_when_input_varies_returns_expected_editor() {
168 assert_that!(
169 Editor::from_str("hx").map(|editor| editor
170 .pane_titles()
171 .iter()
172 .map(ToString::to_string)
173 .collect::<Vec<_>>()),
174 ok(eq(vec!["hx"]))
175 );
176 assert_that!(
177 Editor::from_str("nvim").map(|editor| editor
178 .pane_titles()
179 .iter()
180 .map(ToString::to_string)
181 .collect::<Vec<_>>()),
182 ok(eq(vec!["nvim", "nv"]))
183 );
184 assert_that!(
185 Editor::from_str("nv").map(|editor| editor
186 .pane_titles()
187 .iter()
188 .map(ToString::to_string)
189 .collect::<Vec<_>>()),
190 ok(eq(vec!["nvim", "nv"]))
191 );
192 assert_that!(
193 Editor::from_str("unknown")
194 .map(|editor| editor.pane_titles().iter().map(ToString::to_string).collect::<Vec<_>>())
195 .map_err(|err| err.to_string()),
196 err(contains_substring("unknown editor"))
197 );
198 }
199
200 #[test]
201 fn test_file_to_open_from_str_when_input_varies_returns_expected_file() {
202 let root_dir = std::env::current_dir().unwrap();
203 let dummy_path = root_dir.join("Cargo.toml").to_string_lossy().into_owned();
205
206 let expected = FileToOpen {
207 path: dummy_path.clone(),
208 line_nbr: 0,
209 column: 0,
210 };
211 assert_that!(FileToOpen::from_str(&dummy_path), ok(eq(expected)));
212
213 let expected = FileToOpen {
214 path: dummy_path.clone(),
215 line_nbr: 3,
216 column: 0,
217 };
218 assert_that!(FileToOpen::from_str(&format!("{dummy_path}:3")), ok(eq(expected)));
219
220 let input = format!("{dummy_path}:3:7");
221 let expected = FileToOpen {
222 path: dummy_path,
223 line_nbr: 3,
224 column: 7,
225 };
226 assert_that!(FileToOpen::from_str(&input), ok(eq(expected)));
227 }
228
229 #[test]
230 fn test_try_from_errors_when_pane_is_missing() {
231 let panes: Vec<WeztermPane> = vec![];
232 assert_that!(
233 (FileToOpen::try_from(("README.md", 999, panes.as_slice()))).map(|_| ()),
234 err(displays_as(contains_substring("missing pane")))
235 );
236 }
237
238 #[test]
239 fn test_try_from_errors_when_relative_file_is_missing() {
240 let dir = std::env::current_dir().unwrap();
241 let panes = vec![pane_with(1, 1, &dir)];
242 assert_that!(
243 (FileToOpen::try_from(("definitely_missing_12345__file.rs", 1, panes.as_slice()))).map(|_| ()),
244 err(displays_as(contains_substring("error parsing file to open")))
245 );
246 }
247
248 #[test]
249 fn test_try_from_resolves_relative_existing_file() {
250 let dir = std::env::current_dir().unwrap();
251 let panes = vec![pane_with(7, 1, &dir)];
252 let expected = FileToOpen {
253 path: dir.join("Cargo.toml").to_string_lossy().into_owned(),
254 line_nbr: 0,
255 column: 0,
256 };
257 assert_that!(
258 FileToOpen::try_from(("Cargo.toml", 7, panes.as_slice())),
259 ok(eq(expected))
260 );
261 }
262
263 fn pane_with(pane_id: i64, tab_id: i64, cwd_fs: &std::path::Path) -> WeztermPane {
264 WeztermPane {
265 cursor_shape: "Block".into(),
266 cursor_visibility: "Visible".into(),
267 cursor_x: 0,
268 cursor_y: 0,
269 cwd: PathBuf::from(format!("file://host{}", cwd_fs.display())),
272 is_active: true,
273 is_zoomed: false,
274 left_col: 0,
275 pane_id,
276 size: WeztermPaneSize {
277 cols: 80,
278 dpi: 96,
279 pixel_height: 800,
280 pixel_width: 600,
281 rows: 24,
282 },
283 tab_id,
284 tab_title: "tab".into(),
285 title: "hx".into(),
286 top_row: 0,
287 tty_name: "tty".into(),
288 window_id: 1,
289 window_title: "win".into(),
290 workspace: "default".into(),
291 }
292 }
293}