use std::path::Path;
use serde::{Deserialize, Serialize};
use tokei::{Config as TokeiConfig, LanguageType, Languages};
use crate::core::config::Config;
use crate::observer::{ObservationMeta, Observer};
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct LineCounts {
pub code: usize,
pub comments: usize,
pub blanks: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct LanguageStats {
pub name: String,
pub files: usize,
#[serde(flatten)]
pub counts: LineCounts,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct LocReport {
pub languages: Vec<LanguageStats>,
pub primary: Option<String>,
pub totals: LineCounts,
}
impl LocReport {
#[must_use]
pub fn total_files(&self) -> usize {
self.languages.iter().map(|e| e.files).sum()
}
}
#[derive(Debug, Clone, Default)]
pub struct LocObserver {
pub excluded: Vec<String>,
pub exclude_languages: Vec<String>,
}
impl LocObserver {
#[must_use]
pub fn from_config(cfg: &Config) -> Self {
Self {
excluded: cfg.observer_excluded_paths(),
exclude_languages: Vec::new(),
}
}
pub fn scan(&self, root: &Path) -> LocReport {
let mut languages = Languages::new();
let excluded_refs: Vec<&str> = self.excluded.iter().map(String::as_str).collect();
let paths = [root];
languages.get_statistics(&paths, &excluded_refs, &TokeiConfig::default());
let mut entries = Vec::with_capacity(languages.len());
let mut totals = LineCounts::default();
for (lang_type, lang) in &languages {
if lang.reports.is_empty() {
continue;
}
let name = lang_type.name().to_string();
if self
.exclude_languages
.iter()
.any(|n| n.eq_ignore_ascii_case(&name))
{
continue;
}
totals.code += lang.code;
totals.comments += lang.comments;
totals.blanks += lang.blanks;
entries.push(LanguageStats {
name,
files: lang.reports.len(),
counts: LineCounts {
code: lang.code,
comments: lang.comments,
blanks: lang.blanks,
},
});
}
entries.sort_by(|a, b| b.counts.code.cmp(&a.counts.code).then(a.name.cmp(&b.name)));
let primary = entries
.iter()
.find(|e| !is_literate_name(&e.name))
.map(|e| e.name.clone());
LocReport {
languages: entries,
primary,
totals,
}
}
}
fn is_literate_name(name: &str) -> bool {
LanguageType::from_name(name).is_some_and(LanguageType::is_literate)
}
impl Observer for LocObserver {
type Output = LocReport;
fn meta(&self) -> ObservationMeta {
ObservationMeta {
name: "loc",
version: 1,
}
}
fn observe(&self, project_root: &Path) -> anyhow::Result<Self::Output> {
Ok(self.scan(project_root))
}
}