Skip to main content

matrixcode_core/tools/codegraph/
manager.rs

1//! CodeGraph index manager.
2
3use 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
16/// Manages CodeGraph index for a project.
17pub struct CodeGraphManager {
18    project_path: PathBuf,
19    db_path: PathBuf,
20}
21
22impl CodeGraphManager {
23    /// Create manager for a project path.
24    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    /// Create manager with automatic project root detection.
33    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    /// Check if CodeGraph is initialized for this project.
39    pub fn is_initialized(&self) -> bool {
40        self.db_path.exists()
41    }
42
43    /// Get SQLite connection (read-only for safety).
44    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    /// Initialize CodeGraph index via CLI.
51    pub async fn init(&self) -> Result<()> {
52        self.run_cli_command(&["init", "-i"]).await?;
53        Ok(())
54    }
55
56    /// Reinitialize CodeGraph - delete old index and rebuild.
57    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    /// Sync index with latest file changes.
79    pub async fn sync(&self) -> Result<()> {
80        self.run_cli_command(&["sync"]).await?;
81        Ok(())
82    }
83
84    /// Run codegraph CLI command.
85    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    /// Initialize CodeGraph for this project (check CLI and auto-install if needed).
128    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    // ========================================================================
148    // Query Methods
149    // ========================================================================
150
151    /// Search symbols by name pattern.
152    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    /// Find callers of a symbol.
190    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    /// Find callees of a symbol.
227    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    /// Get index status.
264    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    /// Get files by language.
304    pub fn files(&self, language: Option<&str>) -> Result<Vec<FileInfo>> {
305        let conn = self.connect()?;
306        
307        // Query files with metadata from the files table
308        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}