use std::collections::BTreeMap;
use std::fs;
use std::io;
use std::path::Path;
use serde::{Deserialize, Serialize};
use crate::hash::{ContentHash, normalise_newlines};
use crate::path::SourcePath;
use crate::sym::SymbolId;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceFile {
pub path: SourcePath,
pub language: String,
pub module: SymbolId,
pub hash: ContentHash,
pub lines: u32,
}
impl SourceFile {
pub fn new(path: SourcePath, language: impl Into<String>, module: SymbolId, text: &str) -> Self {
Self {
path,
language: language.into(),
module,
hash: ContentHash::of_text(text),
lines: text.lines().count() as u32,
}
}
}
#[derive(Debug, Clone)]
pub struct LoadOptions {
pub extensions: Vec<String>,
pub skip_dirs: Vec<String>,
pub max_file_bytes: u64,
pub skip_hidden: bool,
pub respect_ignore_files: bool,
}
impl Default for LoadOptions {
fn default() -> Self {
Self {
extensions: vec!["rs".into()],
skip_dirs: ["target", "node_modules", "vendor", "dist", "build", "out"].map(String::from).to_vec(),
max_file_bytes: 2 * 1024 * 1024,
skip_hidden: true,
respect_ignore_files: true,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct SourceSet {
files: BTreeMap<SourcePath, String>,
}
impl SourceSet {
pub fn new() -> Self {
Self::default()
}
pub fn insert(&mut self, path: impl AsRef<str>, text: impl AsRef<str>) -> Result<(), crate::PathError> {
let path = SourcePath::new(path)?;
self.files.insert(path, normalise_newlines(text.as_ref()).into_owned());
Ok(())
}
pub fn get(&self, path: &SourcePath) -> Option<&str> {
self.files.get(path).map(String::as_str)
}
pub fn get_str(&self, path: &str) -> Option<&str> {
SourcePath::new(path).ok().and_then(|p| self.get(&p))
}
pub fn iter(&self) -> impl Iterator<Item = (&SourcePath, &str)> {
self.files.iter().map(|(p, t)| (p, t.as_str()))
}
pub fn paths(&self) -> impl Iterator<Item = &SourcePath> {
self.files.keys()
}
pub fn len(&self) -> usize {
self.files.len()
}
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
pub fn retain(&mut self, mut keep: impl FnMut(&SourcePath) -> bool) {
self.files.retain(|p, _| keep(p));
}
pub fn load_dir(root: &Path, options: &LoadOptions) -> io::Result<Self> {
let mut set = Self::new();
let skip_dirs = options.skip_dirs.clone();
let walker = ignore::WalkBuilder::new(root)
.hidden(options.skip_hidden)
.parents(false)
.ignore(options.respect_ignore_files)
.git_ignore(options.respect_ignore_files)
.git_global(false)
.git_exclude(false)
.require_git(false)
.follow_links(false)
.filter_entry(move |e| {
!(e.file_type().is_some_and(|t| t.is_dir())
&& e.depth() > 0
&& skip_dirs.iter().any(|d| e.file_name() == d.as_str()))
})
.build();
for entry in walker {
let entry = entry.map_err(|e| io::Error::other(e.to_string()))?;
if !entry.file_type().is_some_and(|t| t.is_file()) {
continue;
}
let path = entry.path();
let ext_ok =
path.extension().and_then(|e| e.to_str()).is_some_and(|e| options.extensions.iter().any(|x| x == e));
if !ext_ok || entry.metadata().map_err(|e| io::Error::other(e.to_string()))?.len() > options.max_file_bytes
{
continue;
}
let Ok(text) = fs::read_to_string(path) else { continue };
let Ok(rel) = SourcePath::relative_to(path, root) else { continue };
set.files.insert(rel, normalise_newlines(&text).into_owned());
}
Ok(set)
}
}