1use std::fs::DirEntry;
5use std::fs::ReadDir;
6use std::path::Path;
7use std::path::PathBuf;
8
9use chrono::DateTime;
10use chrono::Local;
11use nvim_oxi::Dictionary;
12use rootcause::report;
13
14const SCRATCHES_PATH_PARTS: &[&str] = &["yog", "nvrim", "src", "plugins", "attempt"];
15
16pub fn dict() -> Dictionary {
18 dict! {
19 "create_scratch_file": fn_from!(create_scratch_file),
20 }
21}
22
23fn create_scratch_file(_: ()) {
28 let Ok(scratches_dir_content) = get_scratches_dir_content() else {
29 return;
30 };
31
32 let scratches = scratches_dir_content
33 .into_iter()
34 .filter_map(|entry| {
35 Scratch::from(entry)?
36 .inspect_err(|err| {
37 ytil_noxi::notify::error(format!("error building Scratch struct | error={err:#?}"));
38 })
39 .ok()
40 })
41 .collect::<Vec<_>>();
42
43 let dest_dir = Path::new("/tmp").join("attempt.rs");
44
45 if let Err(err) = std::fs::create_dir_all(&dest_dir) {
46 ytil_noxi::notify::error(format!(
47 "cannot create dest dir | dest_dir={} error={err:#?}",
48 dest_dir.display()
49 ));
50 return;
51 }
52
53 let callback = {
54 let scratches = scratches.clone();
55 move |choice_idx| {
56 let Some(scratch): Option<&Scratch> = scratches.get(choice_idx) else {
57 return;
58 };
59 let dest = scratch.dest_file_path(&dest_dir, Local::now());
60 if let Err(err) = std::fs::copy(&scratch.path, &dest) {
61 ytil_noxi::notify::error(format!(
62 "cannot copy file | from={} to={} error={err:#?}",
63 scratch.path.display(),
64 dest.display()
65 ));
66 return;
67 }
68 let _ = ytil_noxi::buffer::open(&dest, None, None);
69 }
70 };
71
72 if let Err(err) = ytil_noxi::vim_ui_select::open(
73 scratches.iter().map(|scratch| scratch.display_name.as_str()),
74 &[("prompt", "Create scratch file ")],
75 callback,
76 None,
77 ) {
78 ytil_noxi::notify::error(format!("error creating scratch file | error={err:#?}"));
79 }
80}
81
82fn get_scratches_dir_content() -> rootcause::Result<ReadDir> {
87 ytil_sys::dir::get_workspace_root()
88 .map(|workspace_root| ytil_sys::dir::build_path(workspace_root, SCRATCHES_PATH_PARTS))
89 .inspect_err(|err| {
90 ytil_noxi::notify::error(format!("error getting workspace root | error={err:#?}"));
91 })
92 .and_then(|dir| std::fs::read_dir(dir).map_err(From::from))
93 .inspect_err(|err| {
94 ytil_noxi::notify::error(format!("error reading attempt files dir | error={err:#?}"));
95 })
96}
97
98#[derive(Clone, Debug)]
100#[cfg_attr(test, derive(Eq, PartialEq))]
101struct Scratch {
102 display_name: String,
104 base_name: String,
106 extension: String,
108 path: PathBuf,
110}
111
112impl Scratch {
113 pub fn from(read_dir_res: std::io::Result<DirEntry>) -> Option<rootcause::Result<Self>> {
115 let path = match read_dir_res.map(|entry| entry.path()) {
116 Ok(path) => path,
117 Err(err) => return Some(Err(err.into())),
118 };
119 if !path.is_file() {
120 return None;
121 }
122 let display_name = match path.file_name().map(|s| s.to_string_lossy()) {
123 Some(s) => s.to_string(),
124 None => {
125 return Some(Err(
126 report!("error missing file name in path").attach(format!("path={}", path.display()))
127 ));
128 }
129 };
130 let base_name = match path.file_stem().map(|s| s.to_string_lossy()) {
131 Some(s) => s.to_string(),
132 None => {
133 return Some(Err(
134 report!("error missing file stem in path").attach(format!("path={}", path.display()))
135 ));
136 }
137 };
138 let extension = match path.extension().map(|s| s.to_string_lossy()) {
139 Some(s) => s.to_string(),
140 None => {
141 return Some(Err(
142 report!("error missing extension in path").attach(format!("path={}", path.display()))
143 ));
144 }
145 };
146
147 Some(Ok(Self {
148 display_name,
149 base_name,
150 extension,
151 path,
152 }))
153 }
154
155 pub fn dest_file_path(&self, dest_dir: &Path, date_time: DateTime<Local>) -> PathBuf {
160 dest_dir.join(format!(
161 "{}-{}.{}",
162 self.base_name,
163 date_time.format("%Y%m%d-%H%M%S"),
164 self.extension
165 ))
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use chrono::TimeZone;
172 use rstest::rstest;
173 use tempfile::TempDir;
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 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 assert2::assert!(let Some(Ok(actual)) = result);
192 pretty_assertions::assert_eq!(
193 actual,
194 Scratch {
195 display_name: expected_display.to_string(),
196 base_name: expected_base.to_string(),
197 extension: expected_ext.to_string(),
198 path: expected_path,
199 },
200 );
201 }
202
203 #[test]
204 fn scratch_from_when_directory_returns_none() {
205 let temp_dir = TempDir::new().unwrap();
206 let sub_dir = temp_dir.path().join("subdir");
207 std::fs::create_dir(&sub_dir).unwrap();
208 let mut read_dir = std::fs::read_dir(temp_dir.path()).unwrap();
209 let entry = read_dir.next().unwrap().unwrap();
210
211 let result = Scratch::from(Ok(entry));
212
213 assert!(result.is_none());
214 }
215
216 #[rstest]
217 #[case("test", "missing extension")]
218 #[case(".hidden", "missing extension")]
219 fn scratch_from_when_invalid_file_returns_some_expected_error(
220 #[case] file_name: &str,
221 #[case] expected_error: &str,
222 ) {
223 let (_tmp_dir, entry) = dummy_dir_entry(file_name);
224
225 let result = Scratch::from(Ok(entry));
226
227 assert2::assert!(let Some(Err(err)) = result);
228 assert!(err.to_string().contains(expected_error));
229 }
230
231 #[test]
232 fn scratch_from_when_io_error_returns_some_expected_err() {
233 let err = std::io::Error::new(std::io::ErrorKind::NotFound, "test error");
234
235 let result = Scratch::from(Err(err));
236
237 assert2::assert!(let Some(Err(e)) = result);
238 assert!(e.to_string().contains("test error"));
239 }
240
241 #[test]
242 fn scratch_dest_file_path_returns_expected_path() {
243 let scratch = Scratch {
244 display_name: "test.txt".to_string(),
245 base_name: "test".to_string(),
246 extension: "txt".to_string(),
247 path: PathBuf::from("/some/path/test.txt"),
248 };
249
250 let date_time = Local.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
251 let result = scratch.dest_file_path(Path::new("/tmp"), date_time);
252
253 pretty_assertions::assert_eq!(result, PathBuf::from("/tmp/test-20230101-120000.txt"));
254 }
255
256 fn dummy_dir_entry(file_name: &str) -> (TempDir, DirEntry) {
257 let tmp_dir = TempDir::new().unwrap();
258 let file_path = tmp_dir.path().join(file_name);
259 std::fs::write(&file_path, "content").unwrap();
260 let mut read_dir = std::fs::read_dir(tmp_dir.path()).unwrap();
261 (tmp_dir, read_dir.next().unwrap().unwrap())
262 }
263}