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