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, OpenOptions, read_dir};
8use std::io::{self, BufReader, prelude::*};
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                match aa.as_ref().unwrap().get(t.to_str().unwrap()) {
120                    Some(re) => {
121                        // and has regex for this type
122                        let re = unsafe {
123                            match (re as *const Regex).clone().as_ref() {
124                                Some(a) => a,
125                                None => return,
126                            }
127                        };
128                        files.push(File(path.to_path_buf(), re))
129                    }
130                    _ => {}
131                }
132            }
133        }
134    } else {
135        if let Some(t) = path.extension() {
136            // file has extension
137            let aa = REGEX_TABLE.lock();
138            match aa.as_ref().unwrap().get(t.to_str().unwrap()) {
139                Some(re) => {
140                    // and has regex for this type
141                    let re = unsafe {
142                        match (re as *const Regex).clone().as_ref() {
143                            Some(a) => a,
144                            None => return,
145                        }
146                    };
147                    files.push(File(path.to_path_buf(), re))
148                }
149                _ => {}
150            }
151        }
152    }
153}
154
155/// Filter this line
156fn filter_line(line: &str, line_num: usize, re: &Regex) -> Option<Crumb> {
157    match re.find(line) {
158        Some(mat) => {
159            let position = mat.start();
160            let cap = re.captures(line).unwrap();
161            let content = cap[2].to_string();
162            let comment_symbol_header = cap[1].to_string();
163            if content.starts_with('!') {
164                Some(
165                    Crumb::new(line_num, position, content, comment_symbol_header)
166                        .add_ignore_flag(),
167                )
168            } else {
169                Some(Crumb::new(
170                    line_num,
171                    position,
172                    content,
173                    comment_symbol_header,
174                ))
175            }
176        }
177        None => None,
178    }
179}
180
181/// Operate this file
182fn op_file(file: File, kwreg: &Option<Regex>, conf: Arc<RwLock<Config>>) -> Result<Option<Bread>> {
183    let breads = match bake_bread(&file, kwreg, &conf.read().unwrap()) {
184        Ok(b) => b,
185        Err(e) => {
186            debug!("file {} had error {}", file.to_string(), e.to_string());
187            return Ok(None);
188        }
189    };
190
191    if !conf.read().unwrap().delete {
192        Ok(breads)
193    } else {
194        match breads {
195            Some(bb) => {
196                delete_the_crumbs(bb)?;
197                Ok(None)
198            }
199            None => Ok(None),
200        }
201    }
202}
203
204/// make bread for this file
205fn bake_bread(file: &File, kwreg: &Option<Regex>, conf: &Config) -> Result<Option<Bread>> {
206    // start to read file
207    let mut buf = vec![];
208    let file_p = file.to_string();
209    let mut f: std::fs::File = std::fs::File::open(file.0.clone())?;
210    f.read_to_end(&mut buf)?;
211
212    let mut line_num = 0;
213    let mut ss = String::new(); // temp
214    let mut buf = buf.as_slice();
215    let mut result = vec![];
216    let mut head: Option<Crumb> = None; // for tail support
217    let mut shadow_file = vec![]; // the copy of file for later range operation 
218
219    // closure for keywords feature
220    let mut keyword_checker_and_push = |mut cb: Crumb| {
221        if kwreg.is_some() {
222            // filter_keywords will update keyword even the crumb is ignored
223            if cb.filter_keywords(kwreg.as_ref().unwrap()) {
224                result.push(cb)
225            }
226        } else {
227            if !cb.is_ignore() || conf.show_ignored {
228                result.push(cb)
229            }
230        }
231    };
232
233    loop {
234        line_num += 1;
235        match buf.read_line(&mut ss) {
236            Ok(0) | Err(_) => {
237                if head.is_some() {
238                    keyword_checker_and_push(head.unwrap());
239                }
240                break; // if EOF or any error in this file, break
241            }
242            Ok(_) => match filter_line(&ss, line_num, file.1) {
243                Some(cb) => {
244                    // check head first
245                    match head {
246                        Some(ref mut h) => {
247                            if h.has_tail() {
248                                // if head has tail, add this line to head, continue
249                                h.add_tail(cb);
250                                ss.clear(); // before continue, clear temp
251                                continue;
252                            } else {
253                                // store head
254                                keyword_checker_and_push(head.unwrap());
255                                head = None;
256                            }
257                        }
258                        None => (),
259                    }
260
261                    if cb.has_tail() {
262                        // make new head
263                        head = Some(cb);
264                    } else {
265                        // store result
266                        keyword_checker_and_push(cb)
267                    }
268                }
269                None => {
270                    if head.is_some() {
271                        keyword_checker_and_push(head.unwrap());
272                        head = None;
273                    }
274                }
275            },
276        }
277
278        if conf.range > 0 {
279            shadow_file.push(ss.clone());
280        }
281
282        ss.clear()
283    }
284
285    // if range not equal 0, start to push the context around inside
286    if conf.range > 0 {
287        result.iter_mut().for_each(|crumb| {
288            let ahead_ind = (crumb.line_num - 1).saturating_sub(conf.range as usize);
289            let tail_ind = (crumb.line_num - 1)
290                .saturating_add(conf.range as usize)
291                .min(shadow_file.len());
292            crumb.range_content = Some(
293                (ahead_ind + 1..tail_ind + 1)
294                    .zip(
295                        shadow_file
296                            .get(ahead_ind..tail_ind)
297                            .map(|x| x.to_vec())
298                            .unwrap(),
299                    )
300                    .collect(),
301            );
302        });
303    }
304
305    if result.len() == 0 {
306        Ok(None)
307    } else {
308        Ok(Some(Bread::new(file_p, result)))
309    }
310}
311
312/// delete crumbs and re-write the file
313pub fn delete_the_crumbs(Bread { file_path, crumbs }: Bread) -> Result<String> {
314    let all_delete_line_postion_pairs = crumbs
315        .iter()
316        .map(|crumb| crumb.all_lines_num_postion_pair())
317        .flatten();
318
319    delete_lines_on(&file_path, all_delete_line_postion_pairs)?;
320
321    println!("deleted the crumbs in {}", file_path);
322    Ok(file_path)
323}
324
325/// delete crumbs by special indexes
326pub fn delete_the_crumbs_on_special_index(
327    Bread { file_path, crumbs }: Bread,
328    indexes: HashSet<usize>,
329) -> Result<String> {
330    let mut all_delete_lines = vec![];
331    for ind in &indexes {
332        match crumbs.get(*ind) {
333            Some(c) => all_delete_lines.append(&mut c.all_lines_num_postion_pair()),
334            None => return Err(io::Error::other("cannot find crumb index in bread")),
335        }
336    }
337
338    delete_lines_on(&file_path, all_delete_lines.into_iter())?;
339
340    println!("deleted {} crumbs in {}", indexes.len(), file_path);
341
342    Ok(file_path)
343}
344
345/// delete special lines of the file on file_path
346fn delete_lines_on(
347    file_path: &str,
348    line_num_pos_pairs: impl Iterator<Item = (usize, usize)>,
349) -> Result<()> {
350    let f = fs::File::open(&file_path)?;
351    let reader = BufReader::new(f).lines();
352
353    let all_delete_lines = line_num_pos_pairs.collect();
354
355    let finish_deleted = delete_nth_lines(reader, all_delete_lines)?
356        .into_iter()
357        .map(|line| line.into_bytes());
358
359    let mut new_file = OpenOptions::new()
360        .write(true)
361        .truncate(true)
362        .open(file_path)?;
363
364    for line in finish_deleted {
365        new_file.write_all(&line)?;
366        new_file.write_all(b"\n")?
367    }
368    Ok(())
369}
370
371/// delete crumbs of file, return the new file contents without the crumbs deleted
372fn delete_nth_lines(
373    f: impl Iterator<Item = Result<String>>,
374    nm: HashMap<usize, usize>,
375) -> Result<Vec<String>> {
376    let mut result = vec![];
377
378    for (line_num, ll) in f.enumerate() {
379        if nm.contains_key(&(line_num + 1)) {
380            let mut new_l = ll?;
381            new_l.truncate(*nm.get(&(line_num + 1)).unwrap());
382            if new_l == "" {
383                // empty line just skip
384                continue;
385            }
386            result.push(new_l);
387        } else {
388            result.push(ll?);
389        }
390    }
391
392    Ok(result)
393}
394
395/// restore the bread's crumb to normal comment
396pub fn restore_the_crumb(Bread { file_path, crumbs }: Bread) -> Result<String> {
397    let all_restore_lines = crumbs
398        .iter()
399        .map(|c| c.all_lines_num_postion_and_header_content())
400        .flatten();
401
402    restore_lines_on(&file_path, all_restore_lines)?;
403
404    println!("restored the crumbs in {}", file_path);
405    Ok(file_path)
406}
407
408/// restore the bread's crumb by special indexes
409pub fn restore_the_crumb_on_special_index(
410    Bread { file_path, crumbs }: Bread,
411    indexes: HashSet<usize>,
412) -> Result<String> {
413    let mut all_restore_lines = Vec::with_capacity(indexes.len());
414    for ind in &indexes {
415        match crumbs.get(*ind) {
416            Some(c) => all_restore_lines.append(&mut c.all_lines_num_postion_and_header_content()),
417            None => return Err(io::Error::other("cannot find crumb index in bread")),
418        }
419    }
420
421    restore_lines_on(&file_path, all_restore_lines.into_iter())?;
422
423    println!("restored {} crumbs in {}", indexes.len(), file_path);
424    Ok(file_path)
425}
426
427fn restore_lines_on<'a>(
428    file_path: &'a str,
429    all_restore_lines: impl Iterator<Item = (usize, usize, &'a str, &'a str)>,
430) -> Result<()> {
431    let f = fs::File::open(&file_path)?;
432    let reader = BufReader::new(f).lines();
433
434    let mut table: HashMap<usize, (usize, &str, &str)> =
435        HashMap::with_capacity(all_restore_lines.size_hint().1.unwrap_or(0));
436
437    all_restore_lines.for_each(|(line_num, pos, header, content)| {
438        table.insert(line_num, (pos, header, content));
439    });
440
441    let mut new_file = Vec::with_capacity(reader.size_hint().1.unwrap_or(0));
442    for (line_num, ll) in reader.enumerate() {
443        if let Some((pos, header, content)) = table.get(&(line_num + 1)) {
444            let mut new_l = ll?;
445            new_l.truncate(*pos);
446            new_l.push_str(*header);
447            new_l.push_str(" ");
448            new_l.push_str(*content);
449
450            new_file.push(new_l.into_bytes())
451        } else {
452            new_file.push(ll?.into_bytes());
453        }
454    }
455
456    let mut file = OpenOptions::new()
457        .write(true)
458        .truncate(true)
459        .open(file_path)?;
460
461    for line in new_file {
462        file.write_all(&line)?;
463        file.write_all(b"\n")?
464    }
465
466    Ok(())
467}
468
469/// run format command with filepath input
470pub fn run_format_command_to_file(
471    fmt_command: &str,
472    _files: impl IntoIterator<Item = String>,
473) -> std::result::Result<(), String> {
474    let mut command_splits = fmt_command.split(' ');
475    let first = command_splits
476        .next()
477        .ok_or("fmt_command cannot be empty".to_string())?;
478
479    let mut comm = Command::new(first);
480    let mut child = comm
481        .args(command_splits)
482        .spawn()
483        .expect("Cannot run the fmt_command");
484
485    println!("running fmt command: {}", fmt_command);
486    child
487        .wait()
488        .expect("fmt command wasn't running")
489        .exit_ok()
490        .map_err(|e| e.to_string())
491}
492
493/// entry function of main logic
494pub fn handle_files(conf: Config) -> impl Iterator<Item = Bread> {
495    // first add all files in arguments
496    let mut all_files: Vec<File> = files_in_dir_or_file_vec(&conf.files, &conf).unwrap();
497
498    // split to groups
499    let threads_num: usize = thread::available_parallelism()
500        .unwrap_or(THREAD_NUM.unwrap())
501        .into();
502
503    let len = all_files.len();
504    let count = len / threads_num;
505    let mut groups: Vec<Vec<File>> = vec![];
506    for _ in 0..threads_num - 1 {
507        groups.push(all_files.drain(0..count).collect())
508    }
509    groups.push(all_files.drain(0..).collect());
510
511    let conf = Arc::new(RwLock::new(conf));
512    groups
513        .into_iter()
514        .map(move |fs| {
515            let kwreg = KEYWORDS_REGEX.lock().unwrap().clone();
516            let conf_c = Arc::clone(&conf);
517            thread::spawn(|| {
518                fs.into_iter()
519                    .filter_map(move |f| op_file(f, &kwreg, conf_c.clone()).unwrap())
520                    .collect::<Vec<Bread>>()
521            })
522        })
523        .map(|han| han.join().unwrap())
524        .flatten()
525}
526
527#[cfg(test)]
528mod tests {
529    use super::*;
530
531    #[test]
532    fn test_files_and_dirs_in_path() -> Result<()> {
533        let (fs, dirs) = files_and_dirs_in_path("./tests/testcases", &Default::default())?;
534
535        assert_eq!(dirs.len(), 0);
536        assert_eq!(fs[0].0, PathBuf::from("./tests/testcases/multilines.rs"),);
537        Ok(())
538    }
539
540    // #[test]
541    // fn test_available_parallelism_on_my_machine() {
542    //     dbg!(thread::available_parallelism().unwrap());
543    // }
544}