Skip to main content

codebones_core/
api.rs

1use crate::cache::{CacheStore, SqliteCache, Symbol as CacheSymbol};
2use crate::indexer::{DefaultIndexer, Indexer, IndexerOptions};
3use crate::parser::{get_spec_for_extension, parse_file};
4use crate::plugin::{OutputFormat, Packer};
5use anyhow::Result;
6use std::fs;
7use std::path::Path;
8
9pub fn index(dir: &Path) -> Result<()> {
10    let db_path = dir.join("codebones.db");
11    let cache = SqliteCache::new(db_path.to_str().unwrap())?;
12    cache.init()?;
13
14    let indexer = DefaultIndexer;
15    let hashes = indexer.index(dir, &IndexerOptions::default())?;
16
17    for fh in hashes {
18        let path_str = fh.path.to_string_lossy().to_string();
19        let existing_hash = cache.get_file_hash(&path_str)?;
20
21        if existing_hash.as_deref() != Some(fh.hash.as_str()) {
22            let full_path = dir.join(&fh.path);
23            let content = fs::read(&full_path).unwrap_or_default();
24
25            // Delete old file to trigger cascade delete of symbols
26            let _ = cache.delete_file(&path_str);
27
28            let file_id = cache.upsert_file(&path_str, &fh.hash, &content)?;
29
30            let ext = fh.path.extension().unwrap_or_default().to_string_lossy();
31            if let Some(spec) = get_spec_for_extension(&ext) {
32                if let Ok(source) = String::from_utf8(content) {
33                    let doc = parse_file(&source, &spec);
34                    for sym in doc.symbols {
35                        let kind_str = match sym.kind {
36                            crate::parser::SymbolKind::Function => "Function",
37                            crate::parser::SymbolKind::Method => "Method",
38                            crate::parser::SymbolKind::Class => "Class",
39                            crate::parser::SymbolKind::Struct => "Struct",
40                            crate::parser::SymbolKind::Impl => "Impl",
41                            crate::parser::SymbolKind::Interface => "Interface",
42                        }
43                        .to_string();
44
45                        let cache_sym = CacheSymbol {
46                            id: format!("{}::{}", path_str, sym.qualified_name),
47                            file_id,
48                            name: sym.qualified_name.clone(),
49                            kind: kind_str,
50                            byte_offset: sym.full_range.start,
51                            byte_length: sym.full_range.end - sym.full_range.start,
52                        };
53                        let _ = cache.insert_symbol(&cache_sym);
54                    }
55                }
56            }
57        }
58    }
59
60    Ok(())
61}
62
63pub fn get(dir: &Path, symbol_or_path: &str) -> Result<String> {
64    let db_path = dir.join("codebones.db");
65    let cache = SqliteCache::new(db_path.to_str().unwrap())?;
66    cache.init()?;
67
68    // It's a symbol if it contains ::
69    if symbol_or_path.contains("::") {
70        if let Some(content) = cache.get_symbol_content(symbol_or_path)? {
71            return Ok(String::from_utf8_lossy(&content).to_string());
72        }
73    } else {
74        // Assume file path
75        let mut stmt = cache
76            .conn
77            .prepare("SELECT content FROM files WHERE path = ?1")?;
78        let mut rows = stmt.query([symbol_or_path])?;
79        if let Some(row) = rows.next()? {
80            let content: Vec<u8> = row.get(0)?;
81            return Ok(String::from_utf8_lossy(&content).to_string());
82        }
83    }
84
85    anyhow::bail!("Symbol or path not found: {}", symbol_or_path)
86}
87
88pub fn outline(dir: &Path, path: &str) -> Result<String> {
89    let db_path = dir.join("codebones.db");
90    let cache = SqliteCache::new(db_path.to_str().unwrap())?;
91    cache.init()?;
92
93    let mut stmt = cache
94        .conn
95        .prepare("SELECT content FROM files WHERE path = ?1")?;
96    let mut rows = stmt.query([path])?;
97    if let Some(row) = rows.next()? {
98        let content: Vec<u8> = row.get(0)?;
99        let source = String::from_utf8_lossy(&content).to_string();
100
101        let ext = Path::new(path)
102            .extension()
103            .unwrap_or_default()
104            .to_string_lossy();
105        if let Some(spec) = get_spec_for_extension(&ext) {
106            let doc = parse_file(&source, &spec);
107
108            // elide document
109            let mut result = String::new();
110            let mut last_end = 0;
111
112            let mut sorted_symbols = doc.symbols.clone();
113            sorted_symbols.sort_by_key(|s| s.full_range.start);
114
115            for sym in sorted_symbols {
116                if let Some(body_range) = &sym.body_range {
117                    if body_range.start >= last_end {
118                        result.push_str(&source[last_end..body_range.start]);
119                        result.push_str("...");
120                        last_end = body_range.end;
121                    }
122                }
123            }
124            result.push_str(&source[last_end..]);
125            return Ok(result);
126        }
127
128        return Ok(source);
129    }
130
131    anyhow::bail!("Path not found: {}", path)
132}
133
134pub fn search(dir: &Path, query: &str) -> Result<Vec<String>> {
135    let db_path = dir.join("codebones.db");
136    let cache = SqliteCache::new(db_path.to_str().unwrap())?;
137    cache.init()?;
138
139    // Naive search over symbols name using LIKE
140    let mut stmt = cache
141        .conn
142        .prepare("SELECT id FROM symbols WHERE name LIKE ?1")?;
143    let like_query = format!("%{}%", query);
144    let rows = stmt.query_map([like_query], |row| row.get::<_, String>(0))?;
145
146    let mut results = Vec::new();
147    for row in rows {
148        results.push(row?);
149    }
150
151    Ok(results)
152}
153
154pub fn pack(dir: &Path, format_str: &str) -> Result<String> {
155    // Ensure the cache is up to date before packing
156    index(dir)?;
157
158    let db_path = dir.join("codebones.db");
159    let cache = SqliteCache::new(db_path.to_str().unwrap())?;
160    cache.init()?;
161
162    let format = match format_str.to_lowercase().as_str() {
163        "xml" => OutputFormat::Xml,
164        _ => OutputFormat::Markdown,
165    };
166
167    let packer = Packer::new(
168        crate::cache::Cache {},
169        crate::parser::Parser {},
170        format,
171        None,
172    );
173
174    // Get all files
175    let mut stmt = cache.conn.prepare("SELECT path FROM files")?;
176    let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
177    let mut paths = Vec::new();
178    for row in rows {
179        let path_str = row?;
180        // Convert the string path back to a PathBuf relative to `dir`
181        let file_path = dir.join(path_str);
182        if file_path.exists() {
183            paths.push(file_path);
184        }
185    }
186
187    packer.pack(&paths)
188}