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 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 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 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 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 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 struct PackOptions {
155 pub no_file_summary: bool,
156 pub no_files: bool,
157 pub remove_comments: bool,
158 pub remove_empty_lines: bool,
159 pub truncate_base64: bool,
160 pub include: Option<Vec<String>>,
161 pub ignore: Option<Vec<String>>,
162}
163
164pub fn pack(
165 dir: &Path,
166 format_str: &str,
167 max_tokens: Option<usize>,
168 options: PackOptions,
169) -> Result<String> {
170 let base_dir = if dir.is_file() {
172 dir.parent().unwrap_or(Path::new("."))
173 } else {
174 dir
175 };
176
177 index(base_dir)?;
179
180 let db_path = base_dir.join("codebones.db");
181 let cache = SqliteCache::new(db_path.to_str().unwrap())?;
182 cache.init()?;
183
184 let format = match format_str.to_lowercase().as_str() {
185 "xml" => OutputFormat::Xml,
186 _ => OutputFormat::Markdown,
187 };
188
189 let mut paths = Vec::new();
191 {
192 let mut stmt = cache.conn.prepare("SELECT path FROM files")?;
193 let rows = stmt.query_map([], |row| row.get::<_, String>(0))?;
194
195 let mut include_builder = globset::GlobSetBuilder::new();
196 let mut has_includes = false;
197 if let Some(includes) = &options.include {
198 for pattern in includes {
199 if let Ok(glob) = globset::Glob::new(pattern) {
200 include_builder.add(glob);
201 has_includes = true;
202 }
203 }
204 }
205 let include_set = include_builder.build().unwrap_or(globset::GlobSet::empty());
206
207 let mut ignore_builder = globset::GlobSetBuilder::new();
208 let mut has_ignores = false;
209 if let Some(ignores) = &options.ignore {
210 for pattern in ignores {
211 if let Ok(glob) = globset::Glob::new(pattern) {
212 ignore_builder.add(glob);
213 has_ignores = true;
214 }
215 }
216 }
217 let ignore_set = ignore_builder.build().unwrap_or(globset::GlobSet::empty());
218
219 for row in rows {
220 let path_str = row?;
221
222 if has_includes && !include_set.is_match(&path_str) {
223 continue;
224 }
225 if has_ignores && ignore_set.is_match(&path_str) {
226 continue;
227 }
228
229 let file_path = base_dir.join(&path_str);
230
231 if dir.is_file() {
233 let dir_canon = dir.canonicalize().unwrap_or_else(|_| dir.to_path_buf());
234 let file_canon = file_path
235 .canonicalize()
236 .unwrap_or_else(|_| file_path.clone());
237 if file_canon != dir_canon {
238 continue;
239 }
240 }
241
242 if file_path.exists() {
243 paths.push(file_path);
244 }
245 }
246 }
247
248 let packer = Packer::new(
249 cache,
250 crate::parser::Parser {},
251 format,
252 max_tokens,
253 options.no_file_summary,
254 options.no_files,
255 options.remove_comments,
256 options.remove_empty_lines,
257 options.truncate_base64,
258 );
259
260 packer.pack(&paths)
261}