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 fn pack(dir: &Path, format_str: &str) -> Result<String> {
155 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 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 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}