1mod error;
9
10pub mod cache;
11pub mod client;
12pub mod config;
13pub mod context;
14pub mod daemon;
15pub(crate) mod fsutil;
16pub mod git;
17pub mod indexer;
18pub mod io_backend;
19pub mod lang;
20pub mod meta;
21pub mod paths;
22pub mod proto;
23pub mod resolve;
24pub mod savings;
25pub mod search;
26pub mod segment;
27#[cfg(feature = "semantic")]
28pub mod semantic;
29pub mod structural;
30pub mod symbol;
31pub mod trigram;
32pub mod walk;
33pub mod watch;
34
35pub use error::{Error, Result};
36
37#[doc(hidden)]
41pub use fsutil::faults;
42
43use std::path::{Path, PathBuf};
44
45use config::Config;
46use indexer::{IndexStats, Indexer};
47use io_backend::IoBackend;
48use meta::Meta;
49use paths::Paths;
50use search::Searcher;
51
52#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
54pub struct Status {
55 pub root: PathBuf,
56 pub indexed: bool,
57 pub segments: usize,
58 pub doc_count: u64,
59 pub symbol_count: u64,
60 pub last_indexed: u64,
61 pub backend: String,
62}
63
64pub struct Greplm {
66 paths: Paths,
67 config: Config,
68 backend: Box<dyn IoBackend>,
69}
70
71impl Greplm {
72 pub fn open(root: impl AsRef<Path>) -> Result<Greplm> {
74 let paths = Paths::new(root);
75 let config = Config::load(&paths.config_file())?;
76 Ok(Greplm {
77 paths,
78 config,
79 backend: io_backend::default_backend(),
80 })
81 }
82
83 pub fn discover(start: impl AsRef<Path>) -> Result<Greplm> {
86 let start = start.as_ref();
87 let mut cur = Some(start);
88 while let Some(dir) = cur {
89 if dir.join(paths::DIR_NAME).is_dir() {
90 return Greplm::open(dir);
91 }
92 cur = dir.parent();
93 }
94 Greplm::open(start)
95 }
96
97 pub fn root(&self) -> &Path {
98 &self.paths.root
99 }
100
101 pub fn config(&self) -> &Config {
102 &self.config
103 }
104
105 pub fn ensure_initialized(&self) -> Result<()> {
107 std::fs::create_dir_all(self.paths.segments_dir())
108 .map_err(|e| Error::io(self.paths.segments_dir(), e))?;
109 let cfg = self.paths.config_file();
110 if !cfg.exists() {
111 self.config.save(&cfg)?;
112 }
113 let gi = self.paths.gitignore_file();
114 if !gi.exists() {
115 std::fs::write(&gi, "*\n").map_err(|e| Error::io(&gi, e))?;
116 }
117 Ok(())
118 }
119
120 pub fn index(&self, force: bool) -> Result<IndexStats> {
122 self.ensure_initialized()?;
123 let indexer = Indexer::new(&self.paths, &self.config, self.backend.as_ref());
124 if force {
125 indexer.index_full()
126 } else {
127 indexer.index_incremental()
128 }
129 }
130
131 pub fn ensure_indexed(&self) -> Result<bool> {
141 match self.status() {
142 Ok(s) if s.indexed => return Ok(false),
144 Ok(_) => {}
146 Err(_) => {}
148 }
149 self.index(false)?;
150 Ok(true)
151 }
152
153 pub fn is_dirty(&self) -> Result<bool> {
159 let cache = cache::Cache::open(&self.paths.cache_file())?;
160 let existing = cache.load_all()?;
161 let walked = walk::walk(&self.paths, &self.config)?;
162
163 let mut seen = std::collections::HashSet::with_capacity(walked.entries.len());
164 for e in &walked.entries {
165 seen.insert(e.rel.clone());
166 let (_, mtime_ns, size) = cache::stat_key(&e.metadata);
167 match existing.get(&e.rel) {
168 Some(rec) if rec.size == size && rec.mtime_ns == mtime_ns => {}
170 Some(_) => return Ok(true),
171 None => {
176 if self.would_index(&e.path) {
177 return Ok(true);
178 }
179 }
180 }
181 }
182 for path in existing.keys() {
185 if !seen.contains(path) {
186 return Ok(true);
187 }
188 }
189 Ok(false)
190 }
191
192 fn would_index(&self, path: &Path) -> bool {
196 match std::fs::read(path) {
197 Ok(data) => self.config.index_binary || memchr::memchr(0, &data).is_none(),
198 Err(_) => false,
199 }
200 }
201
202 pub fn compact(&self) -> Result<IndexStats> {
205 self.ensure_initialized()?;
206 Indexer::new(&self.paths, &self.config, self.backend.as_ref()).compact()
207 }
208
209 pub fn searcher(&self) -> Result<Searcher> {
211 Searcher::open(&self.paths)
212 }
213
214 pub fn search_or_grep(&self, query: &search::SearchQuery) -> Result<Vec<search::SearchHit>> {
218 match self.searcher().and_then(|s| s.search(query)) {
219 Ok(hits) => Ok(hits),
220 Err(e) => {
221 tracing::warn!("index unavailable ({e}); falling back to grep walk");
222 search::grep_walk(&self.paths, &self.config, query)
223 }
224 }
225 }
226
227 pub fn status(&self) -> Result<Status> {
229 let meta = if self.paths.meta_file().exists() {
230 Meta::load(&self.paths.meta_file())?
231 } else {
232 Meta::default()
233 };
234 Ok(Status {
235 root: self.paths.root.clone(),
236 indexed: !meta.segments.is_empty(),
237 segments: meta.segments.len(),
238 doc_count: meta.doc_count,
239 symbol_count: meta.symbol_count,
240 last_indexed: meta.last_indexed,
241 backend: self.backend.name().to_string(),
242 })
243 }
244
245 pub fn clean(&self) -> Result<()> {
247 if self.paths.base.is_dir() {
248 std::fs::remove_dir_all(&self.paths.base)
249 .map_err(|e| Error::io(&self.paths.base, e))?;
250 }
251 Ok(())
252 }
253
254 pub fn watch<F: FnMut(&IndexStats)>(
259 &self,
260 debounce: std::time::Duration,
261 on_change: F,
262 ) -> Result<()> {
263 watch::run(self, debounce, on_change)
264 }
265
266 pub fn socket_path(&self) -> PathBuf {
268 self.paths.base.join(proto::SOCKET_NAME)
269 }
270
271 pub fn record_savings(
274 &self,
275 kind: &str,
276 files: &std::collections::BTreeSet<String>,
277 returned_chars: u64,
278 results: u64,
279 ) {
280 savings::record(&self.paths, kind, files, returned_chars, results);
281 }
282
283 pub fn savings_report(&self) -> savings::SavingsReport {
285 savings::report(&self.paths)
286 }
287
288 pub(crate) fn paths(&self) -> &Paths {
289 &self.paths
290 }
291}