Skip to main content

code_it_later_rs/
fs_operation.rs

1use crate::config::REGEX_TABLE_MUL;
2
3use super::config::{Config, FALLBACK_REGEX, KEYWORDS_REGEX, REGEX_TABLE};
4use super::datatypes::*;
5use log::debug;
6use regex::Regex;
7use std::collections::{HashMap, HashSet};
8use std::ffi::OsString;
9use std::fs::{self, OpenOptions, read_dir};
10use std::io::{self, BufReader, prelude::*};
11use std::num::NonZeroUsize;
12use std::process::Command;
13use std::sync::{Arc, RwLock};
14use std::{io::Result, path::Path, path::PathBuf, thread};
15
16/// how many thread when it runs
17const THREAD_NUM: Option<NonZeroUsize> = NonZeroUsize::new(4);
18
19/// Vector of all pathbufs
20type Dirs = Vec<PathBuf>;
21
22/// File struct, including file path and the &Regex of this file
23/// &Regex CANNOT be nil
24#[derive(Debug)]
25struct File(
26    PathBuf,
27    Option<&'static Regex>,
28    Option<&'static (Regex, Regex)>,
29);
30
31impl File {
32    /// Return string of file path
33    fn to_string(&self) -> String {
34        self.0.as_os_str().to_os_string().into_string().unwrap()
35    }
36}
37
38type Files = Vec<File>;
39
40/// loop all string inside paths_or_files, if it is file, store it, if it is dir
41/// store all files inside thsi dir (recursivly)
42fn files_in_dir_or_file_vec(paths_or_files: &[impl AsRef<Path>], conf: &Config) -> Result<Files> {
43    let mut result: Files = vec![];
44    for ele in paths_or_files {
45        if ele.as_ref().is_dir() {
46            result.append(&mut all_files_in_dir(ele, conf)?)
47        } else {
48            file_checker(
49                &mut result,
50                ele.as_ref(),
51                &conf.filetypes,
52                conf.filetypes.len(),
53                true,
54            )
55        }
56    }
57    Ok(result)
58}
59
60/// Find all files in this dir recursivly
61fn all_files_in_dir<T>(p: T, conf: &Config) -> Result<Files>
62where
63    T: AsRef<Path>,
64{
65    let mut result = vec![];
66    let (mut files, dirs) = files_and_dirs_in_path(p, &conf)?;
67    result.append(&mut files);
68
69    if dirs.len() != 0 {
70        result.append(
71            &mut dirs
72                .iter()
73                .map(|d| all_files_in_dir(d, conf).unwrap())
74                .flatten()
75                .collect::<Files>(),
76        )
77    }
78
79    Ok(result)
80}
81
82/// Find files and dirs in this folder
83fn files_and_dirs_in_path(p: impl AsRef<Path>, conf: &Config) -> Result<(Files, Dirs)> {
84    let (mut f, mut d): (Files, Dirs) = (vec![], vec![]);
85
86    // get filetypes
87    let filetypes = &conf.filetypes;
88    let filetypes_count = filetypes.len();
89
90    // get ignore dirs
91    let ignore_dirs = &conf.ignore_dirs;
92    let ignore_dirs_count = ignore_dirs.len();
93
94    for entry in read_dir(p)? {
95        let dir = entry?;
96        let path = dir.path();
97
98        if path.is_dir() {
99            // check ignore dirs
100            if ignore_dirs_count != 0 {
101                if let Some(d_name) = path.file_name() {
102                    if !ignore_dirs.contains(&d_name.to_os_string()) {
103                        d.push(path)
104                    }
105                }
106            } else {
107                d.push(path)
108            }
109        } else {
110            file_checker(&mut f, &path, &filetypes, filetypes_count, false)
111        }
112    }
113    Ok((f, d))
114}
115
116/// if file path pass check, add it to files
117fn file_checker(
118    files: &mut Files,
119    path: &Path,
120    filetypes: &[OsString],
121    filetypes_count: usize,
122    is_explicit: bool,
123) {
124    let ext = path.extension();
125    let file_name = path.file_name();
126    let ext_str = ext.and_then(|t| t.to_str());
127    let file_name_str = file_name.and_then(|f| f.to_str()).map(|s| s.to_lowercase());
128
129    // check filetypes
130    if filetypes_count != 0 {
131        // special filetypes
132        if let Some(t) = ext {
133            // file has extension
134            if filetypes.contains(&t.to_os_string()) {
135                // this file include in filetypes
136                let aa = REGEX_TABLE.lock();
137                let bb = REGEX_TABLE_MUL.lock();
138
139                if let Some(t_str) = ext_str {
140                    let single_line_re = match aa.as_ref().unwrap().get(t_str) {
141                        Some(re) => unsafe { (re as *const Regex).as_ref() },
142                        _ => None,
143                    };
144
145                    let mul_line_re = match bb.as_ref().unwrap().get(t_str) {
146                        Some(re) => unsafe { (re as *const (Regex, Regex)).as_ref() },
147                        _ => None,
148                    };
149
150                    if single_line_re.is_some() || mul_line_re.is_some() {
151                        files.push(File(path.to_path_buf(), single_line_re, mul_line_re));
152                    }
153                }
154            }
155        }
156    } else {
157        let aa = REGEX_TABLE.lock();
158        let aa_guard = aa.as_ref().unwrap();
159
160        let bb = REGEX_TABLE_MUL.lock();
161        let bb_guard = bb.as_ref().unwrap();
162
163        // 1. Try extension
164        if let Some(t_str) = ext_str {
165            let single_line_re = match aa_guard.get(t_str) {
166                Some(re) => unsafe { (re as *const Regex).as_ref() },
167                None => None,
168            };
169
170            let mul_line_re = match bb_guard.get(t_str) {
171                Some(re) => unsafe { (re as *const (Regex, Regex)).as_ref() },
172                None => None,
173            };
174
175            if single_line_re.is_some() || mul_line_re.is_some() {
176                files.push(File(path.to_path_buf(), single_line_re, mul_line_re));
177                return;
178            }
179        }
180
181        // 2. Try filename (lowercase)
182        if let Some(ref name) = file_name_str {
183            let single_line_re = match aa_guard.get(name) {
184                Some(re) => unsafe { (re as *const Regex).as_ref() },
185                None => None,
186            };
187
188            let mul_line_re = match bb_guard.get(name) {
189                Some(re) => unsafe { (re as *const (Regex, Regex)).as_ref() },
190                None => None,
191            };
192
193            if single_line_re.is_some() || mul_line_re.is_some() {
194                files.push(File(path.to_path_buf(), single_line_re, mul_line_re));
195                return;
196            }
197        }
198
199        // 3. Fallback for explicit targets
200        if is_explicit {
201            let re = unsafe {
202                match (&*FALLBACK_REGEX as *const Regex).as_ref() {
203                    Some(a) => a,
204                    None => return,
205                }
206            };
207            files.push(File(path.to_path_buf(), Some(re), None));
208        }
209    }
210}
211
212/// The status pass to filter_line
213enum FilterLineStatus {
214    /// default, without any previous status
215    /// with the one line regex and the mutliline regex start and end
216    None,
217
218    /// in multiple line comment, everything is comment
219    InMulLine,
220}
221
222struct FilterLiner<'this_file> {
223    regex_single_line: Option<&'this_file Regex>,
224
225    regex_multiple_line: Option<&'this_file (Regex, Regex)>,
226
227    status: FilterLineStatus,
228}
229
230impl<'this_file> FilterLiner<'this_file> {
231    fn filter_line(&mut self, line: &str, line_num: usize) -> Option<Crumb> {
232        match self.status {
233            FilterLineStatus::None => {
234                // multi line first
235                if let Some(aa) = self.regex_multiple_line {
236                    match aa.0.find(line) {
237                        Some(mat) => {
238                            let position = mat.start();
239                            let cap = aa.0.captures(line).unwrap();
240                            let content = cap[2].to_string();
241                            let comment_symbol_header = cap[1].to_string();
242                            let mut res = if content.starts_with('!') {
243                                Crumb::new(
244                                    line_num,
245                                    position,
246                                    content,
247                                    comment_symbol_header,
248                                    String::new(),
249                                )
250                                .add_ignore_flag()
251                            } else {
252                                Crumb::new(
253                                    line_num,
254                                    position,
255                                    content,
256                                    comment_symbol_header,
257                                    String::new(),
258                                )
259                            };
260
261                            // crumb will have the tail
262                            res.has_tail = true;
263
264                            // update to in mul lines
265                            self.status = FilterLineStatus::InMulLine;
266
267                            return Some(res);
268                        }
269                        None => (),
270                    }
271                }
272
273                if let Some(bb) = self.regex_single_line {
274                    match bb.find(line) {
275                        Some(mat) => {
276                            let position = mat.start();
277                            let cap = bb.captures(line).unwrap();
278                            let content = cap[2].to_string();
279                            let comment_symbol_header = cap[1].to_string();
280                            let res = if content.starts_with('!') {
281                                Crumb::new(
282                                    line_num,
283                                    position,
284                                    content,
285                                    comment_symbol_header,
286                                    String::new(),
287                                )
288                                .add_ignore_flag()
289                            } else {
290                                Crumb::new(
291                                    line_num,
292                                    position,
293                                    content,
294                                    comment_symbol_header,
295                                    String::new(),
296                                )
297                            };
298
299                            return Some(res);
300                        }
301                        None => (),
302                    }
303                }
304                return None;
305            }
306            FilterLineStatus::InMulLine => {
307                let aa = self.regex_multiple_line.unwrap();
308                if let Some(_mat) = aa.1.find(line) {
309                    self.status = FilterLineStatus::None;
310                    let cap = aa.1.captures(line).unwrap();
311                    let raw_content = &cap[1];
312                    let content = raw_content
313                        .trim_start()
314                        .trim_end_matches(['\r', '\n'])
315                        .to_string();
316                    let position = line.len() - line.trim_start().len();
317                    let comment_symbol_endding = cap[2].to_string();
318                    let cr = Crumb::new(
319                        line_num,
320                        position,
321                        content,
322                        String::new(),
323                        comment_symbol_endding,
324                    );
325                    Some(cr)
326                } else {
327                    let content = line.trim_start().trim_end_matches(['\r', '\n']);
328                    let position = line.len() - line.trim_start().len();
329                    let mut cr = Crumb::new(
330                        line_num,
331                        position,
332                        content.to_string(),
333                        String::new(),
334                        String::new(),
335                    );
336                    cr.has_tail = true;
337                    Some(cr)
338                }
339            }
340        }
341    }
342}
343
344/// Operate this file
345fn op_file(file: File, kwreg: &Option<Regex>, conf: Arc<RwLock<Config>>) -> Result<Option<Bread>> {
346    let breads = match bake_bread(&file, kwreg, &conf.read().unwrap()) {
347        Ok(b) => b,
348        Err(e) => {
349            debug!("file {} had error {}", file.to_string(), e.to_string());
350            return Ok(None);
351        }
352    };
353
354    if !conf.read().unwrap().delete {
355        Ok(breads)
356    } else {
357        match breads {
358            Some(bb) => {
359                delete_the_crumbs(bb)?;
360                Ok(None)
361            }
362            None => Ok(None),
363        }
364    }
365}
366
367/// Make bread for this file
368/// Major logic inside this function
369fn bake_bread(file: &File, kwreg: &Option<Regex>, conf: &Config) -> Result<Option<Bread>> {
370    // start to read file
371    let mut buf = vec![];
372    let file_p = file.to_string();
373    let mut f: std::fs::File = std::fs::File::open(file.0.clone())?;
374    f.read_to_end(&mut buf)?;
375
376    let mut line_num = 0;
377    let mut ss = String::new(); // temp
378    let mut buf = buf.as_slice();
379    let mut result = vec![];
380    let mut head: Option<Crumb> = None; // for tail support
381    let mut shadow_file = vec![]; // the copy of file for later range operation 
382
383    // closure for keywords feature
384    let mut keyword_checker_and_push = |mut cb: Crumb| {
385        cb.has_tail = false;
386        if kwreg.is_some() {
387            // filter_keywords will update keyword even the crumb is ignored
388            if cb.filter_keywords(kwreg.as_ref().unwrap()) {
389                result.push(cb)
390            }
391        } else {
392            if !cb.is_ignore() || conf.show_ignored {
393                result.push(cb)
394            }
395        }
396    };
397
398    // make the new filter
399    let mut fl = FilterLiner {
400        regex_single_line: file.1,
401        regex_multiple_line: file.2,
402        status: FilterLineStatus::None,
403    };
404
405    loop {
406        line_num += 1;
407        match buf.read_line(&mut ss) {
408            Ok(0) => {
409                if head.is_some() {
410                    keyword_checker_and_push(head.unwrap());
411                }
412                break;
413            }
414            Err(e) => {
415                eprintln!(
416                    "Warning: file {} had read error at line {}: {}",
417                    file_p, line_num, e
418                );
419                if head.is_some() {
420                    keyword_checker_and_push(head.unwrap());
421                }
422                break;
423            }
424            Ok(_) => match fl.filter_line(&ss, line_num) {
425                Some(mut cb) => {
426                    // check head first
427                    match head {
428                        Some(ref mut h) => {
429                            if h.has_tail() {
430                                // if head has tail, add this line to head, continue
431                                h.add_tail(cb);
432                                ss.clear(); // before continue, clear temp
433                                continue;
434                            } else {
435                                // store head
436                                keyword_checker_and_push(head.unwrap());
437                                head = None;
438                            }
439                        }
440                        None => (),
441                    }
442
443                    if cb.has_tail() {
444                        // make new head
445                        head = Some(cb);
446                    } else {
447                        // store result
448                        keyword_checker_and_push(cb)
449                    }
450                }
451                None => {
452                    if head.is_some() {
453                        keyword_checker_and_push(head.unwrap());
454                        head = None;
455                    }
456                }
457            },
458        }
459
460        if conf.range > 0 {
461            shadow_file.push(ss.clone());
462        }
463
464        ss.clear()
465    }
466
467    // if range not equal 0, start to push the context around inside
468    if conf.range > 0 {
469        result.iter_mut().for_each(|crumb| {
470            let ahead_ind = (crumb.line_num - 1).saturating_sub(conf.range as usize);
471            let tail_ind = (crumb.line_num - 1)
472                .saturating_add(conf.range as usize)
473                .min(shadow_file.len());
474            crumb.range_content = Some(
475                (ahead_ind + 1..tail_ind + 1)
476                    .zip(
477                        shadow_file
478                            .get(ahead_ind..tail_ind)
479                            .map(|x| x.to_vec())
480                            .unwrap(),
481                    )
482                    .collect(),
483            );
484        });
485    }
486
487    if result.len() == 0 {
488        Ok(None)
489    } else {
490        Ok(Some(Bread::new(file_p, result)))
491    }
492}
493
494/// delete crumbs and re-write the file
495pub fn delete_the_crumbs(Bread { file_path, crumbs }: Bread) -> Result<String> {
496    let all_delete_line_postion_pairs = crumbs
497        .iter()
498        .map(|crumb| crumb.all_lines_num_postion_pair())
499        .flatten();
500
501    delete_lines_on(&file_path, all_delete_line_postion_pairs)?;
502
503    println!("deleted the crumbs in {}", file_path);
504    Ok(file_path)
505}
506
507/// delete crumbs by special indexes
508pub fn delete_the_crumbs_on_special_index(
509    Bread { file_path, crumbs }: Bread,
510    indexes: HashSet<usize>,
511) -> Result<String> {
512    let mut all_delete_lines = vec![];
513    for ind in &indexes {
514        match crumbs.get(*ind) {
515            Some(c) => all_delete_lines.append(&mut c.all_lines_num_postion_pair()),
516            None => return Err(io::Error::other("cannot find crumb index in bread")),
517        }
518    }
519
520    delete_lines_on(&file_path, all_delete_lines.into_iter())?;
521
522    println!("deleted {} crumbs in {}", indexes.len(), file_path);
523
524    Ok(file_path)
525}
526
527fn write_file_atomically(file_path: &str, lines: &[Vec<u8>]) -> Result<()> {
528    let temp_path = format!("{}.tmp", file_path);
529    let write_res = (|| {
530        let mut temp_file = OpenOptions::new()
531            .create(true)
532            .write(true)
533            .truncate(true)
534            .open(&temp_path)?;
535        for line in lines {
536            temp_file.write_all(line)?;
537            temp_file.write_all(b"\n")?;
538        }
539        Ok(())
540    })();
541
542    if let Err(e) = write_res {
543        let _ = fs::remove_file(&temp_path);
544        return Err(e);
545    }
546
547    if let Err(e) = fs::rename(&temp_path, file_path) {
548        let _ = fs::remove_file(&temp_path);
549        return Err(e);
550    }
551
552    Ok(())
553}
554
555/// delete special lines of the file on file_path
556fn delete_lines_on(
557    file_path: &str,
558    line_num_pos_pairs: impl Iterator<Item = (usize, usize)>,
559) -> Result<()> {
560    let f = fs::File::open(&file_path)?;
561    let reader = BufReader::new(f).lines();
562
563    let all_delete_lines = line_num_pos_pairs.collect();
564
565    let finish_deleted = delete_nth_lines(reader, all_delete_lines)?
566        .into_iter()
567        .map(|line| line.into_bytes())
568        .collect::<Vec<_>>();
569
570    write_file_atomically(file_path, &finish_deleted)
571}
572
573/// delete crumbs of file, return the new file contents without the crumbs deleted
574fn delete_nth_lines(
575    f: impl Iterator<Item = Result<String>>,
576    nm: HashMap<usize, usize>,
577) -> Result<Vec<String>> {
578    let mut result = vec![];
579
580    for (line_num, ll) in f.enumerate() {
581        if nm.contains_key(&(line_num + 1)) {
582            let mut new_l = ll?;
583            new_l.truncate(*nm.get(&(line_num + 1)).unwrap());
584            if new_l == "" {
585                // empty line just skip
586                continue;
587            }
588            result.push(new_l);
589        } else {
590            result.push(ll?);
591        }
592    }
593
594    Ok(result)
595}
596
597/// restore the bread's crumb to normal comment
598pub fn restore_the_crumb(Bread { file_path, crumbs }: Bread) -> Result<String> {
599    let all_restore_lines = crumbs
600        .iter()
601        .map(|c| c.all_lines_num_postion_and_header_content())
602        .flatten();
603
604    restore_lines_on(&file_path, all_restore_lines)?;
605
606    println!("restored the crumbs in {}", file_path);
607    Ok(file_path)
608}
609
610/// restore the bread's crumb by special indexes
611pub fn restore_the_crumb_on_special_index(
612    Bread { file_path, crumbs }: Bread,
613    indexes: HashSet<usize>,
614) -> Result<String> {
615    let mut all_restore_lines = Vec::with_capacity(indexes.len());
616    for ind in &indexes {
617        match crumbs.get(*ind) {
618            Some(c) => all_restore_lines.append(&mut c.all_lines_num_postion_and_header_content()),
619            None => return Err(io::Error::other("cannot find crumb index in bread")),
620        }
621    }
622
623    restore_lines_on(&file_path, all_restore_lines.into_iter())?;
624
625    println!("restored {} crumbs in {}", indexes.len(), file_path);
626    Ok(file_path)
627}
628
629fn restore_lines_on<'a>(
630    file_path: &'a str,
631    all_restore_lines: impl Iterator<Item = (usize, usize, &'a str, &'a str, &'a str)>,
632) -> Result<()> {
633    let f = fs::File::open(&file_path)?;
634    let reader = BufReader::new(f).lines();
635
636    let mut table: HashMap<usize, (usize, &str, &str, &str)> =
637        HashMap::with_capacity(all_restore_lines.size_hint().1.unwrap_or(0));
638
639    all_restore_lines.for_each(|(line_num, pos, header, endding, content)| {
640        table.insert(line_num, (pos, header, content, endding));
641    });
642
643    let mut new_file = Vec::with_capacity(reader.size_hint().1.unwrap_or(0));
644    for (line_num, ll) in reader.enumerate() {
645        if let Some((pos, header, content, endding)) = table.get(&(line_num + 1)) {
646            let mut new_l = ll?;
647            new_l.truncate(*pos);
648            new_l.push_str(*header);
649            if *header != "" {
650                new_l.push_str(" ");
651            }
652            new_l.push_str(*content);
653            new_l.push_str(*endding);
654
655            new_file.push(new_l.into_bytes())
656        } else {
657            new_file.push(ll?.into_bytes());
658        }
659    }
660
661    write_file_atomically(file_path, &new_file)
662}
663
664/// run format command with filepath input
665pub fn run_format_command_to_file(
666    fmt_command: &str,
667    _files: impl IntoIterator<Item = String>,
668) -> std::result::Result<(), String> {
669    let mut command_splits = fmt_command.split(' ');
670    let first = command_splits
671        .next()
672        .ok_or("fmt_command cannot be empty".to_string())?;
673
674    let mut comm = Command::new(first);
675    let mut child = comm
676        .args(command_splits)
677        .spawn()
678        .expect("Cannot run the fmt_command");
679
680    println!("running fmt command: {}", fmt_command);
681    child
682        .wait()
683        .expect("fmt command wasn't running")
684        .exit_ok()
685        .map_err(|e| e.to_string())
686}
687
688/// entry function of main logic
689pub fn handle_files(conf: Config) -> impl Iterator<Item = Bread> {
690    // first add all files in arguments
691    let mut all_files: Vec<File> = files_in_dir_or_file_vec(&conf.files, &conf).unwrap();
692
693    // split to groups
694    let threads_num: usize = thread::available_parallelism()
695        .unwrap_or(THREAD_NUM.unwrap())
696        .into();
697
698    let len = all_files.len();
699    let count = len / threads_num;
700    let mut groups: Vec<Vec<File>> = vec![];
701    for _ in 0..threads_num - 1 {
702        groups.push(all_files.drain(0..count).collect())
703    }
704    groups.push(all_files.drain(0..).collect());
705
706    let conf = Arc::new(RwLock::new(conf));
707    groups
708        .into_iter()
709        .map(move |fs| {
710            let kwreg = KEYWORDS_REGEX.lock().unwrap().clone();
711            let conf_c = Arc::clone(&conf);
712            thread::spawn(|| {
713                fs.into_iter()
714                    .filter_map(move |f| op_file(f, &kwreg, conf_c.clone()).unwrap())
715                    .collect::<Vec<Bread>>()
716            })
717        })
718        .map(|han| han.join().unwrap())
719        .flatten()
720}
721
722#[cfg(test)]
723mod tests {
724    use super::*;
725
726    #[test]
727    fn test_files_and_dirs_in_path() -> Result<()> {
728        let (fs, dirs) = files_and_dirs_in_path("./tests/testcases", &Default::default())?;
729
730        assert_eq!(dirs.len(), 0);
731        assert_eq!(fs[0].0, PathBuf::from("./tests/testcases/multilines.rs"),);
732        Ok(())
733    }
734
735    // #[test]
736    // fn test_available_parallelism_on_my_machine() {
737    //     dbg!(thread::available_parallelism().unwrap());
738    // }
739}