Skip to main content

greplm_core/
lib.rs

1//! greplm-core: an extreme-performance, trigram-based code indexer for LLM agents.
2//!
3//! The index lives in a `.greplm/` directory at the project root and consists of
4//! immutable, mmap-backed segments (trigram FST + roaring posting lists + doc and
5//! symbol tables). Search filters candidate documents by trigram intersection,
6//! then verifies matches with the real literal/regex matcher.
7
8mod 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/// Status snapshot for `greplm status`.
47#[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
58/// A handle to a greplm project (a directory and its `.greplm` index).
59pub struct Greplm {
60    paths: Paths,
61    config: Config,
62    backend: Box<dyn IoBackend>,
63}
64
65impl Greplm {
66    /// Open (and lazily initialize) the index for `root`.
67    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    /// Find the nearest ancestor of `start` containing a `.greplm` directory,
78    /// falling back to `start` itself if none is found.
79    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    /// Ensure the `.greplm` directory exists with a default config and gitignore.
100    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    /// Build or refresh the index.
115    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    /// Merge all segments into a single compact segment, dropping tombstoned
126    /// documents. Falls back to a full rebuild if the merge cannot proceed.
127    pub fn compact(&self) -> Result<IndexStats> {
128        self.ensure_initialized()?;
129        Indexer::new(&self.paths, &self.config, self.backend.as_ref()).compact()
130    }
131
132    /// Open a searcher over the current index.
133    pub fn searcher(&self) -> Result<Searcher> {
134        Searcher::open(&self.paths)
135    }
136
137    /// Report index status.
138    pub fn status(&self) -> Result<Status> {
139        let meta = if self.paths.meta_file().exists() {
140            Meta::load(&self.paths.meta_file())?
141        } else {
142            Meta::default()
143        };
144        Ok(Status {
145            root: self.paths.root.clone(),
146            indexed: !meta.segments.is_empty(),
147            segments: meta.segments.len(),
148            doc_count: meta.doc_count,
149            symbol_count: meta.symbol_count,
150            last_indexed: meta.last_indexed,
151            backend: self.backend.name().to_string(),
152        })
153    }
154
155    /// Remove the entire `.greplm` directory.
156    pub fn clean(&self) -> Result<()> {
157        if self.paths.base.is_dir() {
158            std::fs::remove_dir_all(&self.paths.base)
159                .map_err(|e| Error::io(&self.paths.base, e))?;
160        }
161        Ok(())
162    }
163
164    /// Watch the project tree and re-index incrementally on changes.
165    ///
166    /// `on_change` is called after each successful incremental update. This call
167    /// blocks until an error occurs or the watcher is dropped.
168    pub fn watch<F: FnMut(&IndexStats)>(
169        &self,
170        debounce: std::time::Duration,
171        on_change: F,
172    ) -> Result<()> {
173        watch::run(self, debounce, on_change)
174    }
175
176    /// Path to the daemon's Unix socket for this project.
177    pub fn socket_path(&self) -> PathBuf {
178        self.paths.base.join(proto::SOCKET_NAME)
179    }
180
181    /// Record a query's token savings (grep+read baseline vs. returned payload).
182    /// Best-effort; never fails a query.
183    pub fn record_savings(
184        &self,
185        kind: &str,
186        files: &std::collections::BTreeSet<String>,
187        returned_chars: u64,
188        results: u64,
189    ) {
190        savings::record(&self.paths, kind, files, returned_chars, results);
191    }
192
193    /// Aggregate the recorded token-savings log.
194    pub fn savings_report(&self) -> savings::SavingsReport {
195        savings::report(&self.paths)
196    }
197
198    pub(crate) fn paths(&self) -> &Paths {
199        &self.paths
200    }
201}