Skip to main content

code_it_later_rs/
fs_operation.rs

1use super::config::{Config, FALLBACK_REGEX, 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                true,
48            )
49        }
50    }
51    Ok(result)
52}
53
54/// Find all files in this dir recursivly
55fn all_files_in_dir<T>(p: T, conf: &Config) -> Result<Files>
56where
57    T: AsRef<Path>,
58{
59    let mut result = vec![];
60    let (mut files, dirs) = files_and_dirs_in_path(p, &conf)?;
61    result.append(&mut files);
62
63    if dirs.len() != 0 {
64        result.append(
65            &mut dirs
66                .iter()
67                .map(|d| all_files_in_dir(d, conf).unwrap())
68                .flatten()
69                .collect::<Files>(),
70        )
71    }
72
73    Ok(result)
74}
75
76/// Find files and dirs in this folder
77fn files_and_dirs_in_path(p: impl AsRef<Path>, conf: &Config) -> Result<(Files, Dirs)> {
78    let (mut f, mut d): (Files, Dirs) = (vec![], vec![]);
79
80    // get filetypes
81    let filetypes = &conf.filetypes;
82    let filetypes_count = filetypes.len();
83
84    // get ignore dirs
85    let ignore_dirs = &conf.ignore_dirs;
86    let ignore_dirs_count = ignore_dirs.len();
87
88    for entry in read_dir(p)? {
89        let dir = entry?;
90        let path = dir.path();
91
92        if path.is_dir() {
93            // check ignore dirs
94            if ignore_dirs_count != 0 {
95                if let Some(d_name) = path.file_name() {
96                    if !ignore_dirs.contains(&d_name.to_os_string()) {
97                        d.push(path)
98                    }
99                }
100            } else {
101                d.push(path)
102            }
103        } else {
104            file_checker(&mut f, &path, &filetypes, filetypes_count, false)
105        }
106    }
107    Ok((f, d))
108}
109
110/// if file path pass check, add it to files
111fn file_checker(
112    files: &mut Files,
113    path: &Path,
114    filetypes: &[OsString],
115    filetypes_count: usize,
116    is_explicit: bool,
117) {
118    let ext = path.extension();
119    let file_name = path.file_name();
120    let ext_str = ext.and_then(|t| t.to_str());
121    let file_name_str = file_name.and_then(|f| f.to_str()).map(|s| s.to_lowercase());
122
123    // check filetypes
124    if filetypes_count != 0 {
125        // special filetypes
126        if let Some(t) = ext {
127            // file has extension
128            if filetypes.contains(&t.to_os_string()) {
129                // this file include in filetypes
130                let aa = REGEX_TABLE.lock();
131                if let Some(t_str) = ext_str {
132                    match aa.as_ref().unwrap().get(t_str) {
133                        Some(re) => {
134                            // and has regex for this type
135                            let re = unsafe {
136                                match (re as *const Regex).clone().as_ref() {
137                                    Some(a) => a,
138                                    None => return,
139                                }
140                            };
141                            files.push(File(path.to_path_buf(), re))
142                        }
143                        _ => {}
144                    }
145                }
146            }
147        }
148    } else {
149        let aa = REGEX_TABLE.lock();
150        let guard = aa.as_ref().unwrap();
151
152        // 1. Try extension
153        if let Some(t_str) = ext_str {
154            if let Some(re) = guard.get(t_str) {
155                let re = unsafe {
156                    match (re as *const Regex).clone().as_ref() {
157                        Some(a) => a,
158                        None => return,
159                    }
160                };
161                files.push(File(path.to_path_buf(), re));
162                return;
163            }
164        }
165
166        // 2. Try filename (lowercase)
167        if let Some(ref name) = file_name_str {
168            if let Some(re) = guard.get(name) {
169                let re = unsafe {
170                    match (re as *const Regex).clone().as_ref() {
171                        Some(a) => a,
172                        None => return,
173                    }
174                };
175                files.push(File(path.to_path_buf(), re));
176                return;
177            }
178        }
179
180        // 3. Fallback for explicit targets
181        if is_explicit {
182            let re = unsafe {
183                match (&*FALLBACK_REGEX as *const Regex).as_ref() {
184                    Some(a) => a,
185                    None => return,
186                }
187            };
188            files.push(File(path.to_path_buf(), re));
189        }
190    }
191}
192
193/// Filter this line
194fn filter_line(line: &str, line_num: usize, re: &Regex) -> Option<Crumb> {
195    match re.find(line) {
196        Some(mat) => {
197            let position = mat.start();
198            let cap = re.captures(line).unwrap();
199            let content = cap[2].to_string();
200            let comment_symbol_header = cap[1].to_string();
201            if content.starts_with('!') {
202                Some(
203                    Crumb::new(line_num, position, content, comment_symbol_header)
204                        .add_ignore_flag(),
205                )
206            } else {
207                Some(Crumb::new(
208                    line_num,
209                    position,
210                    content,
211                    comment_symbol_header,
212                ))
213            }
214        }
215        None => None,
216    }
217}
218
219/// Operate this file
220fn op_file(file: File, kwreg: &Option<Regex>, conf: Arc<RwLock<Config>>) -> Result<Option<Bread>> {
221    let breads = match bake_bread(&file, kwreg, &conf.read().unwrap()) {
222        Ok(b) => b,
223        Err(e) => {
224            debug!("file {} had error {}", file.to_string(), e.to_string());
225            return Ok(None);
226        }
227    };
228
229    if !conf.read().unwrap().delete {
230        Ok(breads)
231    } else {
232        match breads {
233            Some(bb) => {
234                delete_the_crumbs(bb)?;
235                Ok(None)
236            }
237            None => Ok(None),
238        }
239    }
240}
241
242/// make bread for this file
243fn bake_bread(file: &File, kwreg: &Option<Regex>, conf: &Config) -> Result<Option<Bread>> {
244    // start to read file
245    let mut buf = vec![];
246    let file_p = file.to_string();
247    let mut f: std::fs::File = std::fs::File::open(file.0.clone())?;
248    f.read_to_end(&mut buf)?;
249
250    let mut line_num = 0;
251    let mut ss = String::new(); // temp
252    let mut buf = buf.as_slice();
253    let mut result = vec![];
254    let mut head: Option<Crumb> = None; // for tail support
255    let mut shadow_file = vec![]; // the copy of file for later range operation 
256
257    // closure for keywords feature
258    let mut keyword_checker_and_push = |mut cb: Crumb| {
259        if kwreg.is_some() {
260            // filter_keywords will update keyword even the crumb is ignored
261            if cb.filter_keywords(kwreg.as_ref().unwrap()) {
262                result.push(cb)
263            }
264        } else {
265            if !cb.is_ignore() || conf.show_ignored {
266                result.push(cb)
267            }
268        }
269    };
270
271    loop {
272        line_num += 1;
273        match buf.read_line(&mut ss) {
274            Ok(0) => {
275                if head.is_some() {
276                    keyword_checker_and_push(head.unwrap());
277                }
278                break;
279            }
280            Err(e) => {
281                eprintln!(
282                    "Warning: file {} had read error at line {}: {}",
283                    file_p, line_num, e
284                );
285                if head.is_some() {
286                    keyword_checker_and_push(head.unwrap());
287                }
288                break;
289            }
290            Ok(_) => match filter_line(&ss, line_num, file.1) {
291                Some(cb) => {
292                    // check head first
293                    match head {
294                        Some(ref mut h) => {
295                            if h.has_tail() {
296                                // if head has tail, add this line to head, continue
297                                h.add_tail(cb);
298                                ss.clear(); // before continue, clear temp
299                                continue;
300                            } else {
301                                // store head
302                                keyword_checker_and_push(head.unwrap());
303                                head = None;
304                            }
305                        }
306                        None => (),
307                    }
308
309                    if cb.has_tail() {
310                        // make new head
311                        head = Some(cb);
312                    } else {
313                        // store result
314                        keyword_checker_and_push(cb)
315                    }
316                }
317                None => {
318                    if head.is_some() {
319                        keyword_checker_and_push(head.unwrap());
320                        head = None;
321                    }
322                }
323            },
324        }
325
326        if conf.range > 0 {
327            shadow_file.push(ss.clone());
328        }
329
330        ss.clear()
331    }
332
333    // if range not equal 0, start to push the context around inside
334    if conf.range > 0 {
335        result.iter_mut().for_each(|crumb| {
336            let ahead_ind = (crumb.line_num - 1).saturating_sub(conf.range as usize);
337            let tail_ind = (crumb.line_num - 1)
338                .saturating_add(conf.range as usize)
339                .min(shadow_file.len());
340            crumb.range_content = Some(
341                (ahead_ind + 1..tail_ind + 1)
342                    .zip(
343                        shadow_file
344                            .get(ahead_ind..tail_ind)
345                            .map(|x| x.to_vec())
346                            .unwrap(),
347                    )
348                    .collect(),
349            );
350        });
351    }
352
353    if result.len() == 0 {
354        Ok(None)
355    } else {
356        Ok(Some(Bread::new(file_p, result)))
357    }
358}
359
360/// delete crumbs and re-write the file
361pub fn delete_the_crumbs(Bread { file_path, crumbs }: Bread) -> Result<String> {
362    let all_delete_line_postion_pairs = crumbs
363        .iter()
364        .map(|crumb| crumb.all_lines_num_postion_pair())
365        .flatten();
366
367    delete_lines_on(&file_path, all_delete_line_postion_pairs)?;
368
369    println!("deleted the crumbs in {}", file_path);
370    Ok(file_path)
371}
372
373/// delete crumbs by special indexes
374pub fn delete_the_crumbs_on_special_index(
375    Bread { file_path, crumbs }: Bread,
376    indexes: HashSet<usize>,
377) -> Result<String> {
378    let mut all_delete_lines = vec![];
379    for ind in &indexes {
380        match crumbs.get(*ind) {
381            Some(c) => all_delete_lines.append(&mut c.all_lines_num_postion_pair()),
382            None => return Err(io::Error::other("cannot find crumb index in bread")),
383        }
384    }
385
386    delete_lines_on(&file_path, all_delete_lines.into_iter())?;
387
388    println!("deleted {} crumbs in {}", indexes.len(), file_path);
389
390    Ok(file_path)
391}
392
393fn write_file_atomically(file_path: &str, lines: &[Vec<u8>]) -> Result<()> {
394    let temp_path = format!("{}.tmp", file_path);
395    let write_res = (|| {
396        let mut temp_file = OpenOptions::new()
397            .create(true)
398            .write(true)
399            .truncate(true)
400            .open(&temp_path)?;
401        for line in lines {
402            temp_file.write_all(line)?;
403            temp_file.write_all(b"\n")?;
404        }
405        Ok(())
406    })();
407
408    if let Err(e) = write_res {
409        let _ = fs::remove_file(&temp_path);
410        return Err(e);
411    }
412
413    if let Err(e) = fs::rename(&temp_path, file_path) {
414        let _ = fs::remove_file(&temp_path);
415        return Err(e);
416    }
417
418    Ok(())
419}
420
421/// delete special lines of the file on file_path
422fn delete_lines_on(
423    file_path: &str,
424    line_num_pos_pairs: impl Iterator<Item = (usize, usize)>,
425) -> Result<()> {
426    let f = fs::File::open(&file_path)?;
427    let reader = BufReader::new(f).lines();
428
429    let all_delete_lines = line_num_pos_pairs.collect();
430
431    let finish_deleted = delete_nth_lines(reader, all_delete_lines)?
432        .into_iter()
433        .map(|line| line.into_bytes())
434        .collect::<Vec<_>>();
435
436    write_file_atomically(file_path, &finish_deleted)
437}
438
439/// delete crumbs of file, return the new file contents without the crumbs deleted
440fn delete_nth_lines(
441    f: impl Iterator<Item = Result<String>>,
442    nm: HashMap<usize, usize>,
443) -> Result<Vec<String>> {
444    let mut result = vec![];
445
446    for (line_num, ll) in f.enumerate() {
447        if nm.contains_key(&(line_num + 1)) {
448            let mut new_l = ll?;
449            new_l.truncate(*nm.get(&(line_num + 1)).unwrap());
450            if new_l == "" {
451                // empty line just skip
452                continue;
453            }
454            result.push(new_l);
455        } else {
456            result.push(ll?);
457        }
458    }
459
460    Ok(result)
461}
462
463/// restore the bread's crumb to normal comment
464pub fn restore_the_crumb(Bread { file_path, crumbs }: Bread) -> Result<String> {
465    let all_restore_lines = crumbs
466        .iter()
467        .map(|c| c.all_lines_num_postion_and_header_content())
468        .flatten();
469
470    restore_lines_on(&file_path, all_restore_lines)?;
471
472    println!("restored the crumbs in {}", file_path);
473    Ok(file_path)
474}
475
476/// restore the bread's crumb by special indexes
477pub fn restore_the_crumb_on_special_index(
478    Bread { file_path, crumbs }: Bread,
479    indexes: HashSet<usize>,
480) -> Result<String> {
481    let mut all_restore_lines = Vec::with_capacity(indexes.len());
482    for ind in &indexes {
483        match crumbs.get(*ind) {
484            Some(c) => all_restore_lines.append(&mut c.all_lines_num_postion_and_header_content()),
485            None => return Err(io::Error::other("cannot find crumb index in bread")),
486        }
487    }
488
489    restore_lines_on(&file_path, all_restore_lines.into_iter())?;
490
491    println!("restored {} crumbs in {}", indexes.len(), file_path);
492    Ok(file_path)
493}
494
495fn restore_lines_on<'a>(
496    file_path: &'a str,
497    all_restore_lines: impl Iterator<Item = (usize, usize, &'a str, &'a str)>,
498) -> Result<()> {
499    let f = fs::File::open(&file_path)?;
500    let reader = BufReader::new(f).lines();
501
502    let mut table: HashMap<usize, (usize, &str, &str)> =
503        HashMap::with_capacity(all_restore_lines.size_hint().1.unwrap_or(0));
504
505    all_restore_lines.for_each(|(line_num, pos, header, content)| {
506        table.insert(line_num, (pos, header, content));
507    });
508
509    let mut new_file = Vec::with_capacity(reader.size_hint().1.unwrap_or(0));
510    for (line_num, ll) in reader.enumerate() {
511        if let Some((pos, header, content)) = table.get(&(line_num + 1)) {
512            let mut new_l = ll?;
513            new_l.truncate(*pos);
514            new_l.push_str(*header);
515            new_l.push_str(" ");
516            new_l.push_str(*content);
517
518            new_file.push(new_l.into_bytes())
519        } else {
520            new_file.push(ll?.into_bytes());
521        }
522    }
523
524    write_file_atomically(file_path, &new_file)
525}
526
527/// run format command with filepath input
528pub fn run_format_command_to_file(
529    fmt_command: &str,
530    _files: impl IntoIterator<Item = String>,
531) -> std::result::Result<(), String> {
532    let mut command_splits = fmt_command.split(' ');
533    let first = command_splits
534        .next()
535        .ok_or("fmt_command cannot be empty".to_string())?;
536
537    let mut comm = Command::new(first);
538    let mut child = comm
539        .args(command_splits)
540        .spawn()
541        .expect("Cannot run the fmt_command");
542
543    println!("running fmt command: {}", fmt_command);
544    child
545        .wait()
546        .expect("fmt command wasn't running")
547        .exit_ok()
548        .map_err(|e| e.to_string())
549}
550
551/// entry function of main logic
552pub fn handle_files(conf: Config) -> impl Iterator<Item = Bread> {
553    // first add all files in arguments
554    let mut all_files: Vec<File> = files_in_dir_or_file_vec(&conf.files, &conf).unwrap();
555
556    // split to groups
557    let threads_num: usize = thread::available_parallelism()
558        .unwrap_or(THREAD_NUM.unwrap())
559        .into();
560
561    let len = all_files.len();
562    let count = len / threads_num;
563    let mut groups: Vec<Vec<File>> = vec![];
564    for _ in 0..threads_num - 1 {
565        groups.push(all_files.drain(0..count).collect())
566    }
567    groups.push(all_files.drain(0..).collect());
568
569    let conf = Arc::new(RwLock::new(conf));
570    groups
571        .into_iter()
572        .map(move |fs| {
573            let kwreg = KEYWORDS_REGEX.lock().unwrap().clone();
574            let conf_c = Arc::clone(&conf);
575            thread::spawn(|| {
576                fs.into_iter()
577                    .filter_map(move |f| op_file(f, &kwreg, conf_c.clone()).unwrap())
578                    .collect::<Vec<Bread>>()
579            })
580        })
581        .map(|han| han.join().unwrap())
582        .flatten()
583}
584
585#[cfg(test)]
586mod tests {
587    use super::*;
588
589    #[test]
590    fn test_files_and_dirs_in_path() -> Result<()> {
591        let (fs, dirs) = files_and_dirs_in_path("./tests/testcases", &Default::default())?;
592
593        assert_eq!(dirs.len(), 0);
594        assert_eq!(fs[0].0, PathBuf::from("./tests/testcases/multilines.rs"),);
595        Ok(())
596    }
597
598    // #[test]
599    // fn test_available_parallelism_on_my_machine() {
600    //     dbg!(thread::available_parallelism().unwrap());
601    // }
602}