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