1use std::fs::DirEntry;
5use std::fs::ReadDir;
6use std::path::Path;
7use std::path::PathBuf;
8
9use jiff::Zoned;
10use nvim_oxi::Dictionary;
11use rootcause::report;
12
13const SCRATCHES_PATH_PARTS: &[&str] = &["yog", "nvrim", "src", "plugins", "attempt"];
14
15pub fn dict() -> Dictionary {
17 dict! {
18 "create_scratch_file": fn_from!(create_scratch_file),
19 }
20}
21
22#[derive(Clone, Debug)]
24#[cfg_attr(test, derive(Eq, PartialEq))]
25struct Scratch {
26 display_name: String,
28 base_name: String,
30 extension: String,
32 path: PathBuf,
34}
35
36impl Scratch {
37 pub fn from(read_dir_res: std::io::Result<DirEntry>) -> Option<rootcause::Result<Self>> {
39 let path = match read_dir_res.map(|entry| entry.path()) {
40 Ok(path) => path,
41 Err(err) => return Some(Err(err.into())),
42 };
43 if !path.is_file() {
44 return None;
45 }
46 let display_name = match path.file_name().map(|s| s.to_string_lossy()) {
47 Some(s) => s.to_string(),
48 None => {
49 return Some(Err(
50 report!("error missing file name in path").attach(format!("path={}", path.display()))
51 ));
52 }
53 };
54 let base_name = match path.file_stem().map(|s| s.to_string_lossy()) {
55 Some(s) => s.to_string(),
56 None => {
57 return Some(Err(
58 report!("error missing file stem in path").attach(format!("path={}", path.display()))
59 ));
60 }
61 };
62 let extension = match path.extension().map(|s| s.to_string_lossy()) {
63 Some(s) => s.to_string(),
64 None => {
65 return Some(Err(
66 report!("error missing extension in path").attach(format!("path={}", path.display()))
67 ));
68 }
69 };
70
71 Some(Ok(Self {
72 display_name,
73 base_name,
74 extension,
75 path,
76 }))
77 }
78
79 pub fn dest_file_path(&self, dest_dir: &Path, date_time: &Zoned) -> PathBuf {
84 dest_dir.join(format!(
85 "{}-{}.{}",
86 self.base_name,
87 date_time.strftime("%Y%m%d-%H%M%S"),
88 self.extension
89 ))
90 }
91}
92
93fn create_scratch_file(_: ()) {
98 let Ok(scratches_dir_content) = get_scratches_dir_content() else {
99 return;
100 };
101
102 let scratches = scratches_dir_content
103 .into_iter()
104 .filter_map(|entry| {
105 Scratch::from(entry)?
106 .inspect_err(|err| {
107 ytil_noxi::notify::error(format!("error building Scratch struct | error={err:#?}"));
108 })
109 .ok()
110 })
111 .collect::<Vec<_>>();
112
113 let dest_dir = Path::new("/tmp").join("attempt.rs");
114
115 if let Err(err) = std::fs::create_dir_all(&dest_dir) {
116 ytil_noxi::notify::error(format!(
117 "cannot create dest dir | dest_dir={} error={err:#?}",
118 dest_dir.display()
119 ));
120 return;
121 }
122
123 let callback = {
124 let scratches = scratches.clone();
125 move |choice_idx| {
126 let Some(scratch): Option<&Scratch> = scratches.get(choice_idx) else {
127 return;
128 };
129 let dest = scratch.dest_file_path(&dest_dir, &Zoned::now());
130 if let Err(err) = std::fs::copy(&scratch.path, &dest) {
131 ytil_noxi::notify::error(format!(
132 "cannot copy file | from={} to={} error={err:#?}",
133 scratch.path.display(),
134 dest.display()
135 ));
136 return;
137 }
138 drop(ytil_noxi::buffer::open(&dest, None, None));
139 }
140 };
141
142 if let Err(err) = ytil_noxi::vim_ui_select::open(
143 scratches.iter().map(|scratch| scratch.display_name.as_str()),
144 &[("prompt", "Create scratch file ")],
145 callback,
146 None,
147 ) {
148 ytil_noxi::notify::error(format!("error creating scratch file | error={err:#?}"));
149 }
150}
151
152fn get_scratches_dir_content() -> rootcause::Result<ReadDir> {
157 ytil_sys::dir::get_workspace_root()
158 .map(|workspace_root| ytil_sys::dir::build_path(workspace_root, SCRATCHES_PATH_PARTS))
159 .inspect_err(|err| {
160 ytil_noxi::notify::error(format!("error getting workspace root | error={err:#?}"));
161 })
162 .and_then(|dir| std::fs::read_dir(dir).map_err(From::from))
163 .inspect_err(|err| {
164 ytil_noxi::notify::error(format!("error reading attempt files dir | error={err:#?}"));
165 })
166}
167
168#[cfg(test)]
169mod tests {
170 use jiff::Zoned;
171 use rstest::rstest;
172 use tempfile::TempDir;
173 use test_that::prelude::*;
174
175 use super::*;
176
177 #[rstest]
178 #[case("test.txt", "test.txt", "test", "txt")]
179 #[case(".hidden.txt", ".hidden.txt", ".hidden", "txt")]
180 fn test_scratch_from_when_valid_file_returns_some_ok(
181 #[case] file_name: &str,
182 #[case] expected_display: &str,
183 #[case] expected_base: &str,
184 #[case] expected_ext: &str,
185 ) {
186 let (_tmp_dir, entry) = dummy_dir_entry(file_name);
187 let expected_path = entry.path();
188
189 let result = Scratch::from(Ok(entry));
190
191 assert_that!(result, some(ok(anything())));
192 let actual = result
193 .expect("scratch conversion should return a result")
194 .expect("valid file should convert to scratch");
195 assert_that!(
196 actual,
197 eq(Scratch {
198 display_name: expected_display.to_string(),
199 base_name: expected_base.to_string(),
200 extension: expected_ext.to_string(),
201 path: expected_path,
202 })
203 );
204 }
205
206 #[test]
207 fn test_scratch_from_when_directory_returns_none() {
208 let temp_dir = TempDir::new().unwrap();
209 let sub_dir = temp_dir.path().join("subdir");
210 std::fs::create_dir(&sub_dir).unwrap();
211 let mut read_dir = std::fs::read_dir(temp_dir.path()).unwrap();
212 let entry = read_dir.next().unwrap().unwrap();
213
214 let result = Scratch::from(Ok(entry));
215
216 assert_that!(result, none());
217 }
218
219 #[rstest]
220 #[case("test", "missing extension")]
221 #[case(".hidden", "missing extension")]
222 fn test_scratch_from_when_invalid_file_returns_some_expected_error(
223 #[case] file_name: &str,
224 #[case] expected_error: &str,
225 ) {
226 let (_tmp_dir, entry) = dummy_dir_entry(file_name);
227
228 let result = Scratch::from(Ok(entry));
229
230 assert_that!(
231 result.as_ref().map(|res| res.as_ref().map(|_| ())),
232 some(err(anything()))
233 );
234 let err = result
235 .expect("invalid file should return a result")
236 .map_or_else(|err| err, |_| panic!("invalid file should fail to convert"));
237 assert_that!(err.to_string(), contains_substring(expected_error));
238 }
239
240 #[test]
241 fn test_scratch_from_when_io_error_returns_some_expected_err() {
242 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "test error");
243
244 let result = Scratch::from(Err(io_err));
245
246 assert_that!(
247 result.as_ref().map(|res| res.as_ref().map(|_| ())),
248 some(err(anything()))
249 );
250 let err = result
251 .expect("I/O failure should return a result")
252 .map_or_else(|err| err, |_| panic!("I/O failure should propagate"));
253 assert_that!(err.to_string(), contains_substring("test error"));
254 }
255
256 #[test]
257 fn test_scratch_dest_file_path_returns_expected_path() {
258 let scratch = Scratch {
259 display_name: "test.txt".to_string(),
260 base_name: "test".to_string(),
261 extension: "txt".to_string(),
262 path: PathBuf::from("/some/path/test.txt"),
263 };
264
265 let date_time = Zoned::now()
266 .with()
267 .year(2023)
268 .month(1)
269 .day(1)
270 .hour(12)
271 .minute(0)
272 .second(0)
273 .subsec_nanosecond(0)
274 .build()
275 .unwrap();
276 let result = scratch.dest_file_path(Path::new("/tmp"), &date_time);
277
278 assert_that!(result, eq(PathBuf::from("/tmp/test-20230101-120000.txt")));
279 }
280
281 fn dummy_dir_entry(file_name: &str) -> (TempDir, DirEntry) {
282 let tmp_dir = TempDir::new().unwrap();
283 let file_path = tmp_dir.path().join(file_name);
284 std::fs::write(&file_path, "content").unwrap();
285 let mut read_dir = std::fs::read_dir(tmp_dir.path()).unwrap();
286 (tmp_dir, read_dir.next().unwrap().unwrap())
287 }
288}