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;
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
18/// Manages CodeGraph index for a project.
19pub struct CodeGraphManager {
20    project_path: PathBuf,
21    db_path: PathBuf,
22}
23
24impl CodeGraphManager {
25    /// Create manager for a project path.
26    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    /// Create manager with automatic project root detection.
35    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    /// Check if CodeGraph is initialized for this project.
41    pub fn is_initialized(&self) -> bool {
42        self.db_path.exists()
43    }
44
45    /// Get SQLite connection (read-only for safety).
46    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    /// Initialize CodeGraph index via CLI.
53    pub async fn init(&self) -> Result<()> {
54        self.run_cli_command(&["init", "-i"]).await?;
55        Ok(())
56    }
57
58    /// Reinitialize CodeGraph - delete old index and rebuild.
59    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    /// Sync index with latest file changes.
81    pub async fn sync(&self) -> Result<()> {
82        self.run_cli_command(&["sync"]).await?;
83        Ok(())
84    }
85
86    /// Run codegraph CLI command.
87    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    /// Initialize CodeGraph for this project (check CLI and auto-install if needed).
130    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    // ========================================================================
150    // Query Methods
151    // ========================================================================
152
153    /// Search symbols by name pattern.
154    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    /// Find callers of a symbol.
192    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    /// Find callees of a symbol.
229    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    /// Get index status.
266    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    /// Get files by language.
306    pub fn files(&self, language: Option<&str>) -> Result<Vec<FileInfo>> {
307        let conn = self.connect()?;
308        
309        // Query files with metadata from the files table
310        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}