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::git::update_version_after_sync;
12use super::install::{ensure_codegraph, get_codegraph_path};
13use super::project::find_project_root;
14use super::types::{FileInfo, IndexStatus, Node, PendingChanges};
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 =
89 get_codegraph_path().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
99 .args(args)
100 .current_dir(&self.project_path)
101 .creation_flags(CREATE_NO_WINDOW);
102
103 let result = std_cmd.output()?;
104 if !result.status.success() {
105 let stderr = String::from_utf8_lossy(&result.stderr);
106 return Err(anyhow::anyhow!("CodeGraph command failed: {}", stderr));
107 }
108 Ok::<_, anyhow::Error>(())
109 }
110
111 #[cfg(not(target_os = "windows"))]
112 {
113 let result = Command::new(&codegraph_path)
114 .args(args)
115 .current_dir(&self.project_path)
116 .output()
117 .await?;
118
119 if !result.status.success() {
120 let stderr = String::from_utf8_lossy(&result.stderr);
121 return Err(anyhow::anyhow!("CodeGraph command failed: {}", stderr));
122 }
123 Ok::<_, anyhow::Error>(())
124 }
125 })
126 .await
127 .map_err(|_| anyhow::anyhow!("CodeGraph CLI timeout"))?
128 }
129
130 pub async fn ensure_initialized(&self) -> Result<()> {
132 ensure_codegraph().await?;
133
134 let analyzer = ProjectStructureAnalyzer::new(self.project_path.clone());
135 if analyzer.detect_project_type().is_none() {
136 return Err(anyhow::anyhow!(
137 "Not a code project directory: {}",
138 self.project_path.display()
139 ));
140 }
141
142 if !self.is_initialized() {
143 log::info!(
144 "Initializing CodeGraph for: {}",
145 self.project_path.display()
146 );
147 self.init().await?;
148 }
149
150 Ok(())
151 }
152
153 pub fn search(&self, pattern: &str, limit: usize) -> Result<Vec<Node>> {
159 let conn = self.connect()?;
160 let mut stmt = conn.prepare(
161 "SELECT id, kind, name, qualified_name, file_path, language,
162 start_line, end_line, start_column, end_column,
163 signature, docstring, visibility, is_exported, is_async
164 FROM nodes
165 WHERE name LIKE ? OR qualified_name LIKE ?
166 ORDER BY name
167 LIMIT ?",
168 )?;
169
170 let pattern = format!("%{}%", pattern);
171 let nodes = stmt
172 .query_map(params![&pattern, &pattern, limit], |row| {
173 Ok(Node {
174 id: row.get(0)?,
175 kind: row.get(1)?,
176 name: row.get(2)?,
177 qualified_name: row.get(3)?,
178 file_path: row.get(4)?,
179 language: row.get(5)?,
180 start_line: row.get(6)?,
181 end_line: row.get(7)?,
182 start_column: row.get(8)?,
183 end_column: row.get(9)?,
184 signature: row.get(10)?,
185 docstring: row.get(11)?,
186 visibility: row.get(12)?,
187 is_exported: row.get::<_, i32>(13)? != 0,
188 is_async: row.get::<_, i32>(14)? != 0,
189 })
190 })?
191 .collect::<Result<Vec<_>, _>>()?;
192
193 Ok(nodes)
194 }
195
196 pub fn callers(&self, symbol_id: &str, limit: usize) -> Result<Vec<Node>> {
198 let conn = self.connect()?;
199 let mut stmt = conn.prepare(
200 "SELECT n.id, n.kind, n.name, n.qualified_name, n.file_path, n.language,
201 n.start_line, n.end_line, n.start_column, n.end_column,
202 n.signature, n.docstring, n.visibility, n.is_exported, n.is_async
203 FROM nodes n
204 INNER JOIN edges e ON n.id = e.source
205 WHERE e.target = ? AND e.kind = 'calls'
206 LIMIT ?",
207 )?;
208
209 let nodes = stmt
210 .query_map(params![symbol_id, limit], |row| {
211 Ok(Node {
212 id: row.get(0)?,
213 kind: row.get(1)?,
214 name: row.get(2)?,
215 qualified_name: row.get(3)?,
216 file_path: row.get(4)?,
217 language: row.get(5)?,
218 start_line: row.get(6)?,
219 end_line: row.get(7)?,
220 start_column: row.get(8)?,
221 end_column: row.get(9)?,
222 signature: row.get(10)?,
223 docstring: row.get(11)?,
224 visibility: row.get(12)?,
225 is_exported: row.get::<_, i32>(13)? != 0,
226 is_async: row.get::<_, i32>(14)? != 0,
227 })
228 })?
229 .collect::<Result<Vec<_>, _>>()?;
230
231 Ok(nodes)
232 }
233
234 pub fn callees(&self, symbol_id: &str, limit: usize) -> Result<Vec<Node>> {
236 let conn = self.connect()?;
237 let mut stmt = conn.prepare(
238 "SELECT n.id, n.kind, n.name, n.qualified_name, n.file_path, n.language,
239 n.start_line, n.end_line, n.start_column, n.end_column,
240 n.signature, n.docstring, n.visibility, n.is_exported, n.is_async
241 FROM nodes n
242 INNER JOIN edges e ON n.id = e.target
243 WHERE e.source = ? AND e.kind = 'calls'
244 LIMIT ?",
245 )?;
246
247 let nodes = stmt
248 .query_map(params![symbol_id, limit], |row| {
249 Ok(Node {
250 id: row.get(0)?,
251 kind: row.get(1)?,
252 name: row.get(2)?,
253 qualified_name: row.get(3)?,
254 file_path: row.get(4)?,
255 language: row.get(5)?,
256 start_line: row.get(6)?,
257 end_line: row.get(7)?,
258 start_column: row.get(8)?,
259 end_column: row.get(9)?,
260 signature: row.get(10)?,
261 docstring: row.get(11)?,
262 visibility: row.get(12)?,
263 is_exported: row.get::<_, i32>(13)? != 0,
264 is_async: row.get::<_, i32>(14)? != 0,
265 })
266 })?
267 .collect::<Result<Vec<_>, _>>()?;
268
269 Ok(nodes)
270 }
271
272 pub fn status(&self) -> Result<IndexStatus> {
274 if !self.is_initialized() {
275 return Ok(IndexStatus {
276 initialized: false,
277 file_count: 0,
278 node_count: 0,
279 edge_count: 0,
280 languages: vec![],
281 pending_changes: PendingChanges {
282 added: 0,
283 modified: 0,
284 removed: 0,
285 },
286 });
287 }
288
289 let conn = self.connect()?;
290 let file_count: u32 = conn.query_row("SELECT COUNT(*) FROM files", [], |r| r.get(0))?;
291 let node_count: u32 = conn.query_row("SELECT COUNT(*) FROM nodes", [], |r| r.get(0))?;
292 let edge_count: u32 = conn.query_row("SELECT COUNT(*) FROM edges", [], |r| r.get(0))?;
293
294 let mut stmt = conn.prepare("SELECT DISTINCT language FROM nodes")?;
295 let languages: Vec<String> = stmt
296 .query_map([], |r| r.get(0))?
297 .collect::<Result<Vec<_>, _>>()?;
298
299 Ok(IndexStatus {
300 initialized: true,
301 file_count,
302 node_count,
303 edge_count,
304 languages,
305 pending_changes: PendingChanges {
306 added: 0,
307 modified: 0,
308 removed: 0,
309 },
310 })
311 }
312
313 pub fn files(&self, language: Option<&str>) -> Result<Vec<FileInfo>> {
315 let conn = self.connect()?;
316
317 let mut stmt = if let Some(_lang) = language {
319 conn.prepare("SELECT path, language, 0 as size, 0 as modified, node_count FROM files WHERE language = ?")?
320 } else {
321 conn.prepare("SELECT path, language, 0 as size, 0 as modified, node_count FROM files")?
322 };
323
324 let files = if let Some(lang) = language {
325 stmt.query_map(params![lang], |row| {
326 Ok(FileInfo {
327 path: row.get(0)?,
328 language: row.get(1)?,
329 size: row.get(2)?,
330 modified: row.get(3)?,
331 node_count: Some(row.get(4)?),
332 })
333 })?
334 .collect::<Result<Vec<_>, _>>()?
335 } else {
336 stmt.query_map([], |row| {
337 Ok(FileInfo {
338 path: row.get(0)?,
339 language: row.get(1)?,
340 size: row.get(2)?,
341 modified: row.get(3)?,
342 node_count: Some(row.get(4)?),
343 })
344 })?
345 .collect::<Result<Vec<_>, _>>()?
346 };
347
348 Ok(files)
349 }
350}