1use std::path::PathBuf;
17
18#[derive(Debug, Clone)]
24pub struct ResolvedPackage {
25 pub path: PathBuf,
27 pub name: String,
29 pub is_namespace: bool,
31}
32
33pub fn get_global_cache_dir() -> Option<PathBuf> {
39 let cache_base = if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
40 PathBuf::from(xdg)
41 } else if let Ok(home) = std::env::var("HOME") {
42 PathBuf::from(home).join(".cache")
43 } else if let Ok(home) = std::env::var("USERPROFILE") {
44 PathBuf::from(home).join(".cache")
45 } else {
46 return None;
47 };
48
49 let moss_cache = cache_base.join("moss");
50 if !moss_cache.exists() {
51 std::fs::create_dir_all(&moss_cache).ok()?;
52 }
53
54 Some(moss_cache)
55}
56
57pub fn get_global_packages_db() -> Option<PathBuf> {
59 let cache = get_global_cache_dir()?;
60 Some(cache.join("packages.db"))
61}
62
63pub fn version_cmp(a: &str, b: &str) -> std::cmp::Ordering {
65 let a_parts: Vec<u32> = a.split('.').filter_map(|p| p.parse().ok()).collect();
66 let b_parts: Vec<u32> = b.split('.').filter_map(|p| p.parse().ok()).collect();
67
68 for (ap, bp) in a_parts.iter().zip(b_parts.iter()) {
69 match ap.cmp(bp) {
70 std::cmp::Ordering::Equal => continue,
71 other => return other,
72 }
73 }
74 a_parts.len().cmp(&b_parts.len())
75}
76
77use libsql::{Connection, Database, params};
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct Version {
86 pub major: u32,
87 pub minor: u32,
88}
89
90impl Version {
91 pub fn parse(s: &str) -> Option<Version> {
92 let parts: Vec<&str> = s.split('.').collect();
93 if parts.len() >= 2 {
94 Some(Version {
95 major: parts[0].parse().ok()?,
96 minor: parts[1].parse().ok()?,
97 })
98 } else {
99 None
100 }
101 }
102
103 pub fn in_range(&self, min: Version, max: Option<Version>) -> bool {
104 if *self < min {
105 return false;
106 }
107 if let Some(max) = max {
108 if *self > max {
109 return false;
110 }
111 }
112 true
113 }
114}
115
116impl PartialOrd for Version {
117 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
118 Some(self.cmp(other))
119 }
120}
121
122impl Ord for Version {
123 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
124 match self.major.cmp(&other.major) {
125 std::cmp::Ordering::Equal => self.minor.cmp(&other.minor),
126 ord => ord,
127 }
128 }
129}
130
131#[derive(Debug, Clone)]
133pub struct PackageRecord {
134 pub id: i64,
135 pub language: String,
136 pub name: String,
137 pub path: String,
138 pub min_major: u32,
139 pub min_minor: u32,
140 pub max_major: Option<u32>,
141 pub max_minor: Option<u32>,
142}
143
144impl PackageRecord {
145 pub fn min_version(&self) -> Version {
146 Version {
147 major: self.min_major,
148 minor: self.min_minor,
149 }
150 }
151
152 pub fn max_version(&self) -> Option<Version> {
153 match (self.max_major, self.max_minor) {
154 (Some(major), Some(minor)) => Some(Version { major, minor }),
155 _ => None,
156 }
157 }
158}
159
160#[derive(Debug, Clone)]
162pub struct SymbolRecord {
163 pub id: i64,
164 pub package_id: i64,
165 pub name: String,
166 pub kind: String,
167 pub signature: String,
168 pub line: u32,
169}
170
171pub struct PackageIndex {
173 conn: Connection,
174 #[allow(dead_code)]
175 db: Database,
176}
177
178impl PackageIndex {
179 pub async fn open() -> Result<Self, libsql::Error> {
180 let db_path = get_global_packages_db().ok_or_else(|| {
181 libsql::Error::SqliteFailure(1, "Cannot determine cache directory".into())
182 })?;
183
184 let db = libsql::Builder::new_local(db_path).build().await?;
185 let conn = db.connect()?;
186 let index = PackageIndex { conn, db };
187 index.init_schema().await?;
188 Ok(index)
189 }
190
191 pub async fn open_in_memory() -> Result<Self, libsql::Error> {
192 let db = libsql::Builder::new_local(":memory:").build().await?;
193 let conn = db.connect()?;
194 let index = PackageIndex { conn, db };
195 index.init_schema().await?;
196 Ok(index)
197 }
198
199 async fn init_schema(&self) -> Result<(), libsql::Error> {
200 self.conn
201 .execute(
202 "CREATE TABLE IF NOT EXISTS packages (
203 id INTEGER PRIMARY KEY,
204 language TEXT NOT NULL,
205 name TEXT NOT NULL,
206 path TEXT NOT NULL,
207 min_major INTEGER NOT NULL,
208 min_minor INTEGER NOT NULL,
209 max_major INTEGER,
210 max_minor INTEGER,
211 indexed_at INTEGER NOT NULL
212 )",
213 (),
214 )
215 .await?;
216
217 self.conn
218 .execute(
219 "CREATE INDEX IF NOT EXISTS idx_packages_lang_name ON packages(language, name)",
220 (),
221 )
222 .await?;
223
224 self.conn
225 .execute(
226 "CREATE TABLE IF NOT EXISTS symbols (
227 id INTEGER PRIMARY KEY,
228 package_id INTEGER NOT NULL,
229 name TEXT NOT NULL,
230 kind TEXT NOT NULL,
231 signature TEXT NOT NULL,
232 line INTEGER NOT NULL,
233 FOREIGN KEY (package_id) REFERENCES packages(id) ON DELETE CASCADE
234 )",
235 (),
236 )
237 .await?;
238
239 self.conn
240 .execute(
241 "CREATE INDEX IF NOT EXISTS idx_symbols_package ON symbols(package_id)",
242 (),
243 )
244 .await?;
245 self.conn
246 .execute(
247 "CREATE INDEX IF NOT EXISTS idx_symbols_name ON symbols(name)",
248 (),
249 )
250 .await?;
251
252 Ok(())
253 }
254
255 pub async fn insert_package(
256 &self,
257 language: &str,
258 name: &str,
259 path: &str,
260 min_version: Version,
261 max_version: Option<Version>,
262 ) -> Result<i64, libsql::Error> {
263 let now = std::time::SystemTime::now()
264 .duration_since(std::time::UNIX_EPOCH)
265 .unwrap_or_default()
266 .as_secs() as i64;
267
268 self.conn.execute(
269 "INSERT INTO packages (language, name, path, min_major, min_minor, max_major, max_minor, indexed_at)
270 VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)",
271 params![
272 language,
273 name,
274 path,
275 min_version.major,
276 min_version.minor,
277 max_version.map(|v| v.major),
278 max_version.map(|v| v.minor),
279 now,
280 ],
281 ).await?;
282 Ok(self.conn.last_insert_rowid())
283 }
284
285 pub async fn insert_symbol(
286 &self,
287 package_id: i64,
288 name: &str,
289 kind: &str,
290 signature: &str,
291 line: u32,
292 ) -> Result<i64, libsql::Error> {
293 self.conn
294 .execute(
295 "INSERT INTO symbols (package_id, name, kind, signature, line)
296 VALUES (?1, ?2, ?3, ?4, ?5)",
297 params![package_id, name, kind, signature, line],
298 )
299 .await?;
300 Ok(self.conn.last_insert_rowid())
301 }
302
303 pub async fn find_package(
304 &self,
305 language: &str,
306 name: &str,
307 version: Option<Version>,
308 ) -> Result<Option<PackageRecord>, libsql::Error> {
309 let mut rows = self
310 .conn
311 .query(
312 "SELECT id, language, name, path, min_major, min_minor, max_major, max_minor
313 FROM packages WHERE language = ?1 AND name = ?2",
314 params![language, name],
315 )
316 .await?;
317
318 let mut packages = Vec::new();
319 while let Some(row) = rows.next().await? {
320 packages.push(PackageRecord {
321 id: row.get(0)?,
322 language: row.get(1)?,
323 name: row.get(2)?,
324 path: row.get(3)?,
325 min_major: row.get(4)?,
326 min_minor: row.get(5)?,
327 max_major: row.get(6)?,
328 max_minor: row.get(7)?,
329 });
330 }
331
332 if let Some(ver) = version {
333 for pkg in packages {
334 if ver.in_range(pkg.min_version(), pkg.max_version()) {
335 return Ok(Some(pkg));
336 }
337 }
338 Ok(None)
339 } else {
340 Ok(packages.into_iter().next())
341 }
342 }
343
344 pub async fn get_symbols(&self, package_id: i64) -> Result<Vec<SymbolRecord>, libsql::Error> {
345 let mut rows = self
346 .conn
347 .query(
348 "SELECT id, package_id, name, kind, signature, line
349 FROM symbols WHERE package_id = ?1 ORDER BY line",
350 params![package_id],
351 )
352 .await?;
353
354 let mut symbols = Vec::new();
355 while let Some(row) = rows.next().await? {
356 symbols.push(SymbolRecord {
357 id: row.get(0)?,
358 package_id: row.get(1)?,
359 name: row.get(2)?,
360 kind: row.get(3)?,
361 signature: row.get(4)?,
362 line: row.get(5)?,
363 });
364 }
365
366 Ok(symbols)
367 }
368
369 pub async fn find_symbol(
370 &self,
371 language: &str,
372 symbol_name: &str,
373 version: Option<Version>,
374 ) -> Result<Vec<(PackageRecord, SymbolRecord)>, libsql::Error> {
375 let mut rows = self.conn.query(
376 "SELECT p.id, p.language, p.name, p.path, p.min_major, p.min_minor, p.max_major, p.max_minor,
377 s.id, s.package_id, s.name, s.kind, s.signature, s.line
378 FROM symbols s
379 JOIN packages p ON s.package_id = p.id
380 WHERE p.language = ?1 AND s.name = ?2",
381 params![language, symbol_name],
382 ).await?;
383
384 let mut results = Vec::new();
385 while let Some(row) = rows.next().await? {
386 results.push((
387 PackageRecord {
388 id: row.get(0)?,
389 language: row.get(1)?,
390 name: row.get(2)?,
391 path: row.get(3)?,
392 min_major: row.get(4)?,
393 min_minor: row.get(5)?,
394 max_major: row.get(6)?,
395 max_minor: row.get(7)?,
396 },
397 SymbolRecord {
398 id: row.get(8)?,
399 package_id: row.get(9)?,
400 name: row.get(10)?,
401 kind: row.get(11)?,
402 signature: row.get(12)?,
403 line: row.get(13)?,
404 },
405 ));
406 }
407
408 if let Some(ver) = version {
409 Ok(results
410 .into_iter()
411 .filter(|(pkg, _)| ver.in_range(pkg.min_version(), pkg.max_version()))
412 .collect())
413 } else {
414 Ok(results)
415 }
416 }
417
418 pub async fn is_indexed(&self, language: &str, name: &str) -> Result<bool, libsql::Error> {
419 let mut rows = self
420 .conn
421 .query(
422 "SELECT COUNT(*) FROM packages WHERE language = ?1 AND name = ?2",
423 params![language, name],
424 )
425 .await?;
426
427 if let Some(row) = rows.next().await? {
428 let count: i64 = row.get(0)?;
429 Ok(count > 0)
430 } else {
431 Ok(false)
432 }
433 }
434
435 pub async fn delete_package(&self, package_id: i64) -> Result<(), libsql::Error> {
436 self.conn
437 .execute(
438 "DELETE FROM symbols WHERE package_id = ?1",
439 params![package_id],
440 )
441 .await?;
442 self.conn
443 .execute("DELETE FROM packages WHERE id = ?1", params![package_id])
444 .await?;
445 Ok(())
446 }
447
448 pub async fn clear(&self) -> Result<(), libsql::Error> {
449 self.conn.execute("DELETE FROM symbols", ()).await?;
450 self.conn.execute("DELETE FROM packages", ()).await?;
451 Ok(())
452 }
453}
454
455#[cfg(test)]
456mod tests {
457 use super::*;
458
459 #[test]
460 fn test_version_parsing() {
461 assert_eq!(
462 Version::parse("3.11"),
463 Some(Version {
464 major: 3,
465 minor: 11
466 })
467 );
468 assert_eq!(
469 Version::parse("1.21"),
470 Some(Version {
471 major: 1,
472 minor: 21
473 })
474 );
475 assert_eq!(Version::parse("invalid"), None);
476 }
477
478 #[test]
479 fn test_version_comparison() {
480 let v1 = Version {
481 major: 3,
482 minor: 10,
483 };
484 let v2 = Version {
485 major: 3,
486 minor: 11,
487 };
488 let v3 = Version { major: 4, minor: 0 };
489
490 assert!(v1 < v2);
491 assert!(v2 < v3);
492 assert!(v1.in_range(v1, Some(v2)));
493 assert!(!v3.in_range(v1, Some(v2)));
494 }
495
496 #[tokio::test]
497 async fn test_package_index() {
498 let index = PackageIndex::open_in_memory().await.unwrap();
499
500 let pkg_id = index
502 .insert_package(
503 "python",
504 "requests",
505 "/path/to/requests",
506 Version { major: 3, minor: 8 },
507 None,
508 )
509 .await
510 .unwrap();
511
512 index
514 .insert_symbol(pkg_id, "get", "function", "def get(url)", 10)
515 .await
516 .unwrap();
517
518 let found = index
520 .find_package("python", "requests", None)
521 .await
522 .unwrap();
523 assert!(found.is_some());
524 assert_eq!(found.unwrap().name, "requests");
525
526 let symbols = index.get_symbols(pkg_id).await.unwrap();
528 assert_eq!(symbols.len(), 1);
529 assert_eq!(symbols[0].name, "get");
530
531 assert!(index.is_indexed("python", "requests").await.unwrap());
533 assert!(!index.is_indexed("python", "nonexistent").await.unwrap());
534 }
535}