1use std::collections::BTreeMap;
2use std::fs;
3use std::io;
4use std::path::Path;
5
6use serde::{Deserialize, Serialize};
7
8use crate::hash::{ContentHash, normalise_newlines};
9use crate::path::SourcePath;
10use crate::sym::SymbolId;
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct SourceFile {
17 pub path: SourcePath,
19 pub language: String,
21 pub module: SymbolId,
23 pub hash: ContentHash,
25 pub lines: u32,
27}
28
29impl SourceFile {
30 pub fn new(path: SourcePath, language: impl Into<String>, module: SymbolId, text: &str) -> Self {
32 Self {
33 path,
34 language: language.into(),
35 module,
36 hash: ContentHash::of_text(text),
37 lines: text.lines().count() as u32,
38 }
39 }
40}
41
42#[derive(Debug, Clone)]
44pub struct LoadOptions {
45 pub extensions: Vec<String>,
47 pub skip_dirs: Vec<String>,
49 pub max_file_bytes: u64,
51 pub skip_hidden: bool,
53 pub respect_ignore_files: bool,
58}
59
60impl Default for LoadOptions {
61 fn default() -> Self {
62 Self {
63 extensions: vec!["rs".into()],
64 skip_dirs: ["target", "node_modules", "vendor", "dist", "build", "out"].map(String::from).to_vec(),
65 max_file_bytes: 2 * 1024 * 1024,
66 skip_hidden: true,
67 respect_ignore_files: true,
68 }
69 }
70}
71
72#[derive(Debug, Clone, Default, PartialEq, Eq)]
94pub struct SourceSet {
95 files: BTreeMap<SourcePath, String>,
96}
97
98impl SourceSet {
99 pub fn new() -> Self {
101 Self::default()
102 }
103
104 pub fn insert(&mut self, path: impl AsRef<str>, text: impl AsRef<str>) -> Result<(), crate::PathError> {
107 let path = SourcePath::new(path)?;
108 self.files.insert(path, normalise_newlines(text.as_ref()).into_owned());
109 Ok(())
110 }
111
112 pub fn get(&self, path: &SourcePath) -> Option<&str> {
114 self.files.get(path).map(String::as_str)
115 }
116
117 pub fn get_str(&self, path: &str) -> Option<&str> {
119 SourcePath::new(path).ok().and_then(|p| self.get(&p))
120 }
121
122 pub fn iter(&self) -> impl Iterator<Item = (&SourcePath, &str)> {
124 self.files.iter().map(|(p, t)| (p, t.as_str()))
125 }
126
127 pub fn paths(&self) -> impl Iterator<Item = &SourcePath> {
129 self.files.keys()
130 }
131
132 pub fn len(&self) -> usize {
134 self.files.len()
135 }
136
137 pub fn is_empty(&self) -> bool {
139 self.files.is_empty()
140 }
141
142 pub fn retain(&mut self, mut keep: impl FnMut(&SourcePath) -> bool) {
144 self.files.retain(|p, _| keep(p));
145 }
146
147 pub fn load_dir(root: &Path, options: &LoadOptions) -> io::Result<Self> {
152 let mut set = Self::new();
153 let skip_dirs = options.skip_dirs.clone();
154 let walker = ignore::WalkBuilder::new(root)
155 .hidden(options.skip_hidden)
156 .parents(false)
157 .ignore(options.respect_ignore_files)
158 .git_ignore(options.respect_ignore_files)
159 .git_global(false)
160 .git_exclude(false)
161 .require_git(false)
162 .follow_links(false)
163 .filter_entry(move |e| {
164 !(e.file_type().is_some_and(|t| t.is_dir())
165 && e.depth() > 0
166 && skip_dirs.iter().any(|d| e.file_name() == d.as_str()))
167 })
168 .build();
169 for entry in walker {
170 let entry = entry.map_err(|e| io::Error::other(e.to_string()))?;
171 if !entry.file_type().is_some_and(|t| t.is_file()) {
172 continue;
173 }
174 let path = entry.path();
175 let ext_ok =
176 path.extension().and_then(|e| e.to_str()).is_some_and(|e| options.extensions.iter().any(|x| x == e));
177 if !ext_ok || entry.metadata().map_err(|e| io::Error::other(e.to_string()))?.len() > options.max_file_bytes
178 {
179 continue;
180 }
181 let Ok(text) = fs::read_to_string(path) else { continue };
182 let Ok(rel) = SourcePath::relative_to(path, root) else { continue };
183 set.files.insert(rel, normalise_newlines(&text).into_owned());
184 }
185 Ok(set)
186 }
187}