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
37use std::path::{Path, PathBuf};
38
39use config::Config;
40use indexer::{IndexStats, Indexer};
41use io_backend::IoBackend;
42use meta::Meta;
43use paths::Paths;
44use search::Searcher;
45
46#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
48pub struct Status {
49 pub root: PathBuf,
50 pub indexed: bool,
51 pub segments: usize,
52 pub doc_count: u64,
53 pub symbol_count: u64,
54 pub last_indexed: u64,
55 pub backend: String,
56}
57
58pub struct Greplm {
60 paths: Paths,
61 config: Config,
62 backend: Box<dyn IoBackend>,
63}
64
65impl Greplm {
66 pub fn open(root: impl AsRef<Path>) -> Result<Greplm> {
68 let paths = Paths::new(root);
69 let config = Config::load(&paths.config_file())?;
70 Ok(Greplm {
71 paths,
72 config,
73 backend: io_backend::default_backend(),
74 })
75 }
76
77 pub fn discover(start: impl AsRef<Path>) -> Result<Greplm> {
80 let start = start.as_ref();
81 let mut cur = Some(start);
82 while let Some(dir) = cur {
83 if dir.join(paths::DIR_NAME).is_dir() {
84 return Greplm::open(dir);
85 }
86 cur = dir.parent();
87 }
88 Greplm::open(start)
89 }
90
91 pub fn root(&self) -> &Path {
92 &self.paths.root
93 }
94
95 pub fn config(&self) -> &Config {
96 &self.config
97 }
98
99 pub fn ensure_initialized(&self) -> Result<()> {
101 std::fs::create_dir_all(self.paths.segments_dir())
102 .map_err(|e| Error::io(self.paths.segments_dir(), e))?;
103 let cfg = self.paths.config_file();
104 if !cfg.exists() {
105 self.config.save(&cfg)?;
106 }
107 let gi = self.paths.gitignore_file();
108 if !gi.exists() {
109 std::fs::write(&gi, "*\n").map_err(|e| Error::io(&gi, e))?;
110 }
111 Ok(())
112 }
113
114 pub fn index(&self, force: bool) -> Result<IndexStats> {
116 self.ensure_initialized()?;
117 let indexer = Indexer::new(&self.paths, &self.config, self.backend.as_ref());
118 if force {
119 indexer.index_full()
120 } else {
121 indexer.index_incremental()
122 }
123 }
124
125 pub fn ensure_indexed(&self) -> Result<bool> {
135 match self.status() {
136 Ok(s) if s.indexed => return Ok(false),
138 Ok(_) => {}
140 Err(_) => {}
142 }
143 self.index(false)?;
144 Ok(true)
145 }
146
147 pub fn is_dirty(&self) -> Result<bool> {
153 let cache = cache::Cache::open(&self.paths.cache_file())?;
154 let existing = cache.load_all()?;
155 let walked = walk::walk(&self.paths, &self.config)?;
156
157 let mut seen = std::collections::HashSet::with_capacity(walked.entries.len());
158 for e in &walked.entries {
159 seen.insert(e.rel.clone());
160 let (_, mtime_ns, size) = cache::stat_key(&e.metadata);
161 match existing.get(&e.rel) {
162 Some(rec) if rec.size == size && rec.mtime_ns == mtime_ns => {}
164 Some(_) => return Ok(true),
165 None => {
170 if self.would_index(&e.path) {
171 return Ok(true);
172 }
173 }
174 }
175 }
176 for path in existing.keys() {
179 if !seen.contains(path) {
180 return Ok(true);
181 }
182 }
183 Ok(false)
184 }
185
186 fn would_index(&self, path: &Path) -> bool {
190 match std::fs::read(path) {
191 Ok(data) => self.config.index_binary || memchr::memchr(0, &data).is_none(),
192 Err(_) => false,
193 }
194 }
195
196 pub fn compact(&self) -> Result<IndexStats> {
199 self.ensure_initialized()?;
200 Indexer::new(&self.paths, &self.config, self.backend.as_ref()).compact()
201 }
202
203 pub fn searcher(&self) -> Result<Searcher> {
205 Searcher::open(&self.paths)
206 }
207
208 pub fn search_or_grep(&self, query: &search::SearchQuery) -> Result<Vec<search::SearchHit>> {
212 match self.searcher().and_then(|s| s.search(query)) {
213 Ok(hits) => Ok(hits),
214 Err(e) => {
215 tracing::warn!("index unavailable ({e}); falling back to grep walk");
216 search::grep_walk(&self.paths, &self.config, query)
217 }
218 }
219 }
220
221 pub fn status(&self) -> Result<Status> {
223 let meta = if self.paths.meta_file().exists() {
224 Meta::load(&self.paths.meta_file())?
225 } else {
226 Meta::default()
227 };
228 Ok(Status {
229 root: self.paths.root.clone(),
230 indexed: !meta.segments.is_empty(),
231 segments: meta.segments.len(),
232 doc_count: meta.doc_count,
233 symbol_count: meta.symbol_count,
234 last_indexed: meta.last_indexed,
235 backend: self.backend.name().to_string(),
236 })
237 }
238
239 pub fn clean(&self) -> Result<()> {
241 if self.paths.base.is_dir() {
242 std::fs::remove_dir_all(&self.paths.base)
243 .map_err(|e| Error::io(&self.paths.base, e))?;
244 }
245 Ok(())
246 }
247
248 pub fn watch<F: FnMut(&IndexStats)>(
253 &self,
254 debounce: std::time::Duration,
255 on_change: F,
256 ) -> Result<()> {
257 watch::run(self, debounce, on_change)
258 }
259
260 pub fn socket_path(&self) -> PathBuf {
262 self.paths.base.join(proto::SOCKET_NAME)
263 }
264
265 pub fn record_savings(
268 &self,
269 kind: &str,
270 files: &std::collections::BTreeSet<String>,
271 returned_chars: u64,
272 results: u64,
273 ) {
274 savings::record(&self.paths, kind, files, returned_chars, results);
275 }
276
277 pub fn savings_report(&self) -> savings::SavingsReport {
279 savings::report(&self.paths)
280 }
281
282 pub(crate) fn paths(&self) -> &Paths {
283 &self.paths
284 }
285}