code_it_later_rs/
fs_operation.rs

1use super::config::{Config, KEYWORDS_REGEX, REGEX_TABLE};
2use super::datatypes::*;
3use log::debug;
4use regex::Regex;
5use std::collections::{HashMap, HashSet};
6use std::ffi::OsString;
7use std::fs::{self, read_dir, OpenOptions};
8use std::io::{self, prelude::*, BufReader};
9use std::num::NonZeroUsize;
10use std::process::Command;
11use std::sync::{Arc, RwLock};
12use std::{io::Result, path::Path, path::PathBuf, thread};
13
14/// how many thread when it runs
15const THREAD_NUM: Option<NonZeroUsize> = NonZeroUsize::new(4);
16
17/// Vector of all pathbufs
18type Dirs = Vec<PathBuf>;
19
20/// File struct, including file path and the &Regex of this file
21/// &Regex CANNOT be nil
22#[derive(Debug)]
23struct File(PathBuf, &'static Regex);
24
25impl File {
26    /// Return string of file path
27    fn to_string(&self) -> String {
28        self.0.as_os_str().to_os_string().into_string().unwrap()
29    }
30}
31
32type Files = Vec<File>;
33
34/// loop all string inside paths_or_files, if it is file, store it, if it is dir
35/// store all files inside thsi dir (recursivly)
36fn files_in_dir_or_file_vec(paths_or_files: &[impl AsRef<Path>], conf: &Config) -> Result<Files> {
37    let mut result: Files = vec![];
38    for ele in paths_or_files {
39        if ele.as_ref().is_dir() {
40            result.append(&mut all_files_in_dir(ele, conf)?)
41        } else {
42            file_checker(
43                &mut result,
44                ele.as_ref(),
45                &conf.filetypes,
46                conf.filetypes.len(),
47            )
48        }
49    }
50    Ok(result)
51}
52
53/// Find all files in this dir recursivly
54fn all_files_in_dir<T>(p: T, conf: &Config) -> Result<Files>
55where
56    T: AsRef<Path>,
57{
58    let mut result = vec![];
59    let (mut files, dirs) = files_and_dirs_in_path(p, &conf)?;
60    result.append(&mut files);
61
62    if dirs.len() != 0 {
63        result.append(
64            &mut dirs
65                .iter()
66                .map(|d| all_files_in_dir(d, conf).unwrap())
67                .flatten()
68                .collect::<Files>(),
69        )
70    }
71
72    Ok(result)
73}
74
75/// Find files and dirs in this folder
76fn files_and_dirs_in_path(p: impl AsRef<Path>, conf: &Config) -> Result<(Files, Dirs)> {
77    let (mut f, mut d): (Files, Dirs) = (vec![], vec![]);
78
79    // get filetypes
80    let filetypes = &conf.filetypes;
81    let filetypes_count = filetypes.len();
82
83    // get ignore dirs
84    let ignore_dirs = &conf.ignore_dirs;
85    let ignore_dirs_count = ignore_dirs.len();
86
87    for entry in read_dir(p)? {
88        let dir = entry?;
89        let path = dir.path();
90
91        if path.is_dir() {
92            // check ignore dirs
93            if ignore_dirs_count != 0 {
94                if let Some(d_name) = path.file_name() {
95                    if !ignore_dirs.contains(&d_name.to_os_string()) {
96                        d.push(path)
97                    }
98                }
99            } else {
100                d.push(path)
101            }
102        } else {
103            file_checker(&mut f, &path, &filetypes, filetypes_count)
104        }
105    }
106    Ok((f, d))
107}
108
109/// if file path pass check, add it to files
110fn file_checker(files: &mut Files, path: &Path, filetypes: &[OsString], filetypes_count: usize) {
111    // check filetypes
112    if filetypes_count != 0 {
113        // special filetypes
114        if let Some(t) = path.extension() {
115            // file has extension
116            if filetypes.contains(&t.to_os_string()) {
117                // this file include in filetypes
118                let aa = REGEX_TABLE.lock();
119                if let Some(re) = aa.as_ref().unwrap().get(t.to_str().unwrap()) {
120                    // and has regex for this type
121                    let re = unsafe {
122                        match (re as *const Regex).clone().as_ref() {
123                            Some(a) => a,
124                            None => return,
125                        }
126                    };
127                    files.push(File(path.to_path_buf(), re))
128                }
129            }
130        }
131    } else {
132        if let Some(t) = path.extension() {
133            // file has extension
134            let aa = REGEX_TABLE.lock();
135            if let Some(re) = aa.as_ref().unwrap().get(t.to_str().unwrap()) {
136                // and has regex for this type
137                let re = unsafe {
138                    match (re as *const Regex).clone().as_ref() {
139                        Some(a) => a,
140                        None => return,
141                    }
142                };
143                files.push(File(path.to_path_buf(), re))
144            }
145        }
146    }
147}
148
149/// Filter this line
150fn filter_line(line: &str, line_num: usize, re: &Regex) -> Option<Crumb> {
151    match re.find(line) {
152        Some(mat) => {
153            let position = mat.start();
154            let cap = re.captures(line).unwrap();
155            let content = cap[2].to_string();
156            let comment_symbol_header = cap[1].to_string();
157            if content.starts_with('!') {
158                Some(
159                    Crumb::new(line_num, position, content, comment_symbol_header)
160                        .add_ignore_flag(),
161                )
162            } else {
163                Some(Crumb::new(
164                    line_num,
165                    position,
166                    content,
167                    comment_symbol_header,
168                ))
169            }
170        }
171        None => None,
172    }
173}
174
175/// Operate this file
176fn op_file(file: File, kwreg: &Option<Regex>, conf: Arc<RwLock<Config>>) -> Result<Option<Bread>> {
177    let breads = match bake_bread(&file, kwreg, &conf.read().unwrap()) {
178        Ok(b) => b,
179        Err(e) => {
180            debug!("file {} had error {}", file.to_string(), e.to_string());
181            return Ok(None);
182        }
183    };
184
185    if !conf.read().unwrap().delete {
186        Ok(breads)
187    } else {
188        match breads {
189            Some(bb) => {
190                delete_the_crumbs(bb)?;
191                Ok(None)
192            }
193            None => Ok(None),
194        }
195    }
196}
197
198/// make bread for this file
199fn bake_bread(file: &File, kwreg: &Option<Regex>, conf: &Config) -> Result<Option<Bread>> {
200    // start to read file
201    let mut buf = vec![];
202    let file_p = file.to_string();
203    let mut f: std::fs::File = std::fs::File::open(file.0.clone())?;
204    f.read_to_end(&mut buf)?;
205
206    let mut line_num = 0;
207    let mut ss = String::new(); // temp
208    let mut buf = buf.as_slice();
209    let mut result = vec![];
210    let mut head: Option<Crumb> = None; // for tail support
211
212    // closure for keywords feature
213    let mut keyword_checker_and_push = |mut cb: Crumb| {
214        if kwreg.is_some() {
215            // filter_keywords will update keyword even the crumb is ignored
216            if cb.filter_keywords(kwreg.as_ref().unwrap()) {
217                result.push(cb)
218            }
219        } else {
220            if !cb.is_ignore() || conf.show_ignored {
221                result.push(cb)
222            }
223        }
224    };
225
226    loop {
227        line_num += 1;
228        match buf.read_line(&mut ss) {
229            Ok(0) | Err(_) => {
230                if head.is_some() {
231                    keyword_checker_and_push(head.unwrap());
232                }
233                break; // if EOF or any error in this file, break
234            }
235            Ok(_) => match filter_line(&ss, line_num, file.1) {
236                Some(cb) => {
237                    // check head first
238                    match head {
239                        Some(ref mut h) => {
240                            if h.has_tail() {
241                                // if head has tail, add this line to head, continue
242                                h.add_tail(cb);
243                                ss.clear(); // before continue, clear temp
244                                continue;
245                            } else {
246                                // store head
247                                keyword_checker_and_push(head.unwrap());
248                                head = None;
249                            }
250                        }
251                        None => (),
252                    }
253
254                    if cb.has_tail() {
255                        // make new head
256                        head = Some(cb);
257                    } else {
258                        // store result
259                        keyword_checker_and_push(cb)
260                    }
261                }
262                None => {
263                    if head.is_some() {
264                        keyword_checker_and_push(head.unwrap());
265                        head = None;
266                    }
267                }
268            },
269        }
270        ss.clear()
271    }
272
273    if result.len() == 0 {
274        Ok(None)
275    } else {
276        Ok(Some(Bread::new(file_p, result)))
277    }
278}
279
280/// delete crumbs and re-write the file
281pub fn delete_the_crumbs(Bread { file_path, crumbs }: Bread) -> Result<String> {
282    let all_delete_line_postion_pairs = crumbs
283        .iter()
284        .map(|crumb| crumb.all_lines_num_postion_pair())
285        .flatten();
286
287    delete_lines_on(&file_path, all_delete_line_postion_pairs)?;
288
289    println!("deleted the crumbs in {}", file_path);
290    Ok(file_path)
291}
292
293/// delete crumbs by special indexes
294pub fn delete_the_crumbs_on_special_index(
295    Bread { file_path, crumbs }: Bread,
296    indexes: HashSet<usize>,
297) -> Result<String> {
298    let mut all_delete_lines = vec![];
299    for ind in &indexes {
300        match crumbs.get(*ind) {
301            Some(c) => all_delete_lines.append(&mut c.all_lines_num_postion_pair()),
302            None => return Err(io::Error::other("cannot find crumb index in bread")),
303        }
304    }
305
306    delete_lines_on(&file_path, all_delete_lines.into_iter())?;
307
308    println!("deleted {} crumbs in {}", indexes.len(), file_path);
309
310    Ok(file_path)
311}
312
313/// delete special lines of the file on file_path
314fn delete_lines_on(
315    file_path: &str,
316    line_num_pos_pairs: impl Iterator<Item = (usize, usize)>,
317) -> Result<()> {
318    let f = fs::File::open(&file_path)?;
319    let reader = BufReader::new(f).lines();
320
321    let all_delete_lines = line_num_pos_pairs.collect();
322
323    let finish_deleted = delete_nth_lines(reader, all_delete_lines)?
324        .into_iter()
325        .map(|line| line.into_bytes());
326
327    let mut new_file = OpenOptions::new()
328        .write(true)
329        .truncate(true)
330        .open(file_path)?;
331
332    for line in finish_deleted {
333        new_file.write_all(&line)?;
334        new_file.write_all(b"\n")?
335    }
336    Ok(())
337}
338
339/// delete crumbs of file, return the new file contents without the crumbs deleted
340fn delete_nth_lines(
341    f: impl Iterator<Item = Result<String>>,
342    nm: HashMap<usize, usize>,
343) -> Result<Vec<String>> {
344    let mut result = vec![];
345
346    for (line_num, ll) in f.enumerate() {
347        if nm.contains_key(&(line_num + 1)) {
348            let mut new_l = ll?;
349            new_l.truncate(*nm.get(&(line_num + 1)).unwrap());
350            if new_l == "" {
351                // empty line just skip
352                continue;
353            }
354            result.push(new_l);
355        } else {
356            result.push(ll?);
357        }
358    }
359
360    Ok(result)
361}
362
363/// restore the bread's crumb to normal comment
364pub fn restore_the_crumb(Bread { file_path, crumbs }: Bread) -> Result<String> {
365    let all_restore_lines = crumbs
366        .iter()
367        .map(|c| c.all_lines_num_postion_and_header_content())
368        .flatten();
369
370    restore_lines_on(&file_path, all_restore_lines)?;
371
372    println!("restored the crumbs in {}", file_path);
373    Ok(file_path)
374}
375
376/// restore the bread's crumb by special indexes
377pub fn restore_the_crumb_on_special_index(
378    Bread { file_path, crumbs }: Bread,
379    indexes: HashSet<usize>,
380) -> Result<String> {
381    let mut all_restore_lines = Vec::with_capacity(indexes.len());
382    for ind in &indexes {
383        match crumbs.get(*ind) {
384            Some(c) => all_restore_lines.append(&mut c.all_lines_num_postion_and_header_content()),
385            None => return Err(io::Error::other("cannot find crumb index in bread")),
386        }
387    }
388
389    restore_lines_on(&file_path, all_restore_lines.into_iter())?;
390
391    println!("restored {} crumbs in {}", indexes.len(), file_path);
392    Ok(file_path)
393}
394
395fn restore_lines_on<'a>(
396    file_path: &'a str,
397    all_restore_lines: impl Iterator<Item = (usize, usize, &'a str, &'a str)>,
398) -> Result<()> {
399    let f = fs::File::open(&file_path)?;
400    let reader = BufReader::new(f).lines();
401
402    let mut table: HashMap<usize, (usize, &str, &str)> =
403        HashMap::with_capacity(all_restore_lines.size_hint().1.unwrap_or(0));
404
405    all_restore_lines.for_each(|(line_num, pos, header, content)| {
406        table.insert(line_num, (pos, header, content));
407    });
408
409    let mut new_file = Vec::with_capacity(reader.size_hint().1.unwrap_or(0));
410    for (line_num, ll) in reader.enumerate() {
411        if let Some((pos, header, content)) = table.get(&(line_num + 1)) {
412            let mut new_l = ll?;
413            new_l.truncate(*pos);
414            new_l.push_str(*header);
415            new_l.push_str(" ");
416            new_l.push_str(*content);
417
418            new_file.push(new_l.into_bytes())
419        } else {
420            new_file.push(ll?.into_bytes());
421        }
422    }
423
424    let mut file = OpenOptions::new()
425        .write(true)
426        .truncate(true)
427        .open(file_path)?;
428
429    for line in new_file {
430        file.write_all(&line)?;
431        file.write_all(b"\n")?
432    }
433
434    Ok(())
435}
436
437/// run format command with filepath input
438pub fn run_format_command_to_file(
439    fmt_command: &str,
440    _files: impl IntoIterator<Item = String>,
441) -> std::result::Result<(), String> {
442    let mut command_splits = fmt_command.split(' ');
443    let first = command_splits
444        .next()
445        .ok_or("fmt_command cannot be empty".to_string())?;
446
447    let mut comm = Command::new(first);
448    let mut child = comm
449        .args(command_splits)
450        .spawn()
451        .expect("Cannot run the fmt_command");
452
453    println!("running fmt command: {}", fmt_command);
454    child
455        .wait()
456        .expect("fmt command wasn't running")
457        .exit_ok()
458        .map_err(|e| e.to_string())
459}
460
461/// entry function of main logic
462pub fn handle_files(conf: Config) -> impl Iterator<Item = Bread> {
463    // first add all files in arguments
464    let mut all_files: Vec<File> = files_in_dir_or_file_vec(&conf.files, &conf).unwrap();
465
466    // split to groups
467    let threads_num: usize = thread::available_parallelism()
468        .unwrap_or(THREAD_NUM.unwrap())
469        .into();
470
471    let len = all_files.len();
472    let count = len / threads_num;
473    let mut groups: Vec<Vec<File>> = vec![];
474    for _ in 0..threads_num - 1 {
475        groups.push(all_files.drain(0..count).collect())
476    }
477    groups.push(all_files.drain(0..).collect());
478
479    let conf = Arc::new(RwLock::new(conf));
480    groups
481        .into_iter()
482        .map(move |fs| {
483            let kwreg = KEYWORDS_REGEX.lock().unwrap().clone();
484            let conf_c = Arc::clone(&conf);
485            thread::spawn(|| {
486                fs.into_iter()
487                    .filter_map(move |f| op_file(f, &kwreg, conf_c.clone()).unwrap())
488                    .collect::<Vec<Bread>>()
489            })
490        })
491        .map(|han| han.join().unwrap())
492        .flatten()
493}
494
495#[cfg(test)]
496mod tests {
497    use super::*;
498
499    #[test]
500    fn test_files_and_dirs_in_path() -> Result<()> {
501        let (fs, dirs) = files_and_dirs_in_path("./tests/testcases", &Default::default())?;
502
503        assert_eq!(dirs.len(), 0);
504        assert_eq!(fs[0].0, PathBuf::from("./tests/testcases/multilines.rs"),);
505        Ok(())
506    }
507
508    // #[test]
509    // fn test_available_parallelism_on_my_machine() {
510    //     dbg!(thread::available_parallelism().unwrap());
511    // }
512}