composition_cli/core/
get_composition.rs

1use rayon::prelude::*;
2use std::{collections::HashMap, fs};
3use walkdir::DirEntry;
4
5use crate::context::{AppContext, config::Tracked};
6
7#[derive(Debug)]
8pub struct CompositionEntry {
9    pub tracked: Tracked,
10    pub line_count: usize,
11    pub percentage: f32,
12}
13
14pub fn get_composition(app_context: &AppContext, entries: Vec<DirEntry>) -> Vec<CompositionEntry> {
15    // process files in parallel
16    let line_counts_by_extension: HashMap<String, usize> = entries
17        .par_iter()
18        .filter_map(|entry| {
19            let ext = entry.path().extension()?.to_str()?.to_lowercase();
20            let lines = count_lines(entry.path(), app_context, &ext)?;
21            Some((ext, lines))
22        })
23        .fold(
24            || HashMap::new(),
25            |mut map, (ext, lines)| {
26                *map.entry(ext).or_insert(0) += lines;
27                map
28            },
29        )
30        .reduce(
31            || HashMap::new(),
32            |mut map1, map2| {
33                for (ext, lines) in map2 {
34                    *map1.entry(ext).or_insert(0) += lines;
35                }
36                map1
37            },
38        );
39
40    let composition: Vec<CompositionEntry> = app_context
41        .config
42        .tracked
43        .iter()
44        .filter_map(|tracked| {
45            let total_lines: usize = tracked
46                .extensions
47                .iter()
48                .filter_map(|ext| line_counts_by_extension.get(ext))
49                .sum();
50
51            if total_lines == 0 {
52                return None;
53            }
54
55            Some(CompositionEntry {
56                tracked: Tracked {
57                    display: tracked.display.clone(),
58                    extensions: tracked.extensions.clone(),
59                    color: tracked.color.clone(),
60                    excluded_patterns: tracked.excluded_patterns.clone(),
61                    compiled_excluded_patterns: tracked.compiled_excluded_patterns.clone(),
62                },
63                line_count: total_lines,
64                percentage: 0.0,
65            })
66        })
67        .collect();
68
69    composition
70}
71
72fn count_lines(path: &std::path::Path, app_context: &AppContext, ext: &str) -> Option<usize> {
73    let content = fs::read_to_string(path).ok()?;
74
75    let tracked = app_context
76        .config
77        .tracked
78        .iter()
79        .find(|t| t.extensions.iter().any(|e| e == ext))?;
80
81    let count = content
82        .lines()
83        .filter(|line| {
84            if app_context.config.ignore_empty_lines && line.trim().is_empty() {
85                return false;
86            }
87
88            // global excluded patterns
89            for pattern in &app_context.config.compiled_excluded_patterns {
90                if pattern.is_match(line) {
91                    return false;
92                }
93            }
94
95            // language specific excluded patters
96            for pattern in &tracked.compiled_excluded_patterns {
97                if pattern.is_match(line) {
98                    return false;
99                }
100            }
101
102            true
103        })
104        .count();
105
106    Some(count)
107}