1use std::fs::DirEntry;
5use std::fs::ReadDir;
6use std::path::Path;
7use std::path::PathBuf;
8
9use chrono::DateTime;
10use chrono::Local;
11use color_eyre::eyre::eyre;
12use nvim_oxi::Dictionary;
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() -> color_eyre::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<color_eyre::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 => return Some(Err(eyre!("error missing file name in path | path={}", path.display()))),
125 };
126 let base_name = match path.file_stem().map(|s| s.to_string_lossy()) {
127 Some(s) => s.to_string(),
128 None => return Some(Err(eyre!("error missing file stem in path | path={}", path.display()))),
129 };
130 let extension = match path.extension().map(|s| s.to_string_lossy()) {
131 Some(s) => s.to_string(),
132 None => return Some(Err(eyre!("error missing extension in path | path={}", path.display()))),
133 };
134
135 Some(Ok(Self {
136 display_name,
137 base_name,
138 extension,
139 path,
140 }))
141 }
142
143 pub fn dest_file_path(&self, dest_dir: &Path, date_time: DateTime<Local>) -> PathBuf {
148 dest_dir.join(format!(
149 "{}-{}.{}",
150 self.base_name,
151 date_time.format("%Y%m%d-%H%M%S"),
152 self.extension
153 ))
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use chrono::TimeZone;
160 use rstest::rstest;
161 use tempfile::TempDir;
162
163 use super::*;
164
165 #[rstest]
166 #[case("test.txt", "test.txt", "test", "txt")]
167 #[case(".hidden.txt", ".hidden.txt", ".hidden", "txt")]
168 fn scratch_from_when_valid_file_returns_some_ok(
169 #[case] file_name: &str,
170 #[case] expected_display: &str,
171 #[case] expected_base: &str,
172 #[case] expected_ext: &str,
173 ) {
174 let (_tmp_dir, entry) = dummy_dir_entry(file_name);
175 let expected_path = entry.path();
176
177 let result = Scratch::from(Ok(entry));
178
179 assert2::let_assert!(Some(Ok(actual)) = result);
180 pretty_assertions::assert_eq!(
181 actual,
182 Scratch {
183 display_name: expected_display.to_string(),
184 base_name: expected_base.to_string(),
185 extension: expected_ext.to_string(),
186 path: expected_path,
187 },
188 );
189 }
190
191 #[test]
192 fn scratch_from_when_directory_returns_none() {
193 let temp_dir = TempDir::new().unwrap();
194 let sub_dir = temp_dir.path().join("subdir");
195 std::fs::create_dir(&sub_dir).unwrap();
196 let mut read_dir = std::fs::read_dir(temp_dir.path()).unwrap();
197 let entry = read_dir.next().unwrap().unwrap();
198
199 let result = Scratch::from(Ok(entry));
200
201 assert!(result.is_none());
202 }
203
204 #[rstest]
205 #[case("test", "missing extension")]
206 #[case(".hidden", "missing extension")]
207 fn scratch_from_when_invalid_file_returns_some_expected_error(
208 #[case] file_name: &str,
209 #[case] expected_error: &str,
210 ) {
211 let (_tmp_dir, entry) = dummy_dir_entry(file_name);
212
213 let result = Scratch::from(Ok(entry));
214
215 assert2::let_assert!(Some(Err(err)) = result);
216 assert!(err.to_string().contains(expected_error));
217 }
218
219 #[test]
220 fn scratch_from_when_io_error_returns_some_expected_err() {
221 let err = std::io::Error::new(std::io::ErrorKind::NotFound, "test error");
222
223 let result = Scratch::from(Err(err));
224
225 assert2::let_assert!(Some(Err(e)) = result);
226 assert!(e.to_string().contains("test error"));
227 }
228
229 #[test]
230 fn scratch_dest_file_path_returns_expected_path() {
231 let scratch = Scratch {
232 display_name: "test.txt".to_string(),
233 base_name: "test".to_string(),
234 extension: "txt".to_string(),
235 path: PathBuf::from("/some/path/test.txt"),
236 };
237
238 let date_time = Local.with_ymd_and_hms(2023, 1, 1, 12, 0, 0).unwrap();
239 let result = scratch.dest_file_path(Path::new("/tmp"), date_time);
240
241 pretty_assertions::assert_eq!(result, PathBuf::from("/tmp/test-20230101-120000.txt"));
242 }
243
244 fn dummy_dir_entry(file_name: &str) -> (TempDir, DirEntry) {
245 let tmp_dir = TempDir::new().unwrap();
246 let file_path = tmp_dir.path().join(file_name);
247 std::fs::write(&file_path, "content").unwrap();
248 let mut read_dir = std::fs::read_dir(tmp_dir.path()).unwrap();
249 (tmp_dir, read_dir.next().unwrap().unwrap())
250 }
251}