1use rusqlite::{params, Connection, OptionalExtension};
22use std::collections::HashMap;
23use std::path::{Path, PathBuf};
24
25pub struct CompsysCache {
27 conn: Connection,
29}
30
31pub fn default_cache_path() -> PathBuf {
35 let root = if let Some(custom) = std::env::var_os("ZSHRS_HOME") {
36 PathBuf::from(custom)
37 } else {
38 let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
39 PathBuf::from(home).join(".zshrs")
40 };
41 root.join("compsys.db")
42}
43
44impl CompsysCache {
45 pub fn conn(&self) -> &Connection {
47 &self.conn
48 }
49
50 pub fn count_table(&self, table: &str) -> rusqlite::Result<usize> {
52 let sql = format!("SELECT COUNT(*) FROM {}", table);
54 self.conn
55 .query_row(&sql, [], |row| row.get::<_, i64>(0).map(|n| n as usize))
56 }
57
58 pub fn count_table_where(&self, table: &str, condition: &str) -> rusqlite::Result<usize> {
60 let sql = format!("SELECT COUNT(*) FROM {} WHERE {}", table, condition);
61 self.conn
62 .query_row(&sql, [], |row| row.get::<_, i64>(0).map(|n| n as usize))
63 }
64
65 pub fn open(path: impl AsRef<Path>) -> rusqlite::Result<Self> {
67 let _lowfd = crate::lowfd::LowFdGuard::new();
71 let conn = Connection::open(path)?;
72 let cache = Self { conn };
73 cache.configure_for_speed()?;
74 cache.init_schema()?;
75 Ok(cache)
76 }
77
78 pub fn memory() -> rusqlite::Result<Self> {
80 let conn = Connection::open_in_memory()?;
81 let cache = Self { conn };
82 cache.configure_for_speed()?;
83 cache.init_schema()?;
84 Ok(cache)
85 }
86
87 fn configure_for_speed(&self) -> rusqlite::Result<()> {
89 self.conn.execute_batch(
91 r#"
92 PRAGMA journal_mode = WAL;
93 PRAGMA synchronous = NORMAL;
94 PRAGMA cache_size = -64000;
95 PRAGMA mmap_size = 268435456;
96 PRAGMA temp_store = MEMORY;
97 "#,
98 )
99 }
100
101 fn init_schema(&self) -> rusqlite::Result<()> {
102 let has_legacy_bytecode_col = {
117 let exists: i64 = self.conn.query_row(
118 "SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='autoloads'",
119 [],
120 |row| row.get(0),
121 )?;
122 if exists == 0 {
123 false
124 } else {
125 let mut stmt = self.conn.prepare("PRAGMA table_info(autoloads)")?;
126 let cols: Vec<String> = stmt
127 .query_map([], |row| row.get::<_, String>(1))?
128 .collect::<rusqlite::Result<_>>()?;
129 cols.iter().any(|c| c == "bytecode")
130 }
131 };
132 if has_legacy_bytecode_col {
133 self.conn.execute_batch(
134 r#"
135 BEGIN;
136 CREATE TABLE autoloads_new (
137 name TEXT PRIMARY KEY,
138 source TEXT NOT NULL,
139 offset INTEGER NOT NULL,
140 size INTEGER NOT NULL,
141 body TEXT
142 ) WITHOUT ROWID;
143 INSERT OR IGNORE INTO autoloads_new (name, source, offset, size, body)
144 SELECT name, source, offset, size, body FROM autoloads;
145 DROP TABLE autoloads;
146 ALTER TABLE autoloads_new RENAME TO autoloads;
147 COMMIT;
148 "#,
149 )?;
150 }
151 self.conn.execute_batch(
152 r#"
153 -- Autoloads: flat table, PRIMARY KEY = clustered index
154 -- body stores actual function definition - NO filesystem access on autoload -Xz
155 -- compinit reads from .zwc or plain files ONCE, stores body here.
156 -- bytecode is held separately in the rkyv autoload-cache shard.
157 CREATE TABLE IF NOT EXISTS autoloads (
158 name TEXT PRIMARY KEY,
159 source TEXT NOT NULL,
160 offset INTEGER NOT NULL,
161 size INTEGER NOT NULL,
162 body TEXT
163 ) WITHOUT ROWID;
164
165 -- zstyle: flat lookup by pattern+style
166 CREATE TABLE IF NOT EXISTS zstyles (
167 pattern TEXT NOT NULL,
168 style TEXT NOT NULL,
169 value TEXT NOT NULL,
170 eval INTEGER DEFAULT 0,
171 PRIMARY KEY (pattern, style)
172 ) WITHOUT ROWID;
173
174 -- Completion mappings: direct key lookup
175 CREATE TABLE IF NOT EXISTS comps (
176 command TEXT PRIMARY KEY,
177 function TEXT NOT NULL
178 ) WITHOUT ROWID;
179
180 -- Pattern completions
181 CREATE TABLE IF NOT EXISTS patcomps (
182 pattern TEXT PRIMARY KEY,
183 function TEXT NOT NULL
184 ) WITHOUT ROWID;
185
186 -- Key completions
187 CREATE TABLE IF NOT EXISTS keycomps (
188 key TEXT PRIMARY KEY,
189 function TEXT NOT NULL
190 ) WITHOUT ROWID;
191
192 -- Services
193 CREATE TABLE IF NOT EXISTS services (
194 command TEXT PRIMARY KEY,
195 service TEXT NOT NULL
196 ) WITHOUT ROWID;
197
198 -- Result cache
199 CREATE TABLE IF NOT EXISTS cache (
200 context TEXT PRIMARY KEY,
201 data BLOB NOT NULL,
202 mtime INTEGER NOT NULL
203 ) WITHOUT ROWID;
204
205 -- PATH executables: flat, fast prefix via FTS5
206 CREATE TABLE IF NOT EXISTS executables (
207 name TEXT PRIMARY KEY,
208 path TEXT NOT NULL
209 ) WITHOUT ROWID;
210
211 -- Named directories
212 CREATE TABLE IF NOT EXISTS named_dirs (
213 name TEXT PRIMARY KEY,
214 path TEXT NOT NULL
215 ) WITHOUT ROWID;
216
217 -- Shell functions
218 CREATE TABLE IF NOT EXISTS shell_functions (
219 name TEXT PRIMARY KEY,
220 source TEXT NOT NULL
221 ) WITHOUT ROWID;
222
223 -- Metadata
224 CREATE TABLE IF NOT EXISTS metadata (
225 key TEXT PRIMARY KEY,
226 value TEXT NOT NULL
227 ) WITHOUT ROWID;
228
229 -- FTS5 for lightning-fast prefix search (standalone, not content-synced)
230 CREATE VIRTUAL TABLE IF NOT EXISTS fts_comps USING fts5(
231 command,
232 tokenize='unicode61'
233 );
234
235 CREATE VIRTUAL TABLE IF NOT EXISTS fts_executables USING fts5(
236 name,
237 tokenize='unicode61'
238 );
239
240 CREATE VIRTUAL TABLE IF NOT EXISTS fts_shell_functions USING fts5(
241 name,
242 tokenize='unicode61'
243 );
244
245 -- Covering index for comps prefix search (fallback if FTS unavailable)
246 CREATE INDEX IF NOT EXISTS idx_comps_cmd ON comps(command);
247 CREATE INDEX IF NOT EXISTS idx_comps_func ON comps(function);
248 CREATE INDEX IF NOT EXISTS idx_executables_name ON executables(name);
249 CREATE INDEX IF NOT EXISTS idx_shell_functions_name ON shell_functions(name);
250 CREATE INDEX IF NOT EXISTS idx_named_dirs_name ON named_dirs(name);
251 "#,
252 )?;
253 self.migrate()?;
254 Ok(())
255 }
256
257 fn migrate(&self) -> rusqlite::Result<()> {
264 let has_ast: bool = self
265 .conn
266 .prepare("SELECT ast FROM autoloads LIMIT 0")
267 .is_ok();
268 if has_ast {
269 self.conn.execute_batch(
271 r#"
272 BEGIN;
273 CREATE TABLE autoloads_no_ast (
274 name TEXT PRIMARY KEY,
275 source TEXT NOT NULL,
276 offset INTEGER NOT NULL,
277 size INTEGER NOT NULL,
278 body TEXT
279 ) WITHOUT ROWID;
280 INSERT OR IGNORE INTO autoloads_no_ast (name, source, offset, size, body)
281 SELECT name, source, offset, size, body FROM autoloads;
282 DROP TABLE autoloads;
283 ALTER TABLE autoloads_no_ast RENAME TO autoloads;
284 COMMIT;
285 "#,
286 )?;
287 }
288 Ok(())
289 }
290
291 pub fn add_autoload(
297 &self,
298 name: &str,
299 source: &str,
300 offset: i64,
301 size: i64,
302 ) -> rusqlite::Result<()> {
303 self.conn.execute(
304 "INSERT OR REPLACE INTO autoloads (name, source, offset, size, body) VALUES (?1, ?2, ?3, ?4, NULL)",
305 params![name, source, offset, size],
306 )?;
307 Ok(())
308 }
309
310 pub fn add_autoload_with_body(
312 &self,
313 name: &str,
314 source: &str,
315 body: &str,
316 ) -> rusqlite::Result<()> {
317 self.conn.execute(
318 "INSERT OR REPLACE INTO autoloads (name, source, offset, size, body) VALUES (?1, ?2, 0, ?3, ?4)",
319 params![name, source, body.len() as i64, body],
320 )?;
321 Ok(())
322 }
323
324 pub fn add_autoloads_bulk(
326 &mut self,
327 autoloads: &[(String, String, i64, i64)],
328 ) -> rusqlite::Result<()> {
329 let tx = self.conn.transaction()?;
330 {
331 let mut stmt = tx.prepare(
332 "INSERT OR REPLACE INTO autoloads (name, source, offset, size, body) VALUES (?1, ?2, ?3, ?4, NULL)"
333 )?;
334 for (name, source, offset, size) in autoloads {
335 stmt.execute(params![name, source, offset, size])?;
336 }
337 }
338 tx.commit()?;
339 Ok(())
340 }
341
342 pub fn add_autoloads_with_bodies_bulk(
344 &mut self,
345 autoloads: &[(String, String, String)], ) -> rusqlite::Result<()> {
347 let tx = self.conn.transaction()?;
348 {
349 let mut stmt = tx.prepare(
350 "INSERT OR REPLACE INTO autoloads (name, source, offset, size, body) VALUES (?1, ?2, 0, ?3, ?4)"
351 )?;
352 for (name, source, body) in autoloads {
353 stmt.execute(params![name, source, body.len() as i64, body])?;
354 }
355 }
356 tx.commit()?;
357 Ok(())
358 }
359
360 pub fn get_autoload(&self, name: &str) -> rusqlite::Result<Option<AutoloadStub>> {
362 self.conn
363 .query_row(
364 "SELECT source, offset, size, body FROM autoloads WHERE name = ?1",
365 params![name],
366 |row| {
367 Ok(AutoloadStub {
368 name: name.to_string(),
369 source: row.get(0)?,
370 offset: row.get(1)?,
371 size: row.get(2)?,
372 body: row.get(3)?,
373 })
374 },
375 )
376 .optional()
377 }
378
379 pub fn get_autoload_body(&self, name: &str) -> rusqlite::Result<Option<String>> {
381 self.conn
382 .query_row(
383 "SELECT body FROM autoloads WHERE name = ?1",
384 params![name],
385 |row| row.get(0),
386 )
387 .optional()
388 }
389
390 pub fn count_autoloads_with_body(&self) -> rusqlite::Result<usize> {
395 self.conn.query_row(
396 "SELECT COUNT(*) FROM autoloads WHERE body IS NOT NULL",
397 [],
398 |row| row.get::<_, i64>(0).map(|n| n as usize),
399 )
400 }
401
402 pub fn get_autoload_bodies_excluding(
411 &self,
412 exclude: &std::collections::HashSet<String>,
413 limit: usize,
414 ) -> rusqlite::Result<Vec<(String, String)>> {
415 let mut stmt = self
416 .conn
417 .prepare("SELECT name, body FROM autoloads WHERE body IS NOT NULL ORDER BY name")?;
418 let rows = stmt.query_map([], |row| {
419 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
420 })?;
421 let mut out = Vec::with_capacity(limit.min(256));
422 for row in rows {
423 let (name, body) = row?;
424 if exclude.contains(&name) {
425 continue;
426 }
427 out.push((name, body));
428 if out.len() >= limit {
429 break;
430 }
431 }
432 Ok(out)
433 }
434
435 pub fn get_autoload_body_or_zwc(&self, name: &str) -> Option<String> {
440 let stub = self.get_autoload(name).ok()??;
441
442 if let Some(body) = stub.body {
444 return Some(body);
445 }
446
447 if stub.size > 0 && !stub.source.is_empty() {
449 return Self::read_function_from_zwc(&stub.source, stub.offset, stub.size);
450 }
451
452 None
453 }
454
455 fn read_function_from_zwc(zwc_path: &str, offset: i64, size: i64) -> Option<String> {
457 use std::io::{Read, Seek, SeekFrom};
458
459 let mut file = std::fs::File::open(zwc_path).ok()?;
460 file.seek(SeekFrom::Start(offset as u64)).ok()?;
461
462 let mut buf = vec![0u8; size as usize];
463 file.read_exact(&mut buf).ok()?;
464
465 match String::from_utf8(buf) {
469 Ok(s) => Some(s),
470 Err(e) => Some(String::from_utf8_lossy(e.as_bytes()).into_owned()),
471 }
472 }
473
474 pub fn autoload_count(&self) -> rusqlite::Result<i64> {
476 self.conn
477 .query_row("SELECT COUNT(*) FROM autoloads", [], |row| row.get(0))
478 }
479
480 pub fn list_autoloads(&self, limit: usize) -> rusqlite::Result<Vec<String>> {
482 let mut stmt = self.conn.prepare("SELECT name FROM autoloads LIMIT ?1")?;
483 let rows = stmt.query_map(params![limit as i64], |row| row.get(0))?;
484 rows.collect()
485 }
486
487 pub fn list_autoload_names(&self) -> rusqlite::Result<Vec<String>> {
489 let mut stmt = self.conn.prepare("SELECT name FROM autoloads")?;
490 let rows = stmt.query_map([], |row| row.get(0))?;
491 rows.collect()
492 }
493
494 pub fn set_zstyle(
500 &self,
501 pattern: &str,
502 style: &str,
503 values: &[String],
504 eval: bool,
505 ) -> rusqlite::Result<()> {
506 let value_json = serde_values_to_json(values);
507 self.conn.execute(
508 "INSERT OR REPLACE INTO zstyles (pattern, style, value, eval) VALUES (?1, ?2, ?3, ?4)",
509 params![pattern, style, value_json, eval as i32],
510 )?;
511 Ok(())
512 }
513
514 pub fn set_zstyles_bulk(
516 &mut self,
517 styles: &[(String, String, Vec<String>, bool)],
518 ) -> rusqlite::Result<()> {
519 let tx = self.conn.transaction()?;
520 {
521 let mut stmt = tx.prepare(
522 "INSERT OR REPLACE INTO zstyles (pattern, style, value, eval) VALUES (?1, ?2, ?3, ?4)"
523 )?;
524 for (pattern, style, values, eval) in styles {
525 let value_json = serde_values_to_json(values);
526 stmt.execute(params![pattern, style, value_json, *eval as i32])?;
527 }
528 }
529 tx.commit()?;
530 Ok(())
531 }
532
533 pub fn delete_zstyle(&self, pattern: &str, style: Option<&str>) -> rusqlite::Result<usize> {
535 if let Some(s) = style {
536 self.conn.execute(
537 "DELETE FROM zstyles WHERE pattern = ?1 AND style = ?2",
538 params![pattern, s],
539 )
540 } else {
541 self.conn
542 .execute("DELETE FROM zstyles WHERE pattern = ?1", params![pattern])
543 }
544 }
545
546 pub fn lookup_zstyle(
548 &self,
549 context: &str,
550 style: &str,
551 ) -> rusqlite::Result<Option<ZStyleEntry>> {
552 let mut stmt = self
553 .conn
554 .prepare("SELECT pattern, value, eval FROM zstyles WHERE style = ?1")?;
555
556 let entries: Vec<(String, String, bool)> = stmt
557 .query_map(params![style], |row| {
558 Ok((row.get(0)?, row.get(1)?, row.get::<_, i32>(2)? != 0))
559 })?
560 .filter_map(|r| r.ok())
561 .collect();
562
563 let mut best: Option<(i32, String, bool)> = None;
565 for (pattern, value, eval) in entries {
566 if pattern_matches_context(&pattern, context) {
567 let weight = calculate_pattern_weight(&pattern);
568 if best.is_none() || weight > best.as_ref().unwrap().0 {
569 best = Some((weight, value, eval));
570 }
571 }
572 }
573
574 Ok(best.map(|(_, value, eval)| ZStyleEntry {
575 values: serde_json_to_values(&value),
576 eval,
577 }))
578 }
579
580 #[allow(clippy::type_complexity)]
582 pub fn list_zstyles(&self) -> rusqlite::Result<Vec<(String, String, Vec<String>, bool)>> {
583 let mut stmt = self
584 .conn
585 .prepare("SELECT pattern, style, value, eval FROM zstyles ORDER BY pattern, style")?;
586 let rows = stmt.query_map([], |row| {
587 let pattern: String = row.get(0)?;
588 let style: String = row.get(1)?;
589 let value: String = row.get(2)?;
590 let eval: bool = row.get::<_, i32>(3)? != 0;
591 Ok((pattern, style, serde_json_to_values(&value), eval))
592 })?;
593 rows.collect()
594 }
595
596 pub fn zstyle_count(&self) -> rusqlite::Result<i64> {
598 self.conn
599 .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))
600 }
601
602 pub fn set_comp(&self, command: &str, function: &str) -> rusqlite::Result<()> {
608 self.conn.execute(
609 "INSERT OR REPLACE INTO comps (command, function) VALUES (?1, ?2)",
610 params![command, function],
611 )?;
612 Ok(())
613 }
614
615 pub fn set_comps_bulk(&mut self, comps: &[(String, String)]) -> rusqlite::Result<()> {
617 let tx = self.conn.transaction()?;
618 tx.execute("DELETE FROM comps", [])?;
620 tx.execute("DELETE FROM fts_comps", [])?;
621 {
622 let mut stmt = tx.prepare("INSERT INTO comps (command, function) VALUES (?1, ?2)")?;
623 let mut fts_stmt = tx.prepare("INSERT INTO fts_comps (command) VALUES (?1)")?;
624 for (command, function) in comps {
625 stmt.execute(params![command, function])?;
626 fts_stmt.execute(params![command])?;
627 }
628 }
629 tx.commit()
630 }
631
632 pub fn comps_prefix_fts(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
634 if prefix.is_empty() {
635 return self.comps_kv();
636 }
637 let pattern = format!("{}*", prefix);
639 let mut stmt = self.conn.prepare(
640 "SELECT c.command, c.function FROM fts_comps f, comps c WHERE f.command MATCH ?1 AND c.command = f.command"
641 )?;
642 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
643 rows.collect()
644 }
645
646 pub fn comps_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
648 if prefix.is_empty() {
649 return self.comps_kv();
650 }
651 let pattern = format!("{}%", prefix);
652 let mut stmt = self.conn.prepare(
653 "SELECT command, function FROM comps WHERE command LIKE ?1 ORDER BY command",
654 )?;
655 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
656 rows.collect()
657 }
658
659 pub fn get_comp(&self, command: &str) -> rusqlite::Result<Option<String>> {
661 self.conn
662 .query_row(
663 "SELECT function FROM comps WHERE command = ?1",
664 params![command],
665 |row| row.get(0),
666 )
667 .optional()
668 }
669
670 pub fn get_all_comps(&self) -> rusqlite::Result<HashMap<String, String>> {
672 let mut stmt = self.conn.prepare("SELECT command, function FROM comps")?;
673 let rows = stmt.query_map([], |row| {
674 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
675 })?;
676 let mut map = HashMap::new();
677 for row in rows {
678 let (k, v) = row?;
679 map.insert(k, v);
680 }
681 Ok(map)
682 }
683
684 pub fn comp_count(&self) -> rusqlite::Result<i64> {
686 self.conn
687 .query_row("SELECT COUNT(*) FROM comps", [], |row| row.get(0))
688 }
689
690 pub fn delete_comp(&self, command: &str) -> rusqlite::Result<usize> {
692 self.conn
693 .execute("DELETE FROM comps WHERE command = ?1", params![command])
694 }
695
696 pub fn set_patcomp(&self, pattern: &str, function: &str) -> rusqlite::Result<()> {
702 self.conn.execute(
703 "INSERT OR REPLACE INTO patcomps (pattern, function) VALUES (?1, ?2)",
704 params![pattern, function],
705 )?;
706 Ok(())
707 }
708
709 pub fn find_patcomp(&self, command: &str) -> rusqlite::Result<Option<String>> {
711 let mut stmt = self
712 .conn
713 .prepare("SELECT pattern, function FROM patcomps")?;
714 let rows = stmt.query_map([], |row| {
715 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
716 })?;
717
718 for row in rows {
719 let (pattern, function) = row?;
720 if glob_matches(&pattern, command) {
721 return Ok(Some(function));
722 }
723 }
724 Ok(None)
725 }
726
727 pub fn set_keycomp(&self, key: &str, function: &str) -> rusqlite::Result<()> {
733 self.conn.execute(
734 "INSERT OR REPLACE INTO keycomps (key, function) VALUES (?1, ?2)",
735 params![key, function],
736 )?;
737 Ok(())
738 }
739
740 pub fn get_keycomp(&self, key: &str) -> rusqlite::Result<Option<String>> {
742 self.conn
743 .query_row(
744 "SELECT function FROM keycomps WHERE key = ?1",
745 params![key],
746 |row| row.get(0),
747 )
748 .optional()
749 }
750
751 pub fn cache_results(&self, context: &str, data: &[u8], mtime: i64) -> rusqlite::Result<()> {
757 self.conn.execute(
758 "INSERT OR REPLACE INTO cache (context, data, mtime) VALUES (?1, ?2, ?3)",
759 params![context, data, mtime],
760 )?;
761 Ok(())
762 }
763
764 pub fn get_cached(&self, context: &str, max_age: i64) -> rusqlite::Result<Option<Vec<u8>>> {
766 let now = std::time::SystemTime::now()
767 .duration_since(std::time::UNIX_EPOCH)
768 .unwrap()
769 .as_secs() as i64;
770
771 self.conn
772 .query_row(
773 "SELECT data FROM cache WHERE context = ?1 AND mtime > ?2",
774 params![context, now - max_age],
775 |row| row.get(0),
776 )
777 .optional()
778 }
779
780 pub fn clear_stale_cache(&self, max_age: i64) -> rusqlite::Result<usize> {
782 let now = std::time::SystemTime::now()
783 .duration_since(std::time::UNIX_EPOCH)
784 .unwrap()
785 .as_secs() as i64;
786
787 self.conn
788 .execute("DELETE FROM cache WHERE mtime < ?1", params![now - max_age])
789 }
790
791 pub fn clear_cache(&self) -> rusqlite::Result<()> {
793 self.conn.execute("DELETE FROM cache", [])?;
794 Ok(())
795 }
796
797 pub fn vacuum(&self) -> rusqlite::Result<()> {
803 self.conn.execute("VACUUM", [])?;
804 Ok(())
805 }
806
807 pub fn stats(&self) -> rusqlite::Result<CacheStats> {
809 Ok(CacheStats {
810 autoloads: self.autoload_count()?,
811 zstyles: self.zstyle_count()?,
812 comps: self.comp_count()?,
813 patcomps: self
814 .conn
815 .query_row("SELECT COUNT(*) FROM patcomps", [], |r| r.get(0))?,
816 keycomps: self
817 .conn
818 .query_row("SELECT COUNT(*) FROM keycomps", [], |r| r.get(0))?,
819 services: self
820 .conn
821 .query_row("SELECT COUNT(*) FROM services", [], |r| r.get(0))?,
822 cache_entries: self
823 .conn
824 .query_row("SELECT COUNT(*) FROM cache", [], |r| r.get(0))?,
825 })
826 }
827}
828
829#[derive(Debug, Clone)]
831pub struct AutoloadStub {
832 pub name: String,
834 pub source: String,
836 pub offset: i64,
838 pub size: i64,
840 pub body: Option<String>,
842}
843
844#[derive(Debug, Clone)]
846pub struct ZStyleEntry {
847 pub values: Vec<String>,
849 pub eval: bool,
851}
852
853#[derive(Debug)]
855pub struct CacheStats {
856 pub autoloads: i64,
858 pub zstyles: i64,
860 pub comps: i64,
862 pub patcomps: i64,
864 pub keycomps: i64,
866 pub services: i64,
868 pub cache_entries: i64,
870}
871
872fn serde_values_to_json(values: &[String]) -> String {
874 let escaped: Vec<String> = values
875 .iter()
876 .map(|s| format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")))
877 .collect();
878 format!("[{}]", escaped.join(","))
879}
880
881fn serde_json_to_values(json: &str) -> Vec<String> {
883 let trimmed = json.trim();
884 if !trimmed.starts_with('[') || !trimmed.ends_with(']') {
885 return vec![json.to_string()];
886 }
887
888 let inner = &trimmed[1..trimmed.len() - 1];
889 if inner.is_empty() {
890 return vec![];
891 }
892
893 let mut values = Vec::new();
894 let mut current = String::new();
895 let mut in_string = false;
896 let mut escape = false;
897
898 for c in inner.chars() {
899 if escape {
900 current.push(c);
901 escape = false;
902 } else if c == '\\' {
903 escape = true;
904 } else if c == '"' {
905 in_string = !in_string;
906 } else if c == ',' && !in_string {
907 values.push(current.trim().to_string());
908 current = String::new();
909 } else {
910 current.push(c);
911 }
912 }
913 if !current.is_empty() {
914 values.push(current.trim().to_string());
915 }
916
917 values
918}
919
920fn pattern_matches_context(pattern: &str, context: &str) -> bool {
922 let pat_parts: Vec<&str> = pattern.split(':').collect();
923 let ctx_parts: Vec<&str> = context.split(':').collect();
924
925 if pat_parts.len() > ctx_parts.len() {
926 return false;
927 }
928
929 for (p, c) in pat_parts.iter().zip(ctx_parts.iter()) {
930 if *p != "*" && *p != *c {
931 return false;
932 }
933 }
934
935 true
936}
937
938fn calculate_pattern_weight(pattern: &str) -> i32 {
940 let parts: Vec<&str> = pattern.split(':').filter(|s| !s.is_empty()).collect();
941 let mut weight = parts.len() as i32 * 100;
942
943 for part in &parts {
944 if *part != "*" {
945 weight += 10;
946 }
947 }
948
949 weight
950}
951
952fn glob_matches(pattern: &str, text: &str) -> bool {
954 let mut pat_chars = pattern.chars().peekable();
955 let mut txt_chars = text.chars().peekable();
956
957 while let Some(p) = pat_chars.next() {
958 match p {
959 '*' => {
960 if pat_chars.peek().is_none() {
961 return true;
962 }
963 while txt_chars.peek().is_some() {
964 if glob_matches(
965 &pat_chars.clone().collect::<String>(),
966 &txt_chars.clone().collect::<String>(),
967 ) {
968 return true;
969 }
970 txt_chars.next();
971 }
972 return false;
973 }
974 '?' => {
975 if txt_chars.next().is_none() {
976 return false;
977 }
978 }
979 c => {
980 if txt_chars.next() != Some(c) {
981 return false;
982 }
983 }
984 }
985 }
986
987 txt_chars.peek().is_none()
988}
989
990impl CompsysCache {
996 pub fn comps_count(&self) -> rusqlite::Result<i64> {
998 self.comp_count()
999 }
1000
1001 pub fn comps_keys(&self) -> rusqlite::Result<Vec<String>> {
1003 let mut stmt = self
1004 .conn
1005 .prepare("SELECT command FROM comps ORDER BY command")?;
1006 let rows = stmt.query_map([], |row| row.get(0))?;
1007 rows.collect()
1008 }
1009
1010 pub fn comps_values(&self) -> rusqlite::Result<Vec<String>> {
1012 let mut stmt = self
1013 .conn
1014 .prepare("SELECT function FROM comps ORDER BY command")?;
1015 let rows = stmt.query_map([], |row| row.get(0))?;
1016 rows.collect()
1017 }
1018
1019 pub fn comps_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
1021 let mut stmt = self
1022 .conn
1023 .prepare("SELECT command, function FROM comps ORDER BY command")?;
1024 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1025 rows.collect()
1026 }
1027
1028 pub fn patcomps_count(&self) -> rusqlite::Result<i64> {
1032 self.conn
1033 .query_row("SELECT COUNT(*) FROM patcomps", [], |row| row.get(0))
1034 }
1035
1036 pub fn patcomps_keys(&self) -> rusqlite::Result<Vec<String>> {
1038 let mut stmt = self.conn.prepare("SELECT pattern FROM patcomps")?;
1039 let rows = stmt.query_map([], |row| row.get(0))?;
1040 rows.collect()
1041 }
1042
1043 pub fn patcomps_kv(&self) -> rusqlite::Result<Vec<(String, String)>> {
1045 let mut stmt = self
1046 .conn
1047 .prepare("SELECT pattern, function FROM patcomps")?;
1048 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1049 rows.collect()
1050 }
1051
1052 pub fn set_service(&self, command: &str, service: &str) -> rusqlite::Result<()> {
1056 self.conn.execute(
1057 "INSERT OR REPLACE INTO services (command, service) VALUES (?1, ?2)",
1058 params![command, service],
1059 )?;
1060 Ok(())
1061 }
1062
1063 pub fn get_service(&self, command: &str) -> rusqlite::Result<Option<String>> {
1065 self.conn
1066 .query_row(
1067 "SELECT service FROM services WHERE command = ?1",
1068 params![command],
1069 |row| row.get(0),
1070 )
1071 .optional()
1072 }
1073
1074 pub fn services_count(&self) -> rusqlite::Result<i64> {
1076 self.conn
1077 .query_row("SELECT COUNT(*) FROM services", [], |row| row.get(0))
1078 }
1079
1080 pub fn services_keys(&self) -> rusqlite::Result<Vec<String>> {
1082 let mut stmt = self.conn.prepare("SELECT command FROM services")?;
1083 let rows = stmt.query_map([], |row| row.get(0))?;
1084 rows.collect()
1085 }
1086
1087 pub fn set_services_bulk(&mut self, services: &[(String, String)]) -> rusqlite::Result<()> {
1089 let tx = self.conn.transaction()?;
1090 {
1091 let mut stmt =
1092 tx.prepare("INSERT OR REPLACE INTO services (command, service) VALUES (?1, ?2)")?;
1093 for (command, service) in services {
1094 stmt.execute(params![command, service])?;
1095 }
1096 }
1097 tx.commit()?;
1098 Ok(())
1099 }
1100
1101 pub fn compautos_count(&self) -> rusqlite::Result<i64> {
1105 self.autoload_count()
1106 }
1107
1108 pub fn compautos_keys(&self) -> rusqlite::Result<Vec<String>> {
1110 let mut stmt = self.conn.prepare("SELECT name FROM autoloads")?;
1111 let rows = stmt.query_map([], |row| row.get(0))?;
1112 rows.collect()
1113 }
1114
1115 pub fn has_executables(&self) -> rusqlite::Result<bool> {
1121 let count: i64 = self
1122 .conn
1123 .query_row("SELECT COUNT(*) FROM executables", [], |row| row.get(0))?;
1124 Ok(count > 0)
1125 }
1126
1127 pub fn set_executables_bulk(
1129 &mut self,
1130 executables: &[(String, String)],
1131 ) -> rusqlite::Result<()> {
1132 let tx = self.conn.transaction()?;
1133 tx.execute("DELETE FROM executables", [])?;
1134 tx.execute("DELETE FROM fts_executables", [])?;
1135 {
1136 let mut stmt =
1137 tx.prepare("INSERT OR IGNORE INTO executables (name, path) VALUES (?1, ?2)")?;
1138 let mut fts_stmt =
1139 tx.prepare("INSERT OR IGNORE INTO fts_executables (name) VALUES (?1)")?;
1140 for (name, path) in executables {
1141 stmt.execute(params![name, path])?;
1142 fts_stmt.execute(params![name])?;
1143 }
1144 }
1145 tx.commit()
1146 }
1147
1148 pub fn get_executable_names(&self) -> rusqlite::Result<std::collections::HashSet<String>> {
1150 let mut stmt = self.conn.prepare("SELECT name FROM executables")?;
1151 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
1152 rows.collect::<Result<std::collections::HashSet<_>, _>>()
1153 }
1154
1155 pub fn has_executable(&self, name: &str) -> rusqlite::Result<bool> {
1157 let exists: i64 = self.conn.query_row(
1159 "SELECT EXISTS(SELECT 1 FROM executables WHERE name = ?1)",
1160 params![name],
1161 |row| row.get(0),
1162 )?;
1163 Ok(exists == 1)
1164 }
1165
1166 pub fn get_executable_path(&self, name: &str) -> rusqlite::Result<Option<String>> {
1168 self.conn
1169 .query_row(
1170 "SELECT path FROM executables WHERE name = ?1",
1171 params![name],
1172 |row| row.get(0),
1173 )
1174 .optional()
1175 }
1176
1177 pub fn get_executables_prefix_fts(
1179 &self,
1180 prefix: &str,
1181 ) -> rusqlite::Result<Vec<(String, String)>> {
1182 if prefix.is_empty() {
1183 let mut stmt = self.conn.prepare("SELECT name, path FROM executables")?;
1184 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1185 return rows.collect();
1186 }
1187 let pattern = format!("{}*", prefix);
1188 let mut stmt = self.conn.prepare(
1189 "SELECT e.name, e.path FROM fts_executables f, executables e WHERE f.name MATCH ?1 AND e.name = f.name"
1190 )?;
1191 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1192 rows.collect()
1193 }
1194
1195 pub fn get_executables_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
1197 if prefix.is_empty() {
1198 let mut stmt = self
1199 .conn
1200 .prepare("SELECT name, path FROM executables ORDER BY name")?;
1201 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1202 return rows.collect();
1203 }
1204 let pattern = format!("{}%", prefix);
1205 let mut stmt = self
1206 .conn
1207 .prepare("SELECT name, path FROM executables WHERE name LIKE ?1 ORDER BY name")?;
1208 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1209 rows.collect()
1210 }
1211
1212 pub fn executables_count(&self) -> rusqlite::Result<i64> {
1214 self.conn
1215 .query_row("SELECT COUNT(*) FROM executables", [], |row| row.get(0))
1216 }
1217
1218 pub fn has_named_dirs(&self) -> rusqlite::Result<bool> {
1224 let count: i64 = self
1225 .conn
1226 .query_row("SELECT COUNT(*) FROM named_dirs", [], |row| row.get(0))?;
1227 Ok(count > 0)
1228 }
1229
1230 pub fn set_named_dirs_bulk(&mut self, dirs: &[(String, String)]) -> rusqlite::Result<()> {
1232 let tx = self.conn.transaction()?;
1233 tx.execute("DELETE FROM named_dirs", [])?;
1234 {
1235 let mut stmt = tx.prepare("INSERT INTO named_dirs (name, path) VALUES (?1, ?2)")?;
1236 for (name, path) in dirs {
1237 stmt.execute(params![name, path])?;
1238 }
1239 }
1240 tx.commit()
1241 }
1242
1243 pub fn get_named_dirs(&self) -> rusqlite::Result<Vec<(String, String)>> {
1245 let mut stmt = self
1246 .conn
1247 .prepare("SELECT name, path FROM named_dirs ORDER BY name")?;
1248 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1249 rows.collect()
1250 }
1251
1252 pub fn get_named_dirs_prefix(&self, prefix: &str) -> rusqlite::Result<Vec<(String, String)>> {
1254 if prefix.is_empty() {
1255 return self.get_named_dirs();
1256 }
1257 let pattern = format!("{}%", prefix);
1258 let mut stmt = self
1259 .conn
1260 .prepare("SELECT name, path FROM named_dirs WHERE name LIKE ?1 ORDER BY name")?;
1261 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1262 rows.collect()
1263 }
1264
1265 pub fn named_dirs_count(&self) -> rusqlite::Result<i64> {
1267 self.conn
1268 .query_row("SELECT COUNT(*) FROM named_dirs", [], |row| row.get(0))
1269 }
1270
1271 pub fn has_shell_functions(&self) -> rusqlite::Result<bool> {
1277 let count: i64 =
1278 self.conn
1279 .query_row("SELECT COUNT(*) FROM shell_functions", [], |row| row.get(0))?;
1280 Ok(count > 0)
1281 }
1282
1283 pub fn set_shell_functions_bulk(&mut self, funcs: &[(String, String)]) -> rusqlite::Result<()> {
1285 let tx = self.conn.transaction()?;
1286 tx.execute("DELETE FROM shell_functions", [])?;
1287 tx.execute("DELETE FROM fts_shell_functions", [])?;
1288 {
1289 let mut stmt =
1290 tx.prepare("INSERT OR IGNORE INTO shell_functions (name, source) VALUES (?1, ?2)")?;
1291 let mut fts_stmt =
1292 tx.prepare("INSERT OR IGNORE INTO fts_shell_functions (name) VALUES (?1)")?;
1293 for (name, source) in funcs {
1294 stmt.execute(params![name, source])?;
1295 fts_stmt.execute(params![name])?;
1296 }
1297 }
1298 tx.commit()
1299 }
1300
1301 pub fn get_shell_function_names(&self) -> rusqlite::Result<Vec<String>> {
1303 let mut stmt = self
1304 .conn
1305 .prepare("SELECT name FROM shell_functions ORDER BY name")?;
1306 let rows = stmt.query_map([], |row| row.get(0))?;
1307 rows.collect()
1308 }
1309
1310 pub fn get_shell_functions(&self) -> rusqlite::Result<Vec<(String, String)>> {
1312 let mut stmt = self
1313 .conn
1314 .prepare("SELECT name, source FROM shell_functions ORDER BY name")?;
1315 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?;
1316 rows.collect()
1317 }
1318
1319 pub fn get_shell_functions_prefix_fts(
1321 &self,
1322 prefix: &str,
1323 ) -> rusqlite::Result<Vec<(String, String)>> {
1324 if prefix.is_empty() {
1325 return self.get_shell_functions();
1326 }
1327 let pattern = format!("{}*", prefix);
1328 let mut stmt = self.conn.prepare(
1329 "SELECT s.name, s.source FROM fts_shell_functions f, shell_functions s WHERE f.name MATCH ?1 AND s.name = f.name ORDER BY s.name"
1330 )?;
1331 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1332 rows.collect()
1333 }
1334
1335 pub fn get_shell_functions_prefix(
1337 &self,
1338 prefix: &str,
1339 ) -> rusqlite::Result<Vec<(String, String)>> {
1340 if prefix.is_empty() {
1341 return self.get_shell_functions();
1342 }
1343 let pattern = format!("{}%", prefix);
1344 let mut stmt = self
1345 .conn
1346 .prepare("SELECT name, source FROM shell_functions WHERE name LIKE ?1 ORDER BY name")?;
1347 let rows = stmt.query_map(params![pattern], |row| Ok((row.get(0)?, row.get(1)?)))?;
1348 rows.collect()
1349 }
1350
1351 pub fn shell_functions_count(&self) -> rusqlite::Result<i64> {
1353 self.conn
1354 .query_row("SELECT COUNT(*) FROM shell_functions", [], |row| row.get(0))
1355 }
1356
1357 pub fn set_metadata(&self, key: &str, value: &str) -> rusqlite::Result<()> {
1363 self.conn.execute(
1364 "INSERT OR REPLACE INTO metadata (key, value) VALUES (?1, ?2)",
1365 params![key, value],
1366 )?;
1367 Ok(())
1368 }
1369
1370 pub fn get_metadata(&self, key: &str) -> rusqlite::Result<Option<String>> {
1372 self.conn
1373 .query_row(
1374 "SELECT value FROM metadata WHERE key = ?1",
1375 params![key],
1376 |row| row.get(0),
1377 )
1378 .optional()
1379 }
1380
1381 pub fn has_zstyles(&self) -> rusqlite::Result<bool> {
1387 let count: i64 = self
1388 .conn
1389 .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))?;
1390 Ok(count > 0)
1391 }
1392
1393 pub fn zstyles_count(&self) -> rusqlite::Result<i64> {
1395 self.conn
1396 .query_row("SELECT COUNT(*) FROM zstyles", [], |row| row.get(0))
1397 }
1398
1399 pub fn get_all_zstyles(&self) -> rusqlite::Result<Vec<(String, String, String)>> {
1401 let mut stmt = self
1402 .conn
1403 .prepare("SELECT pattern, style, value FROM zstyles ORDER BY pattern, style")?;
1404 let rows = stmt.query_map([], |row| Ok((row.get(0)?, row.get(1)?, row.get(2)?)))?;
1405 rows.collect()
1406 }
1407}
1408
1409#[cfg(test)]
1410mod tests {
1411 use super::*;
1412
1413 #[test]
1414 fn test_cache_basic() {
1415 let cache = CompsysCache::memory().unwrap();
1416
1417 cache
1418 .add_autoload("_git", "more_src.zwc", 1024, 5000)
1419 .unwrap();
1420 cache
1421 .add_autoload("_docker", "more_src.zwc", 6024, 3000)
1422 .unwrap();
1423
1424 let stub = cache.get_autoload("_git").unwrap().unwrap();
1425 assert_eq!(stub.source, "more_src.zwc");
1426 assert_eq!(stub.offset, 1024);
1427
1428 assert!(cache.get_autoload("_nonexistent").unwrap().is_none());
1429 }
1430
1431 #[test]
1432 fn test_zstyle_cache() {
1433 let cache = CompsysCache::memory().unwrap();
1434
1435 cache
1436 .set_zstyle(":completion:*", "menu", &["select".to_string()], false)
1437 .unwrap();
1438 cache
1439 .set_zstyle(
1440 ":completion:*:descriptions",
1441 "format",
1442 &["%d".to_string()],
1443 false,
1444 )
1445 .unwrap();
1446
1447 let entry = cache
1448 .lookup_zstyle(":completion:foo", "menu")
1449 .unwrap()
1450 .unwrap();
1451 assert_eq!(entry.values, vec!["select"]);
1452
1453 let entry = cache
1454 .lookup_zstyle(":completion:foo:descriptions", "format")
1455 .unwrap()
1456 .unwrap();
1457 assert_eq!(entry.values, vec!["%d"]);
1458 }
1459
1460 #[test]
1461 fn test_zstyle_specificity() {
1462 let cache = CompsysCache::memory().unwrap();
1463
1464 cache
1465 .set_zstyle(":completion:*", "menu", &["no".to_string()], false)
1466 .unwrap();
1467 cache
1468 .set_zstyle(
1469 ":completion:*:*:*:default",
1470 "menu",
1471 &["yes".to_string()],
1472 false,
1473 )
1474 .unwrap();
1475
1476 let entry = cache
1477 .lookup_zstyle(":completion:foo:bar:baz:default", "menu")
1478 .unwrap()
1479 .unwrap();
1480 assert_eq!(entry.values, vec!["yes"]);
1481 }
1482
1483 #[test]
1484 fn test_comps_cache() {
1485 let mut cache = CompsysCache::memory().unwrap();
1486
1487 let comps = vec![
1488 ("git".to_string(), "_git".to_string()),
1489 ("docker".to_string(), "_docker".to_string()),
1490 ("cargo".to_string(), "_cargo".to_string()),
1491 ];
1492 cache.set_comps_bulk(&comps).unwrap();
1493
1494 assert_eq!(cache.get_comp("git").unwrap(), Some("_git".to_string()));
1495 assert_eq!(
1496 cache.get_comp("docker").unwrap(),
1497 Some("_docker".to_string())
1498 );
1499 assert!(cache.get_comp("nonexistent").unwrap().is_none());
1500
1501 assert_eq!(cache.comp_count().unwrap(), 3);
1502 }
1503
1504 #[test]
1505 fn test_bulk_autoloads() {
1506 let mut cache = CompsysCache::memory().unwrap();
1507
1508 let autoloads: Vec<(String, String, i64, i64)> = (0..1000)
1509 .map(|i| (format!("_func{}", i), "test.zwc".to_string(), i * 100, 100))
1510 .collect();
1511
1512 cache.add_autoloads_bulk(&autoloads).unwrap();
1513 assert_eq!(cache.autoload_count().unwrap(), 1000);
1514
1515 let stub = cache.get_autoload("_func500").unwrap().unwrap();
1516 assert_eq!(stub.offset, 50000);
1517 assert!(stub.body.is_none()); }
1519
1520 #[test]
1521 fn test_autoload_with_body() {
1522 let cache = CompsysCache::memory().unwrap();
1523
1524 let body = r#"
1525local -a opts
1526opts=(--help --version --verbose)
1527_arguments $opts
1528"#;
1529 cache
1530 .add_autoload_with_body("_mycommand", "/usr/share/zsh/functions/_mycommand", body)
1531 .unwrap();
1532
1533 let stub = cache.get_autoload("_mycommand").unwrap().unwrap();
1534 assert_eq!(stub.body.as_deref(), Some(body));
1535 assert_eq!(stub.size, body.len() as i64);
1536
1537 let direct_body = cache.get_autoload_body("_mycommand").unwrap();
1539 assert_eq!(direct_body.as_deref(), Some(body));
1540 }
1541
1542 #[test]
1543 fn test_bulk_autoloads_with_bodies() {
1544 let mut cache = CompsysCache::memory().unwrap();
1545
1546 let autoloads: Vec<(String, String, String)> = (0..100)
1547 .map(|i| {
1548 (
1549 format!("_func{}", i),
1550 format!("/path/to/_func{}", i),
1551 format!("# Function {}\necho hello", i),
1552 )
1553 })
1554 .collect();
1555
1556 cache.add_autoloads_with_bodies_bulk(&autoloads).unwrap();
1557 assert_eq!(cache.autoload_count().unwrap(), 100);
1558
1559 let stub = cache.get_autoload("_func50").unwrap().unwrap();
1560 assert!(stub.body.is_some());
1561 assert!(stub.body.unwrap().contains("Function 50"));
1562 }
1563
1564 #[test]
1565 fn test_get_autoload_body_or_zwc_with_body() {
1566 let cache = CompsysCache::memory().unwrap();
1567
1568 let body = "echo from sqlite";
1569 cache
1570 .add_autoload_with_body("_cached", "/some/path", body)
1571 .unwrap();
1572
1573 let result = cache.get_autoload_body_or_zwc("_cached");
1575 assert_eq!(result, Some(body.to_string()));
1576 }
1577
1578 #[test]
1579 fn test_get_autoload_body_or_zwc_no_body() {
1580 let cache = CompsysCache::memory().unwrap();
1581
1582 cache
1584 .add_autoload("_nocache", "nonexistent.zwc", 0, 100)
1585 .unwrap();
1586
1587 let result = cache.get_autoload_body_or_zwc("_nocache");
1589 assert!(result.is_none());
1590 }
1591
1592 #[test]
1593 fn test_get_autoload_body_or_zwc_not_found() {
1594 let cache = CompsysCache::memory().unwrap();
1595
1596 let result = cache.get_autoload_body_or_zwc("_nonexistent");
1598 assert!(result.is_none());
1599 }
1600
1601 #[test]
1602 fn test_patcomp() {
1603 let cache = CompsysCache::memory().unwrap();
1604
1605 cache.set_patcomp("git-*", "_git").unwrap();
1606 cache.set_patcomp("docker-*", "_docker").unwrap();
1607
1608 assert_eq!(
1609 cache.find_patcomp("git-commit").unwrap(),
1610 Some("_git".to_string())
1611 );
1612 assert_eq!(
1613 cache.find_patcomp("docker-compose").unwrap(),
1614 Some("_docker".to_string())
1615 );
1616 assert!(cache.find_patcomp("cargo").unwrap().is_none());
1617 }
1618
1619 #[test]
1620 fn test_glob_matches() {
1621 assert!(glob_matches("git-*", "git-commit"));
1622 assert!(glob_matches("*-compose", "docker-compose"));
1623 assert!(!glob_matches("*.rs", "zle_main"));
1626 assert!(!glob_matches("git-*", "docker-compose"));
1627 assert!(glob_matches("???", "abc"));
1628 assert!(!glob_matches("???", "abcd"));
1629 }
1630
1631 #[test]
1632 fn test_json_serde() {
1633 let values = vec!["hello".to_string(), "world".to_string()];
1634 let json = serde_values_to_json(&values);
1635 let back = serde_json_to_values(&json);
1636 assert_eq!(back, values);
1637
1638 let values = vec!["with \"quotes\"".to_string()];
1639 let json = serde_values_to_json(&values);
1640 let back = serde_json_to_values(&json);
1641 assert_eq!(back, vec!["with \"quotes\""]);
1642 }
1643
1644 #[test]
1645 fn test_stats() {
1646 let mut cache = CompsysCache::memory().unwrap();
1647
1648 cache.add_autoload("_git", "test.zwc", 0, 100).unwrap();
1649 cache
1650 .set_zstyle(":completion:*", "menu", &["select".to_string()], false)
1651 .unwrap();
1652 cache.set_comp("git", "_git").unwrap();
1653
1654 let stats = cache.stats().unwrap();
1655 assert_eq!(stats.autoloads, 1);
1656 assert_eq!(stats.zstyles, 1);
1657 assert_eq!(stats.comps, 1);
1658 }
1659
1660 #[test]
1661 fn test_large_scale() {
1662 let mut cache = CompsysCache::memory().unwrap();
1663
1664 let autoloads: Vec<(String, String, i64, i64)> = (0..10000)
1666 .map(|i| {
1667 (
1668 format!("_func{}", i),
1669 format!("src{}.zwc", i % 10),
1670 i * 50,
1671 50,
1672 )
1673 })
1674 .collect();
1675
1676 cache.add_autoloads_bulk(&autoloads).unwrap();
1677
1678 let stub = cache.get_autoload("_func9999").unwrap().unwrap();
1680 assert_eq!(stub.offset, 9999 * 50);
1681
1682 assert_eq!(cache.autoload_count().unwrap(), 10000);
1683 }
1684
1685 #[test]
1686 fn test_executables_cache() {
1687 let mut cache = CompsysCache::memory().unwrap();
1688
1689 let executables = vec![
1690 ("ls".to_string(), "/bin/ls".to_string()),
1691 ("cat".to_string(), "/bin/cat".to_string()),
1692 ("git".to_string(), "/usr/bin/git".to_string()),
1693 ];
1694 cache.set_executables_bulk(&executables).unwrap();
1695
1696 assert!(cache.has_executables().unwrap());
1697 assert!(cache.has_executable("ls").unwrap());
1698 assert!(cache.has_executable("git").unwrap());
1699 assert!(!cache.has_executable("nonexistent").unwrap());
1700
1701 assert_eq!(
1702 cache.get_executable_path("ls").unwrap(),
1703 Some("/bin/ls".to_string())
1704 );
1705 assert_eq!(cache.executables_count().unwrap(), 3);
1706 }
1707
1708 #[test]
1709 fn test_executables_prefix_search() {
1710 let mut cache = CompsysCache::memory().unwrap();
1711
1712 let executables = vec![
1713 ("git".to_string(), "/usr/bin/git".to_string()),
1714 ("gitk".to_string(), "/usr/bin/gitk".to_string()),
1715 ("grep".to_string(), "/bin/grep".to_string()),
1716 ("gzip".to_string(), "/bin/gzip".to_string()),
1717 ];
1718 cache.set_executables_bulk(&executables).unwrap();
1719
1720 let git_cmds = cache.get_executables_prefix_fts("git").unwrap();
1722 assert_eq!(git_cmds.len(), 2);
1723 assert!(git_cmds.iter().any(|(name, _)| name == "git"));
1724 assert!(git_cmds.iter().any(|(name, _)| name == "gitk"));
1725
1726 let g_cmds = cache.get_executables_prefix_fts("g").unwrap();
1727 assert_eq!(g_cmds.len(), 4);
1728 }
1729
1730 #[test]
1731 fn test_named_dirs_cache() {
1732 let mut cache = CompsysCache::memory().unwrap();
1733
1734 let dirs = vec![
1735 ("proj".to_string(), "/home/user/projects".to_string()),
1736 ("docs".to_string(), "/home/user/documents".to_string()),
1737 ];
1738 cache.set_named_dirs_bulk(&dirs).unwrap();
1739
1740 assert!(cache.has_named_dirs().unwrap());
1741
1742 let all = cache.get_named_dirs().unwrap();
1743 assert_eq!(all.len(), 2);
1744
1745 let p_dirs = cache.get_named_dirs_prefix("p").unwrap();
1746 assert_eq!(p_dirs.len(), 1);
1747 assert_eq!(p_dirs[0].0, "proj");
1748 }
1749
1750 #[test]
1751 fn test_shell_functions_cache() {
1752 let mut cache = CompsysCache::memory().unwrap();
1753
1754 let functions = vec![
1755 ("myFunc".to_string(), "/home/user/.zshrc".to_string()),
1756 (
1757 "zpwrClearList".to_string(),
1758 "/home/user/.zpwr/autoload".to_string(),
1759 ),
1760 (
1761 "zpwrTop".to_string(),
1762 "/home/user/.zpwr/autoload".to_string(),
1763 ),
1764 ];
1765 cache.set_shell_functions_bulk(&functions).unwrap();
1766
1767 assert!(cache.has_shell_functions().unwrap());
1768 assert_eq!(cache.shell_functions_count().unwrap(), 3);
1769
1770 let zpwr = cache.get_shell_functions_prefix("zpwr").unwrap();
1771 assert_eq!(zpwr.len(), 2);
1772 assert!(zpwr.iter().any(|(name, _)| name == "zpwrClearList"));
1774 assert!(zpwr.iter().any(|(name, _)| name == "zpwrTop"));
1775 }
1776
1777 #[test]
1778 fn test_metadata() {
1779 let cache = CompsysCache::memory().unwrap();
1780
1781 cache.set_metadata("version", "1.0.0").unwrap();
1782 cache.set_metadata("build_time", "2026-04-22").unwrap();
1783
1784 assert_eq!(
1785 cache.get_metadata("version").unwrap(),
1786 Some("1.0.0".to_string())
1787 );
1788 assert_eq!(
1789 cache.get_metadata("build_time").unwrap(),
1790 Some("2026-04-22".to_string())
1791 );
1792 assert_eq!(cache.get_metadata("nonexistent").unwrap(), None);
1793 }
1794
1795 #[test]
1796 fn test_comps_keys() {
1797 let mut cache = CompsysCache::memory().unwrap();
1798
1799 let comps = vec![
1800 ("git".to_string(), "_git".to_string()),
1801 ("docker".to_string(), "_docker".to_string()),
1802 ];
1803 cache.set_comps_bulk(&comps).unwrap();
1804
1805 let keys = cache.comps_keys().unwrap();
1806 assert_eq!(keys.len(), 2);
1807 assert!(keys.contains(&"docker".to_string()));
1808 assert!(keys.contains(&"git".to_string()));
1809 }
1810
1811 #[test]
1812 fn test_comps_prefix() {
1813 let mut cache = CompsysCache::memory().unwrap();
1814
1815 let comps = vec![
1816 ("git".to_string(), "_git".to_string()),
1817 ("gitk".to_string(), "_gitk".to_string()),
1818 ("docker".to_string(), "_docker".to_string()),
1819 ];
1820 cache.set_comps_bulk(&comps).unwrap();
1821
1822 let git_comps = cache.comps_prefix("git").unwrap();
1823 assert_eq!(git_comps.len(), 2);
1824 }
1825
1826 #[test]
1827 fn test_zstyles_bulk() {
1828 let mut cache = CompsysCache::memory().unwrap();
1829
1830 let styles = vec![
1831 (
1832 ":completion:*".to_string(),
1833 "menu".to_string(),
1834 vec!["select".to_string()],
1835 false,
1836 ),
1837 (
1838 ":completion:*".to_string(),
1839 "verbose".to_string(),
1840 vec!["yes".to_string()],
1841 false,
1842 ),
1843 (
1844 ":completion:*:descriptions".to_string(),
1845 "format".to_string(),
1846 vec!["%d".to_string()],
1847 false,
1848 ),
1849 ];
1850 cache.set_zstyles_bulk(&styles).unwrap();
1851
1852 assert!(cache.has_zstyles().unwrap());
1853 assert_eq!(cache.zstyles_count().unwrap(), 3);
1854 }
1855
1856 #[test]
1857 fn test_services() {
1858 let cache = CompsysCache::memory().unwrap();
1859
1860 cache.set_service("git", "scm").unwrap();
1861 cache.set_service("hg", "scm").unwrap();
1862
1863 assert_eq!(cache.get_service("git").unwrap(), Some("scm".to_string()));
1864 assert_eq!(cache.get_service("unknown").unwrap(), None);
1865 }
1866
1867 #[test]
1868 fn test_cache_overwrite() {
1869 let cache = CompsysCache::memory().unwrap();
1870
1871 cache.set_comp("git", "_git_old").unwrap();
1872 assert_eq!(cache.get_comp("git").unwrap(), Some("_git_old".to_string()));
1873
1874 cache.set_comp("git", "_git_new").unwrap();
1875 assert_eq!(cache.get_comp("git").unwrap(), Some("_git_new".to_string()));
1876 }
1877
1878 #[test]
1879 fn test_executable_names() {
1880 let mut cache = CompsysCache::memory().unwrap();
1881
1882 let executables = vec![
1883 ("alpha".to_string(), "/bin/alpha".to_string()),
1884 ("beta".to_string(), "/bin/beta".to_string()),
1885 ("gamma".to_string(), "/bin/gamma".to_string()),
1886 ];
1887 cache.set_executables_bulk(&executables).unwrap();
1888
1889 let names = cache.get_executable_names().unwrap();
1890 assert_eq!(names.len(), 3);
1891 assert!(names.contains("alpha"));
1893 assert!(names.contains("beta"));
1894 assert!(names.contains("gamma"));
1895 }
1896}