1use anyhow::Result;
4use rusqlite::{Connection, params};
5use std::path::{Path, PathBuf};
6use std::time::Duration;
7#[cfg(not(target_os = "windows"))]
8use tokio::process::Command;
9use tokio::time::timeout;
10
11use super::types::{Node, IndexStatus, PendingChanges, FileInfo};
12use super::install::{get_codegraph_path, ensure_codegraph};
13use super::project::find_project_root;
14use super::git::update_version_after_sync;
15use crate::constants::{CODEGRAPH_CLI_TIMEOUT_SECS};
16use crate::memory::ProjectStructureAnalyzer;
17
18pub struct CodeGraphManager {
20 project_path: PathBuf,
21 db_path: PathBuf,
22}
23
24impl CodeGraphManager {
25 pub fn new(project_path: &Path) -> Self {
27 let db_path = project_path.join(".codegraph").join("codegraph.db");
28 Self {
29 project_path: project_path.to_path_buf(),
30 db_path,
31 }
32 }
33
34 pub fn with_auto_detect(start_path: &Path) -> Self {
36 let project_path = find_project_root(start_path);
37 Self::new(&project_path)
38 }
39
40 pub fn is_initialized(&self) -> bool {
42 self.db_path.exists()
43 }
44
45 pub fn connect(&self) -> Result<Connection> {
47 let conn = Connection::open(&self.db_path)?;
48 conn.execute_batch("PRAGMA query_only = ON;")?;
49 Ok(conn)
50 }
51
52 pub async fn init(&self) -> Result<()> {
54 self.run_cli_command(&["init", "-i"]).await?;
55 Ok(())
56 }
57
58 pub async fn reinit(&self) -> Result<()> {
60 if get_codegraph_path().is_none() {
61 return Err(anyhow::anyhow!(
62 "CodeGraph CLI not installed. Please install first."
63 ));
64 }
65
66 let codegraph_dir = self.project_path.join(".codegraph");
67 if codegraph_dir.exists() {
68 log::info!("Deleting old index at {}", codegraph_dir.display());
69 std::fs::remove_dir_all(&codegraph_dir)?;
70 }
71
72 log::info!("Rebuilding index for {}", self.project_path.display());
73 self.init().await?;
74 self.sync().await?;
75 update_version_after_sync(&self.project_path);
76
77 Ok(())
78 }
79
80 pub async fn sync(&self) -> Result<()> {
82 self.run_cli_command(&["sync"]).await?;
83 Ok(())
84 }
85
86 async fn run_cli_command(&self, args: &[&str]) -> Result<()> {
88 let codegraph_path = get_codegraph_path()
89 .ok_or_else(|| anyhow::anyhow!("CodeGraph CLI not installed"))?;
90
91 timeout(Duration::from_secs(CODEGRAPH_CLI_TIMEOUT_SECS), async {
92 #[cfg(target_os = "windows")]
93 {
94 use std::os::windows::process::CommandExt;
95 const CREATE_NO_WINDOW: u32 = 0x08000000;
96
97 let mut std_cmd = std::process::Command::new(&codegraph_path);
98 std_cmd.args(args)
99 .current_dir(&self.project_path)
100 .creation_flags(CREATE_NO_WINDOW);
101
102 let result = std_cmd.output()?;
103 if !result.status.success() {
104 let stderr = String::from_utf8_lossy(&result.stderr);
105 return Err(anyhow::anyhow!("CodeGraph command failed: {}", stderr));
106 }
107 Ok::<_, anyhow::Error>(())
108 }
109
110 #[cfg(not(target_os = "windows"))]
111 {
112 let result = Command::new(&codegraph_path)
113 .args(args)
114 .current_dir(&self.project_path)
115 .output()
116 .await?;
117
118 if !result.status.success() {
119 let stderr = String::from_utf8_lossy(&result.stderr);
120 return Err(anyhow::anyhow!("CodeGraph command failed: {}", stderr));
121 }
122 Ok::<_, anyhow::Error>(())
123 }
124 })
125 .await
126 .map_err(|_| anyhow::anyhow!("CodeGraph CLI timeout"))?
127 }
128
129 pub async fn ensure_initialized(&self) -> Result<()> {
131 ensure_codegraph().await?;
132
133 let analyzer = ProjectStructureAnalyzer::new(self.project_path.clone());
134 if analyzer.detect_project_type().is_none() {
135 return Err(anyhow::anyhow!(
136 "Not a code project directory: {}",
137 self.project_path.display()
138 ));
139 }
140
141 if !self.is_initialized() {
142 log::info!("Initializing CodeGraph for: {}", self.project_path.display());
143 self.init().await?;
144 }
145
146 Ok(())
147 }
148
149 pub fn search(&self, pattern: &str, limit: usize) -> Result<Vec<Node>> {
155 let conn = self.connect()?;
156 let mut stmt = conn.prepare(
157 "SELECT id, kind, name, qualified_name, file_path, language,
158 start_line, end_line, start_column, end_column,
159 signature, docstring, visibility, is_exported, is_async
160 FROM nodes
161 WHERE name LIKE ? OR qualified_name LIKE ?
162 ORDER BY name
163 LIMIT ?"
164 )?;
165
166 let pattern = format!("%{}%", pattern);
167 let nodes = stmt.query_map(params![&pattern, &pattern, limit], |row| {
168 Ok(Node {
169 id: row.get(0)?,
170 kind: row.get(1)?,
171 name: row.get(2)?,
172 qualified_name: row.get(3)?,
173 file_path: row.get(4)?,
174 language: row.get(5)?,
175 start_line: row.get(6)?,
176 end_line: row.get(7)?,
177 start_column: row.get(8)?,
178 end_column: row.get(9)?,
179 signature: row.get(10)?,
180 docstring: row.get(11)?,
181 visibility: row.get(12)?,
182 is_exported: row.get::<_, i32>(13)? != 0,
183 is_async: row.get::<_, i32>(14)? != 0,
184 })
185 })?
186 .collect::<Result<Vec<_>, _>>()?;
187
188 Ok(nodes)
189 }
190
191 pub fn callers(&self, symbol_id: &str, limit: usize) -> Result<Vec<Node>> {
193 let conn = self.connect()?;
194 let mut stmt = conn.prepare(
195 "SELECT n.id, n.kind, n.name, n.qualified_name, n.file_path, n.language,
196 n.start_line, n.end_line, n.start_column, n.end_column,
197 n.signature, n.docstring, n.visibility, n.is_exported, n.is_async
198 FROM nodes n
199 INNER JOIN edges e ON n.id = e.source
200 WHERE e.target = ? AND e.kind = 'calls'
201 LIMIT ?"
202 )?;
203
204 let nodes = stmt.query_map(params![symbol_id, limit], |row| {
205 Ok(Node {
206 id: row.get(0)?,
207 kind: row.get(1)?,
208 name: row.get(2)?,
209 qualified_name: row.get(3)?,
210 file_path: row.get(4)?,
211 language: row.get(5)?,
212 start_line: row.get(6)?,
213 end_line: row.get(7)?,
214 start_column: row.get(8)?,
215 end_column: row.get(9)?,
216 signature: row.get(10)?,
217 docstring: row.get(11)?,
218 visibility: row.get(12)?,
219 is_exported: row.get::<_, i32>(13)? != 0,
220 is_async: row.get::<_, i32>(14)? != 0,
221 })
222 })?
223 .collect::<Result<Vec<_>, _>>()?;
224
225 Ok(nodes)
226 }
227
228 pub fn callees(&self, symbol_id: &str, limit: usize) -> Result<Vec<Node>> {
230 let conn = self.connect()?;
231 let mut stmt = conn.prepare(
232 "SELECT n.id, n.kind, n.name, n.qualified_name, n.file_path, n.language,
233 n.start_line, n.end_line, n.start_column, n.end_column,
234 n.signature, n.docstring, n.visibility, n.is_exported, n.is_async
235 FROM nodes n
236 INNER JOIN edges e ON n.id = e.target
237 WHERE e.source = ? AND e.kind = 'calls'
238 LIMIT ?"
239 )?;
240
241 let nodes = stmt.query_map(params![symbol_id, limit], |row| {
242 Ok(Node {
243 id: row.get(0)?,
244 kind: row.get(1)?,
245 name: row.get(2)?,
246 qualified_name: row.get(3)?,
247 file_path: row.get(4)?,
248 language: row.get(5)?,
249 start_line: row.get(6)?,
250 end_line: row.get(7)?,
251 start_column: row.get(8)?,
252 end_column: row.get(9)?,
253 signature: row.get(10)?,
254 docstring: row.get(11)?,
255 visibility: row.get(12)?,
256 is_exported: row.get::<_, i32>(13)? != 0,
257 is_async: row.get::<_, i32>(14)? != 0,
258 })
259 })?
260 .collect::<Result<Vec<_>, _>>()?;
261
262 Ok(nodes)
263 }
264
265 pub fn status(&self) -> Result<IndexStatus> {
267 if !self.is_initialized() {
268 return Ok(IndexStatus {
269 initialized: false,
270 file_count: 0,
271 node_count: 0,
272 edge_count: 0,
273 languages: vec![],
274 pending_changes: PendingChanges {
275 added: 0,
276 modified: 0,
277 removed: 0,
278 },
279 });
280 }
281
282 let conn = self.connect()?;
283 let file_count: u32 = conn.query_row("SELECT COUNT(*) FROM files", [], |r| r.get(0))?;
284 let node_count: u32 = conn.query_row("SELECT COUNT(*) FROM nodes", [], |r| r.get(0))?;
285 let edge_count: u32 = conn.query_row("SELECT COUNT(*) FROM edges", [], |r| r.get(0))?;
286
287 let mut stmt = conn.prepare("SELECT DISTINCT language FROM nodes")?;
288 let languages: Vec<String> = stmt.query_map([], |r| r.get(0))?
289 .collect::<Result<Vec<_>, _>>()?;
290
291 Ok(IndexStatus {
292 initialized: true,
293 file_count,
294 node_count,
295 edge_count,
296 languages,
297 pending_changes: PendingChanges {
298 added: 0,
299 modified: 0,
300 removed: 0,
301 },
302 })
303 }
304
305 pub fn files(&self, language: Option<&str>) -> Result<Vec<FileInfo>> {
307 let conn = self.connect()?;
308
309 let mut stmt = if let Some(_lang) = language {
311 conn.prepare("SELECT path, language, 0 as size, 0 as modified, node_count FROM files WHERE language = ?")?
312 } else {
313 conn.prepare("SELECT path, language, 0 as size, 0 as modified, node_count FROM files")?
314 };
315
316 let files = if let Some(lang) = language {
317 stmt.query_map(params![lang], |row| {
318 Ok(FileInfo {
319 path: row.get(0)?,
320 language: row.get(1)?,
321 size: row.get(2)?,
322 modified: row.get(3)?,
323 node_count: Some(row.get(4)?),
324 })
325 })?
326 .collect::<Result<Vec<_>, _>>()?
327 } else {
328 stmt.query_map([], |row| {
329 Ok(FileInfo {
330 path: row.get(0)?,
331 language: row.get(1)?,
332 size: row.get(2)?,
333 modified: row.get(3)?,
334 node_count: Some(row.get(4)?),
335 })
336 })?
337 .collect::<Result<Vec<_>, _>>()?
338 };
339
340 Ok(files)
341 }
342}