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::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
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 =
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    /// Initialize CodeGraph for this project (check CLI and auto-install if needed).
131    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    // ========================================================================
154    // Query Methods
155    // ========================================================================
156
157    /// Search symbols by name pattern.
158    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    /// Find callers of a symbol.
197    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    /// Find callees of a symbol.
235    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    /// Get index status.
273    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    /// Get files by language.
314    pub fn files(&self, language: Option<&str>) -> Result<Vec<FileInfo>> {
315        let conn = self.connect()?;
316
317        // Query files with metadata from the files table
318        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}