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(crate) mod table;
32pub mod trigram;
33pub mod walk;
34pub mod watch;
35
36pub use error::{Error, Result};
37
38#[doc(hidden)]
42pub use fsutil::faults;
43
44use std::path::{Path, PathBuf};
45
46use config::Config;
47use indexer::{IndexStats, Indexer};
48use io_backend::IoBackend;
49use meta::Meta;
50use paths::Paths;
51use search::Searcher;
52
53#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
55pub struct Status {
56 pub root: PathBuf,
57 pub indexed: bool,
58 pub segments: usize,
59 pub doc_count: u64,
60 pub symbol_count: u64,
61 pub last_indexed: u64,
62 pub backend: String,
63}
64
65pub struct Greplm {
67 paths: Paths,
68 config: Config,
69 backend: Box<dyn IoBackend>,
70}
71
72impl Greplm {
73 pub fn open(root: impl AsRef<Path>) -> Result<Greplm> {
75 let paths = Paths::new(root);
76 let config = Config::load(&paths.config_file())?;
77 Ok(Greplm {
78 paths,
79 config,
80 backend: io_backend::default_backend(),
81 })
82 }
83
84 pub fn discover(start: impl AsRef<Path>) -> Result<Greplm> {
87 let start = start.as_ref();
88 let mut cur = Some(start);
89 while let Some(dir) = cur {
90 if dir.join(paths::DIR_NAME).is_dir() {
91 return Greplm::open(dir);
92 }
93 cur = dir.parent();
94 }
95 Greplm::open(start)
96 }
97
98 pub fn root(&self) -> &Path {
99 &self.paths.root
100 }
101
102 pub fn config(&self) -> &Config {
103 &self.config
104 }
105
106 pub fn ensure_initialized(&self) -> Result<()> {
108 std::fs::create_dir_all(self.paths.segments_dir())
109 .map_err(|e| Error::io(self.paths.segments_dir(), e))?;
110 let cfg = self.paths.config_file();
111 if !cfg.exists() {
112 self.config.save(&cfg)?;
113 }
114 let gi = self.paths.gitignore_file();
115 if !gi.exists() {
116 std::fs::write(&gi, "*\n").map_err(|e| Error::io(&gi, e))?;
117 }
118 Ok(())
119 }
120
121 pub fn index(&self, force: bool) -> Result<IndexStats> {
123 self.ensure_initialized()?;
124 let indexer = Indexer::new(&self.paths, &self.config, self.backend.as_ref());
125 if force {
126 indexer.index_full()
127 } else {
128 indexer.index_incremental()
129 }
130 }
131
132 pub fn ensure_indexed(&self) -> Result<bool> {
142 match self.status() {
143 Ok(s) if s.indexed => return Ok(false),
145 Ok(_) => {}
147 Err(_) => {}
149 }
150 self.index(false)?;
151 Ok(true)
152 }
153
154 pub fn is_dirty(&self) -> Result<bool> {
160 let cache = cache::Cache::open(&self.paths.cache_file())?;
161 let existing = cache.load_all()?;
162 let walked = walk::walk(&self.paths, &self.config)?;
163
164 let mut seen = std::collections::HashSet::with_capacity(walked.entries.len());
165 for e in &walked.entries {
166 seen.insert(e.rel.clone());
167 let (_, mtime_ns, size) = cache::stat_key(&e.metadata);
168 match existing.get(&e.rel) {
169 Some(rec) if rec.size == size && rec.mtime_ns == mtime_ns => {}
171 Some(_) => return Ok(true),
172 None => {
177 if self.would_index(&e.path) {
178 return Ok(true);
179 }
180 }
181 }
182 }
183 for path in existing.keys() {
186 if !seen.contains(path) {
187 return Ok(true);
188 }
189 }
190 Ok(false)
191 }
192
193 fn would_index(&self, path: &Path) -> bool {
197 match std::fs::read(path) {
198 Ok(data) => self.config.index_binary || memchr::memchr(0, &data).is_none(),
199 Err(_) => false,
200 }
201 }
202
203 pub fn compact(&self) -> Result<IndexStats> {
206 self.ensure_initialized()?;
207 Indexer::new(&self.paths, &self.config, self.backend.as_ref()).compact()
208 }
209
210 pub fn searcher(&self) -> Result<Searcher> {
212 Searcher::open(&self.paths)
213 }
214
215 pub fn searcher_reusing(&self, prev: &Searcher) -> Result<Searcher> {
220 Searcher::open_reusing(&self.paths, prev)
221 }
222
223 pub fn search_or_grep(&self, query: &search::SearchQuery) -> Result<Vec<search::SearchHit>> {
227 match self.searcher().and_then(|s| s.search(query)) {
228 Ok(hits) => Ok(hits),
229 Err(e) => {
230 tracing::warn!("index unavailable ({e}); falling back to grep walk");
231 search::grep_walk(&self.paths, &self.config, query)
232 }
233 }
234 }
235
236 pub fn status(&self) -> Result<Status> {
238 let meta = if self.paths.meta_file().exists() {
239 Meta::load(&self.paths.meta_file())?
240 } else {
241 Meta::default()
242 };
243 Ok(Status {
244 root: self.paths.root.clone(),
245 indexed: !meta.segments.is_empty(),
246 segments: meta.segments.len(),
247 doc_count: meta.doc_count,
248 symbol_count: meta.symbol_count,
249 last_indexed: meta.last_indexed,
250 backend: self.backend.name().to_string(),
251 })
252 }
253
254 pub fn clean(&self) -> Result<()> {
256 if self.paths.base.is_dir() {
257 std::fs::remove_dir_all(&self.paths.base)
258 .map_err(|e| Error::io(&self.paths.base, e))?;
259 }
260 Ok(())
261 }
262
263 pub fn watch<F: FnMut(&IndexStats)>(
268 &self,
269 debounce: std::time::Duration,
270 on_change: F,
271 ) -> Result<()> {
272 watch::run(self, debounce, on_change)
273 }
274
275 pub fn socket_path(&self) -> PathBuf {
277 self.paths.base.join(proto::SOCKET_NAME)
278 }
279
280 pub fn record_savings(
283 &self,
284 kind: &str,
285 files: &std::collections::BTreeSet<String>,
286 returned_chars: u64,
287 results: u64,
288 ) {
289 savings::record(&self.paths, kind, files, returned_chars, results);
290 }
291
292 pub fn savings_report(&self) -> savings::SavingsReport {
294 savings::report(&self.paths)
295 }
296
297 pub(crate) fn paths(&self) -> &Paths {
298 &self.paths
299 }
300}