Skip to main content

zsh/extensions/
completion.rs

1//! SQLite-backed completion engine (legacy / unused).
2//!
3//! Vestigial. The live completion hot path is the rkyv-mmap'd shard
4//! set (see `src/extensions/autoload_cache.rs` and `src/compsys/`);
5//! this module's `CompletionEngine` has zero callers. Kept only so
6//! removing it stays a deliberate cleanup commit rather than silent
7//! drift. Treat the rest of this file as a historical artifact —
8//! it does not back any completion behavior in the running shell.
9
10use rusqlite::{params, Connection};
11use std::path::PathBuf;
12/// `CompletionEngine` — see fields for layout.
13pub struct CompletionEngine {
14    /// `conn` field.
15    conn: Connection,
16}
17/// `Completion` — see fields for layout.
18#[derive(Debug, Clone)]
19pub struct Completion {
20    /// `name` field.
21    pub name: String,
22    /// `kind` field.
23    pub kind: CompletionKind,
24    /// `description` field.
25    pub description: Option<String>,
26    /// `frequency` field.
27    pub frequency: u32,
28}
29/// `CompletionKind` — see variants.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum CompletionKind {
32    /// `Command` variant.
33    Command,
34    /// `Builtin` variant.
35    Builtin,
36    /// `Function` variant.
37    Function,
38    /// `Alias` variant.
39    Alias,
40    /// `File` variant.
41    File,
42    /// `Directory` variant.
43    Directory,
44    /// `Variable` variant.
45    Variable,
46    /// `Option` variant.
47    Option,
48}
49
50impl CompletionKind {
51    /// `as_str` — see implementation.
52    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    /// `new` — see implementation.
82    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    /// `in_memory` — see implementation.
92    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        // All zshrs state lives under `$ZSHRS_HOME` (default `~/.zshrs`),
101        // matching the daemon's CachePaths convention (daemon/paths.rs)
102        // and the `~/.zinit`/`~/.zpwr`/`~/.oh-my-zsh` precedent. We
103        // explicitly DO NOT use `dirs::cache_dir()` because that
104        // resolves to `~/Library/Caches/zshrs` on macOS or
105        // `~/.cache/zshrs` on Linux — both forbidden per project policy.
106        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    /// `add_completion` — see implementation.
160    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    /// `add_completions` — see implementation.
173    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    /// `increment_frequency` — see implementation.
190    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    /// `search` — see implementation.
198    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        // Try prefix match first (faster)
204        let prefix_results = self.search_prefix(query, limit)?;
205        if prefix_results.len() >= limit {
206            return Ok(prefix_results);
207        }
208
209        // Fall back to FTS5 fuzzy search
210        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    /// `count` — see implementation.
275    pub fn count(&self) -> rusqlite::Result<usize> {
276        self.conn
277            .query_row("SELECT COUNT(*) FROM completions", [], |row| row.get(0))
278    }
279    /// `index_system_commands` — see implementation.
280    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    /// `index_shell_builtins` — see implementation.
303    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        // Increment aab frequency
421        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"); // Higher frequency first
427    }
428}