1use rusqlite::{params, Connection};
11use std::path::PathBuf;
12pub struct CompletionEngine {
14 conn: Connection,
16}
17#[derive(Debug, Clone)]
19pub struct Completion {
20 pub name: String,
22 pub kind: CompletionKind,
24 pub description: Option<String>,
26 pub frequency: u32,
28}
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum CompletionKind {
32 Command,
34 Builtin,
36 Function,
38 Alias,
40 File,
42 Directory,
44 Variable,
46 Option,
48}
49
50impl CompletionKind {
51 pub fn as_str(&self) -> &'static str {
53 match self {
54 Self::Command => "command",
55 Self::Builtin => "builtin",
56 Self::Function => "function",
57 Self::Alias => "alias",
58 Self::File => "file",
59 Self::Directory => "directory",
60 Self::Variable => "variable",
61 Self::Option => "option",
62 }
63 }
64
65 fn from_str(s: &str) -> Self {
66 match s {
67 "command" => Self::Command,
68 "builtin" => Self::Builtin,
69 "function" => Self::Function,
70 "alias" => Self::Alias,
71 "file" => Self::File,
72 "directory" => Self::Directory,
73 "variable" => Self::Variable,
74 "option" => Self::Option,
75 _ => Self::Command,
76 }
77 }
78}
79
80impl CompletionEngine {
81 pub fn new() -> rusqlite::Result<Self> {
83 let db_path = Self::db_path();
84 std::fs::create_dir_all(db_path.parent().unwrap()).ok();
85 let conn = Connection::open(&db_path)?;
86
87 let engine = Self { conn };
88 engine.init_schema()?;
89 Ok(engine)
90 }
91 pub fn in_memory() -> rusqlite::Result<Self> {
93 let conn = Connection::open_in_memory()?;
94 let engine = Self { conn };
95 engine.init_schema()?;
96 Ok(engine)
97 }
98
99 fn db_path() -> PathBuf {
100 let root = if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
107 PathBuf::from(custom)
108 } else {
109 dirs::home_dir()
110 .unwrap_or_else(|| PathBuf::from("."))
111 .join(".zshrs")
112 };
113 root.join("completions.db")
114 }
115
116 fn init_schema(&self) -> rusqlite::Result<()> {
117 self.conn.execute_batch(
118 r#"
119 CREATE TABLE IF NOT EXISTS completions (
120 id INTEGER PRIMARY KEY,
121 name TEXT NOT NULL,
122 kind TEXT NOT NULL,
123 description TEXT,
124 frequency INTEGER DEFAULT 0,
125 UNIQUE(name, kind)
126 );
127
128 CREATE VIRTUAL TABLE IF NOT EXISTS completions_fts USING fts5(
129 name,
130 description,
131 content='completions',
132 content_rowid='id'
133 );
134
135 CREATE TRIGGER IF NOT EXISTS completions_ai AFTER INSERT ON completions BEGIN
136 INSERT INTO completions_fts(rowid, name, description)
137 VALUES (new.id, new.name, new.description);
138 END;
139
140 CREATE TRIGGER IF NOT EXISTS completions_ad AFTER DELETE ON completions BEGIN
141 INSERT INTO completions_fts(completions_fts, rowid, name, description)
142 VALUES ('delete', old.id, old.name, old.description);
143 END;
144
145 CREATE TRIGGER IF NOT EXISTS completions_au AFTER UPDATE ON completions BEGIN
146 INSERT INTO completions_fts(completions_fts, rowid, name, description)
147 VALUES ('delete', old.id, old.name, old.description);
148 INSERT INTO completions_fts(rowid, name, description)
149 VALUES (new.id, new.name, new.description);
150 END;
151
152 CREATE INDEX IF NOT EXISTS idx_completions_name ON completions(name);
153 CREATE INDEX IF NOT EXISTS idx_completions_kind ON completions(kind);
154 CREATE INDEX IF NOT EXISTS idx_completions_frequency ON completions(frequency DESC);
155 "#,
156 )?;
157 Ok(())
158 }
159 pub fn add_completion(
161 &self,
162 name: &str,
163 kind: CompletionKind,
164 description: Option<&str>,
165 ) -> rusqlite::Result<()> {
166 self.conn.execute(
167 "INSERT OR IGNORE INTO completions (name, kind, description) VALUES (?1, ?2, ?3)",
168 params![name, kind.as_str(), description],
169 )?;
170 Ok(())
171 }
172 pub fn add_completions(
174 &self,
175 completions: &[(String, CompletionKind, Option<String>)],
176 ) -> rusqlite::Result<()> {
177 let tx = self.conn.unchecked_transaction()?;
178 {
179 let mut stmt = self.conn.prepare(
180 "INSERT OR IGNORE INTO completions (name, kind, description) VALUES (?1, ?2, ?3)",
181 )?;
182 for (name, kind, desc) in completions {
183 stmt.execute(params![name, kind.as_str(), desc.as_deref()])?;
184 }
185 }
186 tx.commit()?;
187 Ok(())
188 }
189 pub fn increment_frequency(&self, name: &str) -> rusqlite::Result<()> {
191 self.conn.execute(
192 "UPDATE completions SET frequency = frequency + 1 WHERE name = ?1",
193 params![name],
194 )?;
195 Ok(())
196 }
197 pub fn search(&self, query: &str, limit: usize) -> rusqlite::Result<Vec<Completion>> {
199 if query.is_empty() {
200 return self.get_top_by_frequency(limit);
201 }
202
203 let prefix_results = self.search_prefix(query, limit)?;
205 if prefix_results.len() >= limit {
206 return Ok(prefix_results);
207 }
208
209 self.search_fts(query, limit)
211 }
212
213 fn search_prefix(&self, prefix: &str, limit: usize) -> rusqlite::Result<Vec<Completion>> {
214 let mut stmt = self.conn.prepare(
215 "SELECT name, kind, description, frequency FROM completions
216 WHERE name LIKE ?1 || '%'
217 ORDER BY frequency DESC, name ASC
218 LIMIT ?2",
219 )?;
220
221 let rows = stmt.query_map(params![prefix, limit as i64], |row| {
222 Ok(Completion {
223 name: row.get(0)?,
224 kind: CompletionKind::from_str(&row.get::<_, String>(1)?),
225 description: row.get(2)?,
226 frequency: row.get(3)?,
227 })
228 })?;
229
230 rows.collect()
231 }
232
233 fn search_fts(&self, query: &str, limit: usize) -> rusqlite::Result<Vec<Completion>> {
234 let fts_query = format!("{}*", query);
235 let mut stmt = self.conn.prepare(
236 "SELECT c.name, c.kind, c.description, c.frequency
237 FROM completions c
238 JOIN completions_fts fts ON c.id = fts.rowid
239 WHERE completions_fts MATCH ?1
240 ORDER BY c.frequency DESC, rank
241 LIMIT ?2",
242 )?;
243
244 let rows = stmt.query_map(params![fts_query, limit as i64], |row| {
245 Ok(Completion {
246 name: row.get(0)?,
247 kind: CompletionKind::from_str(&row.get::<_, String>(1)?),
248 description: row.get(2)?,
249 frequency: row.get(3)?,
250 })
251 })?;
252
253 rows.collect()
254 }
255
256 fn get_top_by_frequency(&self, limit: usize) -> rusqlite::Result<Vec<Completion>> {
257 let mut stmt = self.conn.prepare(
258 "SELECT name, kind, description, frequency FROM completions
259 ORDER BY frequency DESC, name ASC
260 LIMIT ?1",
261 )?;
262
263 let rows = stmt.query_map(params![limit as i64], |row| {
264 Ok(Completion {
265 name: row.get(0)?,
266 kind: CompletionKind::from_str(&row.get::<_, String>(1)?),
267 description: row.get(2)?,
268 frequency: row.get(3)?,
269 })
270 })?;
271
272 rows.collect()
273 }
274 pub fn count(&self) -> rusqlite::Result<usize> {
276 self.conn
277 .query_row("SELECT COUNT(*) FROM completions", [], |row| row.get(0))
278 }
279 pub fn index_system_commands(&self) -> rusqlite::Result<usize> {
281 let path = std::env::var("PATH").unwrap_or_default();
282 let mut completions = Vec::new();
283
284 for dir in path.split(':') {
285 if let Ok(entries) = std::fs::read_dir(dir) {
286 for entry in entries.flatten() {
287 if let Ok(ft) = entry.file_type() {
288 if ft.is_file() || ft.is_symlink() {
289 if let Some(name) = entry.file_name().to_str() {
290 completions.push((name.to_string(), CompletionKind::Command, None));
291 }
292 }
293 }
294 }
295 }
296 }
297
298 let count = completions.len();
299 self.add_completions(&completions)?;
300 Ok(count)
301 }
302 pub fn index_shell_builtins(&self) -> rusqlite::Result<usize> {
304 let builtins = [
305 ("cd", "Change directory"),
306 ("pwd", "Print working directory"),
307 ("echo", "Print arguments"),
308 ("export", "Set environment variable"),
309 ("unset", "Unset environment variable"),
310 ("alias", "Define alias"),
311 ("unalias", "Remove alias"),
312 ("source", "Execute file in current shell"),
313 ("exit", "Exit the shell"),
314 ("jobs", "List background jobs"),
315 ("fg", "Bring job to foreground"),
316 ("bg", "Continue job in background"),
317 ("history", "Show command history"),
318 ("set", "Set shell options"),
319 ("unset", "Unset shell options"),
320 ("type", "Show command type"),
321 ("which", "Show command path"),
322 ("builtin", "Execute builtin command"),
323 ("command", "Execute external command"),
324 ("exec", "Replace shell with command"),
325 ("eval", "Evaluate arguments as command"),
326 ("read", "Read input"),
327 ("printf", "Formatted print"),
328 ("test", "Evaluate conditional expression"),
329 ("true", "Return success"),
330 ("false", "Return failure"),
331 (":", "Null command"),
332 ("return", "Return from function"),
333 ("break", "Break from loop"),
334 ("continue", "Continue loop"),
335 ("shift", "Shift positional parameters"),
336 ("wait", "Wait for background jobs"),
337 ("trap", "Set signal handler"),
338 ("umask", "Set file creation mask"),
339 ("ulimit", "Set resource limits"),
340 ("times", "Show shell times"),
341 ("kill", "Send signal to process"),
342 ("let", "Evaluate arithmetic expression"),
343 ("declare", "Declare variable"),
344 ("local", "Declare local variable"),
345 ("readonly", "Make variable readonly"),
346 ("typeset", "Declare variable type"),
347 ("hash", "Remember command path"),
348 ("dirs", "Show directory stack"),
349 ("pushd", "Push directory"),
350 ("popd", "Pop directory"),
351 ("getopts", "Parse options"),
352 ("enable", "Enable/disable builtins"),
353 ("logout", "Exit login shell"),
354 ("suspend", "Suspend shell"),
355 ("disown", "Remove job from table"),
356 ];
357
358 let completions: Vec<_> = builtins
359 .iter()
360 .map(|(name, desc)| {
361 (
362 name.to_string(),
363 CompletionKind::Builtin,
364 Some(desc.to_string()),
365 )
366 })
367 .collect();
368
369 let count = completions.len();
370 self.add_completions(&completions)?;
371 Ok(count)
372 }
373}
374
375impl Default for CompletionEngine {
376 fn default() -> Self {
377 Self::new().expect("Failed to create completion engine")
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn test_completion_engine() {
387 let _g = crate::test_util::global_state_lock();
388 let engine = CompletionEngine::in_memory().unwrap();
389
390 engine
391 .add_completion("git", CompletionKind::Command, Some("Version control"))
392 .unwrap();
393 engine
394 .add_completion("grep", CompletionKind::Command, Some("Search text"))
395 .unwrap();
396 engine
397 .add_completion("gzip", CompletionKind::Command, Some("Compress files"))
398 .unwrap();
399
400 let results = engine.search("g", 10).unwrap();
401 assert_eq!(results.len(), 3);
402
403 let results = engine.search("gi", 10).unwrap();
404 assert_eq!(results.len(), 1);
405 assert_eq!(results[0].name, "git");
406 }
407
408 #[test]
409 fn test_frequency_ranking() {
410 let _g = crate::test_util::global_state_lock();
411 let engine = CompletionEngine::in_memory().unwrap();
412
413 engine
414 .add_completion("aaa", CompletionKind::Command, None)
415 .unwrap();
416 engine
417 .add_completion("aab", CompletionKind::Command, None)
418 .unwrap();
419
420 for _ in 0..5 {
422 engine.increment_frequency("aab").unwrap();
423 }
424
425 let results = engine.search("aa", 10).unwrap();
426 assert_eq!(results[0].name, "aab"); }
428}