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 ytil_wezterm::WeztermPane;
141 use ytil_wezterm::WeztermPaneSize;
142
143 use super::*;
144
145 #[test]
146 fn open_file_cmd_returns_the_expected_cmd_string() {
147 let file = FileToOpen {
148 path: "src/main.rs".into(),
149 line_nbr: 12,
150 column: 5,
151 };
152 assert_eq!(Editor::Hx.open_file_cmd(&file), "':o src/main.rs:12'");
153 assert_eq!(
154 Editor::Nvim.open_file_cmd(&file),
155 ":e src/main.rs | :call cursor(12, 5)"
156 );
157 }
158
159 #[test]
160 fn pane_titles_are_the_expected_ones() {
161 assert_eq!(Editor::Hx.pane_titles(), &["hx"]);
162 assert_eq!(Editor::Nvim.pane_titles(), &["nvim", "nv"]);
163 }
164
165 #[test]
166 fn editor_from_str_works_as_expected() {
167 assert2::assert!(let Ok(Editor::Hx) = Editor::from_str("hx"));
168 assert2::assert!(let Ok(Editor::Nvim) = Editor::from_str("nvim"));
169 assert2::assert!(let Ok(Editor::Nvim) = Editor::from_str("nv"));
170 assert2::assert!(let Err(err) = Editor::from_str("unknown"));
171 assert!(err.to_string().contains("unknown editor"));
172 }
173
174 #[test]
175 fn file_to_open_from_str_works_as_expected() {
176 let root_dir = std::env::current_dir().unwrap();
177 let dummy_path = root_dir.join("Cargo.toml").to_string_lossy().into_owned();
179
180 assert2::assert!(let Ok(f0) = FileToOpen::from_str(&dummy_path));
181 let expected = FileToOpen {
182 path: dummy_path.clone(),
183 line_nbr: 0,
184 column: 0,
185 };
186 assert_eq!(f0, expected);
187
188 assert2::assert!(let Ok(f1) = FileToOpen::from_str(&format!("{dummy_path}:3")));
189 let expected = FileToOpen {
190 path: dummy_path.clone(),
191 line_nbr: 3,
192 column: 0,
193 };
194 assert_eq!(f1, expected);
195
196 assert2::assert!(let Ok(f2) = FileToOpen::from_str(&format!("{dummy_path}:3:7")));
197 let expected = FileToOpen {
198 path: dummy_path,
199 line_nbr: 3,
200 column: 7,
201 };
202 assert_eq!(f2, expected);
203 }
204
205 #[test]
206 fn try_from_errors_when_pane_is_missing() {
207 let panes: Vec<WeztermPane> = vec![];
208 assert2::assert!(let Err(err) = FileToOpen::try_from(("README.md", 999, panes.as_slice())));
209 assert!(err.to_string().contains("missing pane"));
210 }
211
212 #[test]
213 fn try_from_errors_when_relative_file_is_missing() {
214 let dir = std::env::current_dir().unwrap();
215 let panes = vec![pane_with(1, 1, &dir)];
216 assert2::assert!(let
217 Err(err) = FileToOpen::try_from(("definitely_missing_12345__file.rs", 1, panes.as_slice()))
218 );
219 assert!(err.to_string().contains("error parsing file to open"));
220 }
221
222 #[test]
223 fn try_from_resolves_relative_existing_file() {
224 let dir = std::env::current_dir().unwrap();
225 let panes = vec![pane_with(7, 1, &dir)];
226 assert2::assert!(let Ok(file) = FileToOpen::try_from(("Cargo.toml", 7, panes.as_slice())));
227 let expected = FileToOpen {
228 path: dir.join("Cargo.toml").to_string_lossy().into_owned(),
229 line_nbr: 0,
230 column: 0,
231 };
232 assert_eq!(file, expected);
233 }
234
235 fn pane_with(pane_id: i64, tab_id: i64, cwd_fs: &std::path::Path) -> WeztermPane {
236 WeztermPane {
237 cursor_shape: "Block".into(),
238 cursor_visibility: "Visible".into(),
239 cursor_x: 0,
240 cursor_y: 0,
241 cwd: PathBuf::from(format!("file://host{}", cwd_fs.display())),
244 is_active: true,
245 is_zoomed: false,
246 left_col: 0,
247 pane_id,
248 size: WeztermPaneSize {
249 cols: 80,
250 dpi: 96,
251 pixel_height: 800,
252 pixel_width: 600,
253 rows: 24,
254 },
255 tab_id,
256 tab_title: "tab".into(),
257 title: "hx".into(),
258 top_row: 0,
259 tty_name: "tty".into(),
260 window_id: 1,
261 window_title: "win".into(),
262 workspace: "default".into(),
263 }
264 }
265}