1#![allow(clippy::missing_errors_doc)]
2use std::path::Path;
3
4use rusqlite::{params, Connection};
5
6#[derive(Debug, Clone)]
8pub struct WorkspaceInfo {
9 pub path: String,
11 pub language: String,
13 pub status: String,
15 pub last_used_at: Option<i64>,
17}
18
19#[derive(Debug, Clone)]
21pub struct CachedSymbol {
22 pub name: String,
23 pub kind: String,
24 pub path: String,
25 pub range_start_line: u32,
26 pub range_start_col: u32,
27 pub range_end_line: u32,
28 pub range_end_col: u32,
29 pub parent_name: Option<String>,
30}
31
32pub struct IndexStore {
34 conn: Connection,
35}
36
37impl IndexStore {
38 pub fn open(db_path: &Path) -> rusqlite::Result<Self> {
43 let conn = Connection::open(db_path)?;
44 conn.execute_batch(
45 "PRAGMA journal_mode = WAL;
46 PRAGMA synchronous = normal;
47 PRAGMA temp_store = memory;
48 PRAGMA cache_size = -32000;
49 PRAGMA mmap_size = 30000000000;
50 PRAGMA foreign_keys = ON;",
51 )?;
52 let store = Self { conn };
53 store.create_tables()?;
54 Ok(store)
55 }
56
57 #[cfg(test)]
59 pub fn open_in_memory() -> rusqlite::Result<Self> {
60 let conn = Connection::open_in_memory()?;
61 conn.execute_batch("PRAGMA foreign_keys=ON;")?;
62 let store = Self { conn };
63 store.create_tables()?;
64 Ok(store)
65 }
66
67 fn create_tables(&self) -> rusqlite::Result<()> {
68 self.conn.execute_batch(
69 "CREATE TABLE IF NOT EXISTS files (
70 path TEXT PRIMARY KEY,
71 blake3_hash TEXT NOT NULL,
72 indexed_at INTEGER NOT NULL
73 );
74
75 CREATE TABLE IF NOT EXISTS symbols (
76 id INTEGER PRIMARY KEY AUTOINCREMENT,
77 name TEXT NOT NULL,
78 kind TEXT NOT NULL,
79 path TEXT NOT NULL,
80 range_start_line INTEGER NOT NULL,
81 range_start_col INTEGER NOT NULL,
82 range_end_line INTEGER NOT NULL,
83 range_end_col INTEGER NOT NULL,
84 parent_name TEXT,
85 FOREIGN KEY (path) REFERENCES files(path) ON DELETE CASCADE
86 );
87
88 CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name);
89 CREATE INDEX IF NOT EXISTS idx_symbols_path ON symbols(path);
90
91 CREATE TABLE IF NOT EXISTS lsp_cache (
92 request_hash TEXT PRIMARY KEY,
93 response_json TEXT NOT NULL,
94 created_at INTEGER NOT NULL
95 );
96
97 CREATE TABLE IF NOT EXISTS workspaces (
98 path TEXT PRIMARY KEY,
99 language TEXT NOT NULL,
100 status TEXT NOT NULL DEFAULT 'discovered',
101 last_used_at INTEGER
102 );
103
104 CREATE TABLE IF NOT EXISTS server_capabilities (
105 server_name TEXT NOT NULL PRIMARY KEY,
106 workspace_folders_supported INTEGER NOT NULL DEFAULT 0,
107 work_done_progress INTEGER NOT NULL DEFAULT 0
108 );",
109 )
110 }
111
112 pub fn upsert_file(&self, path: &str, hash: &str) -> rusqlite::Result<()> {
114 let now = std::time::SystemTime::now()
115 .duration_since(std::time::UNIX_EPOCH)
116 .unwrap_or_default()
117 .as_secs();
118
119 self.conn.execute(
120 "INSERT INTO files (path, blake3_hash, indexed_at)
121 VALUES (?1, ?2, ?3)
122 ON CONFLICT(path) DO UPDATE SET blake3_hash=?2, indexed_at=?3",
123 params![path, hash, now.cast_signed()],
124 )?;
125 Ok(())
126 }
127
128 pub fn insert_symbols(&self, path: &str, symbols: &[CachedSymbol]) -> rusqlite::Result<()> {
130 self.conn
132 .execute("DELETE FROM symbols WHERE path = ?1", params![path])?;
133
134 let mut stmt = self.conn.prepare(
135 "INSERT INTO symbols (name, kind, path, range_start_line, range_start_col, range_end_line, range_end_col, parent_name)
136 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
137 )?;
138
139 for sym in symbols {
140 stmt.execute(params![
141 sym.name,
142 sym.kind,
143 path,
144 sym.range_start_line,
145 sym.range_start_col,
146 sym.range_end_line,
147 sym.range_end_col,
148 sym.parent_name,
149 ])?;
150 }
151 Ok(())
152 }
153
154 pub fn batch_commit(
159 &self,
160 results: &[(String, String, Vec<CachedSymbol>)],
161 ) -> rusqlite::Result<usize> {
162 let now = std::time::SystemTime::now()
163 .duration_since(std::time::UNIX_EPOCH)
164 .unwrap_or_default()
165 .as_secs()
166 .cast_signed();
167
168 self.conn.execute_batch("BEGIN")?;
169
170 let upsert_result = (|| {
171 let mut upsert_stmt = self.conn.prepare(
172 "INSERT INTO files (path, blake3_hash, indexed_at)
173 VALUES (?1, ?2, ?3)
174 ON CONFLICT(path) DO UPDATE SET blake3_hash=?2, indexed_at=?3",
175 )?;
176 let mut delete_stmt = self.conn.prepare("DELETE FROM symbols WHERE path = ?1")?;
177 let mut insert_stmt = self.conn.prepare(
178 "INSERT INTO symbols (name, kind, path, range_start_line, range_start_col, range_end_line, range_end_col, parent_name)
179 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
180 )?;
181
182 let mut symbols_total = 0;
183 for (rel_path, hash, symbols) in results {
184 upsert_stmt.execute(params![rel_path, hash, now])?;
185 delete_stmt.execute(params![rel_path])?;
186 for sym in symbols {
187 insert_stmt.execute(params![
188 sym.name,
189 sym.kind,
190 rel_path,
191 sym.range_start_line,
192 sym.range_start_col,
193 sym.range_end_line,
194 sym.range_end_col,
195 sym.parent_name,
196 ])?;
197 }
198 symbols_total += symbols.len();
199 }
200 Ok(symbols_total)
201 })();
202
203 match upsert_result {
204 Ok(total) => {
205 self.conn.execute_batch("COMMIT")?;
206 Ok(total)
207 }
208 Err(e) => {
209 let _ = self.conn.execute_batch("ROLLBACK");
210 Err(e)
211 }
212 }
213 }
214
215 pub fn find_symbols_by_name(&self, name: &str) -> rusqlite::Result<Vec<CachedSymbol>> {
217 let qualified_pattern = format!("%).\"{name}\"");
220 let go_pattern = format!("%).{name}");
221 let mut stmt = self.conn.prepare(
222 "SELECT name, kind, path, range_start_line, range_start_col, range_end_line, range_end_col, parent_name
223 FROM symbols WHERE name = ?1 OR name LIKE ?2 OR name LIKE ?3",
224 )?;
225
226 let rows = stmt.query_map(params![name, go_pattern, qualified_pattern], |row| {
227 Ok(CachedSymbol {
228 name: row.get(0)?,
229 kind: row.get(1)?,
230 path: row.get(2)?,
231 range_start_line: row.get(3)?,
232 range_start_col: row.get(4)?,
233 range_end_line: row.get(5)?,
234 range_end_col: row.get(6)?,
235 parent_name: row.get(7)?,
236 })
237 })?;
238
239 rows.collect()
240 }
241
242 pub fn find_symbols_by_path(&self, path: &str) -> rusqlite::Result<Vec<CachedSymbol>> {
244 let mut stmt = self.conn.prepare(
245 "SELECT name, kind, path, range_start_line, range_start_col, range_end_line, range_end_col, parent_name
246 FROM symbols WHERE path = ?1
247 ORDER BY range_start_line",
248 )?;
249
250 let rows = stmt.query_map(params![path], |row| {
251 Ok(CachedSymbol {
252 name: row.get(0)?,
253 kind: row.get(1)?,
254 path: row.get(2)?,
255 range_start_line: row.get(3)?,
256 range_start_col: row.get(4)?,
257 range_end_line: row.get(5)?,
258 range_end_col: row.get(6)?,
259 parent_name: row.get(7)?,
260 })
261 })?;
262
263 rows.collect()
264 }
265
266 pub fn get_file_hash(&self, path: &str) -> rusqlite::Result<Option<String>> {
268 let mut stmt = self
269 .conn
270 .prepare("SELECT blake3_hash FROM files WHERE path = ?1")?;
271
272 let mut rows = stmt.query(params![path])?;
273 match rows.next()? {
274 Some(row) => Ok(Some(row.get(0)?)),
275 None => Ok(None),
276 }
277 }
278
279 pub fn cache_get(&self, request_hash: &str) -> rusqlite::Result<Option<String>> {
281 let mut stmt = self
282 .conn
283 .prepare("SELECT response_json FROM lsp_cache WHERE request_hash = ?1")?;
284
285 let mut rows = stmt.query(params![request_hash])?;
286 match rows.next()? {
287 Some(row) => Ok(Some(row.get(0)?)),
288 None => Ok(None),
289 }
290 }
291
292 pub fn cache_put(&self, request_hash: &str, response_json: &str) -> rusqlite::Result<()> {
294 let now = std::time::SystemTime::now()
295 .duration_since(std::time::UNIX_EPOCH)
296 .unwrap_or_default()
297 .as_secs();
298
299 self.conn.execute(
300 "INSERT OR REPLACE INTO lsp_cache (request_hash, response_json, created_at)
301 VALUES (?1, ?2, ?3)",
302 params![request_hash, response_json, now.cast_signed()],
303 )?;
304 Ok(())
305 }
306
307 pub fn upsert_workspace(&self, path: &str, language: &str) -> rusqlite::Result<()> {
311 self.conn.execute(
312 "INSERT INTO workspaces (path, language)
313 VALUES (?1, ?2)
314 ON CONFLICT(path) DO UPDATE SET language=?2",
315 params![path, language],
316 )?;
317 Ok(())
318 }
319
320 pub fn set_workspace_attached(&self, path: &str) -> rusqlite::Result<()> {
322 let now = now_unix();
323 self.conn.execute(
324 "UPDATE workspaces SET status='attached', last_used_at=?2 WHERE path=?1",
325 params![path, now],
326 )?;
327 Ok(())
328 }
329
330 pub fn set_workspace_detached(&self, path: &str) -> rusqlite::Result<()> {
332 self.conn.execute(
333 "UPDATE workspaces SET status='discovered' WHERE path=?1",
334 params![path],
335 )?;
336 Ok(())
337 }
338
339 pub fn touch_workspace(&self, path: &str) -> rusqlite::Result<()> {
341 let now = now_unix();
342 self.conn.execute(
343 "UPDATE workspaces SET last_used_at=?2 WHERE path=?1",
344 params![path, now],
345 )?;
346 Ok(())
347 }
348
349 pub fn list_workspaces(&self) -> rusqlite::Result<Vec<WorkspaceInfo>> {
351 let mut stmt = self
352 .conn
353 .prepare("SELECT path, language, status, last_used_at FROM workspaces ORDER BY path")?;
354 let rows = stmt.query_map([], |row| {
355 Ok(WorkspaceInfo {
356 path: row.get(0)?,
357 language: row.get(1)?,
358 status: row.get(2)?,
359 last_used_at: row.get(3)?,
360 })
361 })?;
362 rows.collect()
363 }
364
365 pub fn get_lru_attached(&self, language: &str) -> rusqlite::Result<Option<String>> {
367 let mut stmt = self.conn.prepare(
368 "SELECT path FROM workspaces
369 WHERE language=?1 AND status='attached'
370 ORDER BY last_used_at ASC NULLS FIRST
371 LIMIT 1",
372 )?;
373 let mut rows = stmt.query(params![language])?;
374 match rows.next()? {
375 Some(row) => Ok(Some(row.get(0)?)),
376 None => Ok(None),
377 }
378 }
379
380 pub fn workspace_counts(&self) -> rusqlite::Result<(usize, usize)> {
382 let total: usize = self
383 .conn
384 .query_row("SELECT COUNT(*) FROM workspaces", [], |r| r.get(0))?;
385 let attached: usize = self.conn.query_row(
386 "SELECT COUNT(*) FROM workspaces WHERE status='attached'",
387 [],
388 |r| r.get(0),
389 )?;
390 Ok((total, attached))
391 }
392
393 pub fn clear_workspaces(&self) -> rusqlite::Result<()> {
395 self.conn.execute("DELETE FROM workspaces", [])?;
396 Ok(())
397 }
398
399 pub fn count_all_symbols(&self) -> rusqlite::Result<u64> {
402 self.conn
403 .query_row("SELECT COUNT(*) FROM symbols", [], |row| {
404 row.get::<_, u64>(0)
405 })
406 }
407
408 pub fn optimize(&self) -> rusqlite::Result<()> {
409 self.conn.execute_batch(
410 "PRAGMA analysis_limit = 400;
411 PRAGMA optimize;
412 PRAGMA wal_checkpoint(TRUNCATE);",
413 )
414 }
415
416 pub fn get_file_hashes_batch(
418 &self,
419 paths: &[&str],
420 ) -> rusqlite::Result<std::collections::HashMap<String, String>> {
421 if paths.is_empty() {
422 return Ok(std::collections::HashMap::new());
423 }
424 let placeholders: Vec<String> = (1..=paths.len()).map(|i| format!("?{i}")).collect();
425 let sql = format!(
426 "SELECT path, blake3_hash FROM files WHERE path IN ({})",
427 placeholders.join(",")
428 );
429 let mut stmt = self.conn.prepare(&sql)?;
430 let params: Vec<&dyn rusqlite::ToSql> =
431 paths.iter().map(|p| p as &dyn rusqlite::ToSql).collect();
432 let rows = stmt.query_map(params.as_slice(), |row| {
433 Ok((row.get::<_, String>(0)?, row.get::<_, String>(1)?))
434 })?;
435 let mut map = std::collections::HashMap::new();
436 for row in rows {
437 let (path, hash) = row?;
438 map.insert(path, hash);
439 }
440 Ok(map)
441 }
442
443 pub fn upsert_server_capabilities(
447 &self,
448 server_name: &str,
449 workspace_folders_supported: bool,
450 work_done_progress: bool,
451 ) -> rusqlite::Result<()> {
452 self.conn.execute(
453 "INSERT INTO server_capabilities (server_name, workspace_folders_supported, work_done_progress)
454 VALUES (?1, ?2, ?3)
455 ON CONFLICT(server_name) DO UPDATE
456 SET workspace_folders_supported=?2, work_done_progress=?3",
457 params![
458 server_name,
459 i32::from(workspace_folders_supported),
460 i32::from(work_done_progress)
461 ],
462 )?;
463 Ok(())
464 }
465
466 pub fn get_server_capabilities(
468 &self,
469 server_name: &str,
470 ) -> rusqlite::Result<Option<(bool, bool)>> {
471 let mut stmt = self.conn.prepare(
472 "SELECT workspace_folders_supported, work_done_progress
473 FROM server_capabilities WHERE server_name=?1",
474 )?;
475 let mut rows = stmt.query(params![server_name])?;
476 match rows.next()? {
477 Some(row) => {
478 let wf: i32 = row.get(0)?;
479 let wdp: i32 = row.get(1)?;
480 Ok(Some((wf != 0, wdp != 0)))
481 }
482 None => Ok(None),
483 }
484 }
485
486 pub fn delete_file(&self, path: &str) -> rusqlite::Result<()> {
490 self.conn
491 .execute("DELETE FROM files WHERE path = ?1", params![path])?;
492 Ok(())
493 }
494}
495
496fn now_unix() -> i64 {
497 std::time::SystemTime::now()
498 .duration_since(std::time::UNIX_EPOCH)
499 .unwrap_or_default()
500 .as_secs()
501 .cast_signed()
502}
503
504#[cfg(test)]
505mod tests {
506 use super::*;
507
508 #[test]
509 fn open_creates_tables() {
510 let store = IndexStore::open_in_memory().unwrap();
511 let count: i64 = store
513 .conn
514 .query_row("SELECT COUNT(*) FROM files", [], |r| r.get(0))
515 .unwrap();
516 assert_eq!(count, 0);
517
518 let count: i64 = store
519 .conn
520 .query_row("SELECT COUNT(*) FROM symbols", [], |r| r.get(0))
521 .unwrap();
522 assert_eq!(count, 0);
523
524 let count: i64 = store
525 .conn
526 .query_row("SELECT COUNT(*) FROM lsp_cache", [], |r| r.get(0))
527 .unwrap();
528 assert_eq!(count, 0);
529 }
530
531 #[test]
532 fn upsert_file_and_retrieve() {
533 let store = IndexStore::open_in_memory().unwrap();
534 store.upsert_file("src/lib.rs", "abc123").unwrap();
535
536 let hash = store.get_file_hash("src/lib.rs").unwrap();
537 assert_eq!(hash, Some("abc123".to_string()));
538
539 store.upsert_file("src/lib.rs", "def456").unwrap();
541 let hash = store.get_file_hash("src/lib.rs").unwrap();
542 assert_eq!(hash, Some("def456".to_string()));
543 }
544
545 #[test]
546 fn insert_and_find_symbols_by_name() {
547 let store = IndexStore::open_in_memory().unwrap();
548 store.upsert_file("src/lib.rs", "abc").unwrap();
549
550 let symbols = vec![
551 CachedSymbol {
552 name: "Config".into(),
553 kind: "struct".into(),
554 path: "src/lib.rs".into(),
555 range_start_line: 5,
556 range_start_col: 0,
557 range_end_line: 10,
558 range_end_col: 1,
559 parent_name: None,
560 },
561 CachedSymbol {
562 name: "new".into(),
563 kind: "method".into(),
564 path: "src/lib.rs".into(),
565 range_start_line: 6,
566 range_start_col: 4,
567 range_end_line: 8,
568 range_end_col: 5,
569 parent_name: Some("Config".into()),
570 },
571 ];
572 store.insert_symbols("src/lib.rs", &symbols).unwrap();
573
574 let found = store.find_symbols_by_name("Config").unwrap();
575 assert_eq!(found.len(), 1);
576 assert_eq!(found[0].kind, "struct");
577 assert_eq!(found[0].range_start_line, 5);
578
579 let found = store.find_symbols_by_name("new").unwrap();
580 assert_eq!(found.len(), 1);
581 assert_eq!(found[0].parent_name, Some("Config".to_string()));
582 }
583
584 #[test]
585 fn insert_and_find_symbols_by_path() {
586 let store = IndexStore::open_in_memory().unwrap();
587 store.upsert_file("src/lib.rs", "abc").unwrap();
588
589 let symbols = vec![CachedSymbol {
590 name: "greet".into(),
591 kind: "function".into(),
592 path: "src/lib.rs".into(),
593 range_start_line: 1,
594 range_start_col: 0,
595 range_end_line: 3,
596 range_end_col: 1,
597 parent_name: None,
598 }];
599 store.insert_symbols("src/lib.rs", &symbols).unwrap();
600
601 let found = store.find_symbols_by_path("src/lib.rs").unwrap();
602 assert_eq!(found.len(), 1);
603 assert_eq!(found[0].name, "greet");
604 }
605
606 #[test]
607 fn delete_file_cascades_to_symbols() {
608 let store = IndexStore::open_in_memory().unwrap();
609 store.upsert_file("src/lib.rs", "abc").unwrap();
610
611 let symbols = vec![CachedSymbol {
612 name: "Config".into(),
613 kind: "struct".into(),
614 path: "src/lib.rs".into(),
615 range_start_line: 1,
616 range_start_col: 0,
617 range_end_line: 5,
618 range_end_col: 1,
619 parent_name: None,
620 }];
621 store.insert_symbols("src/lib.rs", &symbols).unwrap();
622 assert_eq!(store.find_symbols_by_name("Config").unwrap().len(), 1);
623
624 store.delete_file("src/lib.rs").unwrap();
625 assert_eq!(store.find_symbols_by_name("Config").unwrap().len(), 0);
626 assert!(store.get_file_hash("src/lib.rs").unwrap().is_none());
627 }
628
629 #[test]
630 fn cache_put_and_get() {
631 let store = IndexStore::open_in_memory().unwrap();
632 store.cache_put("hash123", r#"{"result": "ok"}"#).unwrap();
633
634 let cached = store.cache_get("hash123").unwrap();
635 assert_eq!(cached, Some(r#"{"result": "ok"}"#.to_string()));
636 }
637
638 #[test]
639 fn cache_miss_returns_none() {
640 let store = IndexStore::open_in_memory().unwrap();
641 let cached = store.cache_get("nonexistent").unwrap();
642 assert!(cached.is_none());
643 }
644
645 #[test]
646 fn open_existing_db_preserves_data() {
647 let dir = tempfile::tempdir().unwrap();
648 let db_path = dir.path().join("index.db");
649
650 {
652 let store = IndexStore::open(&db_path).unwrap();
653 store.upsert_file("src/lib.rs", "abc").unwrap();
654 store
655 .insert_symbols(
656 "src/lib.rs",
657 &[CachedSymbol {
658 name: "Config".into(),
659 kind: "struct".into(),
660 path: "src/lib.rs".into(),
661 range_start_line: 1,
662 range_start_col: 0,
663 range_end_line: 5,
664 range_end_col: 1,
665 parent_name: None,
666 }],
667 )
668 .unwrap();
669 }
670
671 let store = IndexStore::open(&db_path).unwrap();
673 let hash = store.get_file_hash("src/lib.rs").unwrap();
674 assert_eq!(hash, Some("abc".to_string()));
675
676 let found = store.find_symbols_by_name("Config").unwrap();
677 assert_eq!(found.len(), 1);
678 }
679
680 #[test]
681 fn workspace_upsert_and_list() {
682 let store = IndexStore::open_in_memory().unwrap();
683 store
684 .upsert_workspace("packages/api", "typescript")
685 .unwrap();
686 store
687 .upsert_workspace("packages/web", "typescript")
688 .unwrap();
689 store.upsert_workspace(".", "go").unwrap();
690
691 let workspaces = store.list_workspaces().unwrap();
692 assert_eq!(workspaces.len(), 3);
693 assert_eq!(workspaces[0].path, ".");
694 assert_eq!(workspaces[0].status, "discovered");
695 assert_eq!(workspaces[1].path, "packages/api");
696 assert_eq!(workspaces[2].path, "packages/web");
697 }
698
699 #[test]
700 fn workspace_status_transitions() {
701 let store = IndexStore::open_in_memory().unwrap();
702 store
703 .upsert_workspace("packages/api", "typescript")
704 .unwrap();
705
706 let ws = &store.list_workspaces().unwrap()[0];
707 assert_eq!(ws.status, "discovered");
708 assert!(ws.last_used_at.is_none());
709
710 store.set_workspace_attached("packages/api").unwrap();
711 let ws = &store.list_workspaces().unwrap()[0];
712 assert_eq!(ws.status, "attached");
713 assert!(ws.last_used_at.is_some());
714
715 store.set_workspace_detached("packages/api").unwrap();
716 let ws = &store.list_workspaces().unwrap()[0];
717 assert_eq!(ws.status, "discovered");
718 }
719
720 #[test]
721 fn workspace_touch_updates_timestamp() {
722 let store = IndexStore::open_in_memory().unwrap();
723 store
724 .upsert_workspace("packages/api", "typescript")
725 .unwrap();
726 store.set_workspace_attached("packages/api").unwrap();
727
728 let t1 = store.list_workspaces().unwrap()[0].last_used_at.unwrap();
729 store.touch_workspace("packages/api").unwrap();
731 let t2 = store.list_workspaces().unwrap()[0].last_used_at.unwrap();
732 assert!(t2 >= t1);
733 }
734
735 #[test]
736 fn workspace_counts() {
737 let store = IndexStore::open_in_memory().unwrap();
738 store
739 .upsert_workspace("packages/api", "typescript")
740 .unwrap();
741 store
742 .upsert_workspace("packages/web", "typescript")
743 .unwrap();
744 store.upsert_workspace(".", "go").unwrap();
745
746 let (total, attached) = store.workspace_counts().unwrap();
747 assert_eq!(total, 3);
748 assert_eq!(attached, 0);
749
750 store.set_workspace_attached("packages/api").unwrap();
751 let (total, attached) = store.workspace_counts().unwrap();
752 assert_eq!(total, 3);
753 assert_eq!(attached, 1);
754 }
755
756 #[test]
757 fn workspace_lru_returns_oldest() {
758 let store = IndexStore::open_in_memory().unwrap();
759 store
760 .upsert_workspace("packages/api", "typescript")
761 .unwrap();
762 store
763 .upsert_workspace("packages/web", "typescript")
764 .unwrap();
765 store.upsert_workspace(".", "go").unwrap();
766
767 assert!(store.get_lru_attached("typescript").unwrap().is_none());
769
770 store.set_workspace_attached("packages/api").unwrap();
772 store.set_workspace_attached("packages/web").unwrap();
773 store.touch_workspace("packages/web").unwrap();
775
776 let lru = store.get_lru_attached("typescript").unwrap();
777 assert_eq!(lru, Some("packages/api".to_string()));
778
779 assert!(store.get_lru_attached("go").unwrap().is_none());
781 }
782
783 #[test]
784 fn workspace_clear() {
785 let store = IndexStore::open_in_memory().unwrap();
786 store
787 .upsert_workspace("packages/api", "typescript")
788 .unwrap();
789 store
790 .upsert_workspace("packages/web", "typescript")
791 .unwrap();
792
793 store.clear_workspaces().unwrap();
794 let workspaces = store.list_workspaces().unwrap();
795 assert!(workspaces.is_empty());
796 }
797
798 #[test]
799 fn workspace_upsert_updates_language() {
800 let store = IndexStore::open_in_memory().unwrap();
801 store.upsert_workspace("frontend", "javascript").unwrap();
802 store.upsert_workspace("frontend", "typescript").unwrap();
803
804 let ws = &store.list_workspaces().unwrap()[0];
805 assert_eq!(ws.language, "typescript");
806 }
807
808 #[test]
809 fn get_file_hashes_batch_empty() {
810 let store = IndexStore::open_in_memory().unwrap();
811 let result = store.get_file_hashes_batch(&[]).unwrap();
812 assert!(result.is_empty());
813 }
814
815 #[test]
816 fn get_file_hashes_batch_returns_stored_hashes() {
817 let store = IndexStore::open_in_memory().unwrap();
818 store.upsert_file("src/a.rs", "hash_a").unwrap();
819 store.upsert_file("src/b.rs", "hash_b").unwrap();
820 store.upsert_file("src/c.rs", "hash_c").unwrap();
821
822 let result = store
823 .get_file_hashes_batch(&["src/a.rs", "src/b.rs", "src/missing.rs"])
824 .unwrap();
825
826 assert_eq!(result.get("src/a.rs").map(String::as_str), Some("hash_a"));
827 assert_eq!(result.get("src/b.rs").map(String::as_str), Some("hash_b"));
828 assert!(!result.contains_key("src/missing.rs"));
829 }
830
831 #[test]
832 fn server_capabilities_roundtrip() {
833 let store = IndexStore::open_in_memory().unwrap();
834
835 let caps = store.get_server_capabilities("vtsls").unwrap();
837 assert!(caps.is_none());
838
839 store
841 .upsert_server_capabilities("vtsls", true, false)
842 .unwrap();
843 let caps = store.get_server_capabilities("vtsls").unwrap().unwrap();
844 assert_eq!(caps, (true, false));
845
846 store
848 .upsert_server_capabilities("vtsls", true, true)
849 .unwrap();
850 let caps = store.get_server_capabilities("vtsls").unwrap().unwrap();
851 assert_eq!(caps, (true, true));
852 }
853}