Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions src/bin/query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ use semcode::display::print_welcome_message_with_model;

/// Rebuild the working directory index to pick up any file changes since the last query.
/// Reuses cached analysis results for files whose mtime and size haven't changed.
fn refresh_workdir_index(db_manager: &DatabaseManager, git_repo: &str) {
async fn refresh_workdir_index(db_manager: &DatabaseManager, git_repo: &str) {
let repo_path = std::path::Path::new(git_repo);
let previous = db_manager.take_workdir_index();
match semcode::WorkdirIndex::build_incremental(repo_path, previous.as_ref()) {
match semcode::WorkdirIndex::build_incremental(repo_path, previous.as_ref()).await {
Ok(workdir) => {
if workdir.is_empty() {
// No need to set — we already took it out
Expand Down Expand Up @@ -235,7 +235,14 @@ async fn main() -> Result<()> {
info!("Connecting to database: {}", database_path);

// Connect to database
let db_manager = Arc::new(DatabaseManager::new(&database_path, args.git_repo.clone()).await?);
let db_manager = DatabaseManager::new(&database_path, args.git_repo.clone()).await?;

// Attach rust-analyzer if applicable
if let Err(e) = db_manager.attach_rust_analyzer().await {
tracing::warn!("Failed to attach rust-analyzer: {}", e);
}

let db_manager = Arc::new(db_manager);

// Ensure tables exist
db_manager.create_tables().await?;
Expand Down Expand Up @@ -454,7 +461,7 @@ async fn main() -> Result<()> {

// Rebuild workdir index to reflect current file state
if !args.git_only {
refresh_workdir_index(&db_manager, &args.git_repo);
refresh_workdir_index(&db_manager, &args.git_repo).await;
}

// Execute the command
Expand Down Expand Up @@ -521,7 +528,7 @@ async fn main() -> Result<()> {

// Rebuild workdir index to reflect current file state
if !args.git_only {
refresh_workdir_index(&db_manager, &args.git_repo);
refresh_workdir_index(&db_manager, &args.git_repo).await;
}

// Handle command and check if we should exit
Expand Down
6 changes: 5 additions & 1 deletion src/bin/semcode-lsp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ impl SemcodeLspBackend {

match DatabaseManager::new(&processed_path, git_repo_path.clone()).await {
Ok(database_manager) => {
if let Err(e) = database_manager.attach_rust_analyzer().await {
tracing::warn!("Failed to attach rust-analyzer: {}", e);
}
*db = Some(database_manager);

// Get the current git SHA for git-aware lookups
Expand Down Expand Up @@ -119,7 +122,8 @@ impl SemcodeLspBackend {

let path = std::path::Path::new(&repo_path);
let previous = db.take_workdir_index();
if let Ok(workdir) = semcode::WorkdirIndex::build_incremental(path, previous.as_ref()) {
if let Ok(workdir) = semcode::WorkdirIndex::build_incremental(path, previous.as_ref()).await
{
if !workdir.is_empty() {
db.set_workdir_index(workdir);
}
Expand Down
115 changes: 100 additions & 15 deletions src/bin/semcode-mcp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1948,6 +1948,7 @@ const TOOL_CATEGORIES: &[ToolCategory] = &[
"find_callers",
"find_calls",
"find_callchain",
"find_rust_symbol",
],
},
ToolCategory {
Expand Down Expand Up @@ -1980,6 +1981,20 @@ const TOOL_CATEGORIES: &[ToolCategory] = &[
/// Get the JSON schema for a specific tool by name
fn get_tool_schema(name: &str) -> Option<Value> {
match name {
"find_rust_symbol" => Some(json!({
"name": "find_rust_symbol",
"description": "Find a rust symbol using rust-analyzer's exact type-inference. Only available in rust projects.",
"inputSchema": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "The exact name of the symbol to find"
}
},
"required": ["query"]
}
})),
"find_function" => Some(json!({
"name": "find_function",
"description": "Find a function or macro by exact name, optionally at a specific git commit or branch",
Expand Down Expand Up @@ -2531,6 +2546,7 @@ fn get_tool_schema(name: &str) -> Option<Value> {
/// Get all tool schemas as a vector
fn get_all_tool_schemas() -> Vec<Value> {
let tool_names = [
"find_rust_symbol",
"find_function",
"find_type",
"find_callers",
Expand Down Expand Up @@ -2572,7 +2588,15 @@ impl McpServer {
model_path: Option<String>,
lazy_mode: bool,
) -> Result<Self> {
let db = Arc::new(DatabaseManager::new(database_path, git_repo_path.to_string()).await?);
let db_manager = DatabaseManager::new(database_path, git_repo_path.to_string()).await?;
let db = Arc::new(db_manager);

let db_clone = db.clone();
tokio::spawn(async move {
if let Err(e) = db_clone.attach_rust_analyzer().await {
tracing::warn!("Failed to attach rust-analyzer: {}", e);
}
});

// Get the default git SHA (current HEAD)
let default_git_sha = match git::get_git_sha(git_repo_path) {
Expand Down Expand Up @@ -2617,7 +2641,7 @@ impl McpServer {
/// If branch is provided, resolve it to a SHA. Otherwise use git_sha or default.
/// When using the default HEAD SHA (no explicit git_sha or branch), refreshes
/// the working directory overlay so queries reflect uncommitted changes.
fn resolve_git_sha_or_branch(
async fn resolve_git_sha_or_branch(
&self,
git_sha_arg: Option<&str>,
branch_arg: Option<&str>,
Expand All @@ -2642,17 +2666,17 @@ impl McpServer {
self.db.clear_workdir_index();
} else {
// Using default HEAD — refresh workdir overlay
self.refresh_workdir_index();
self.refresh_workdir_index().await;
}

self.resolve_git_sha(git_sha_arg)
}

/// Rebuild the working directory index to reflect current file state.
fn refresh_workdir_index(&self) {
async fn refresh_workdir_index(&self) {
let repo_path = std::path::Path::new(&self.git_repo_path);
let previous = self.db.take_workdir_index();
match semcode::WorkdirIndex::build_incremental(repo_path, previous.as_ref()) {
match semcode::WorkdirIndex::build_incremental(repo_path, previous.as_ref()).await {
Ok(workdir) => {
if !workdir.is_empty() {
self.db.set_workdir_index(workdir);
Expand Down Expand Up @@ -2808,6 +2832,7 @@ impl McpServer {
let arguments = &params["arguments"];

match name {
"find_rust_symbol" => self.handle_find_rust_symbol(arguments).await,
"find_function" => self.handle_find_function(arguments).await,
"find_type" => self.handle_find_type(arguments).await,
"find_callers" => self.handle_find_callers(arguments).await,
Expand Down Expand Up @@ -2943,6 +2968,51 @@ impl McpServer {
}

// Tool implementation methods
async fn handle_find_rust_symbol(&self, args: &Value) -> Value {
let query = args["query"].as_str().unwrap_or("");

let lsp = match self.db.rust_analyzer() {
Some(lsp) => lsp,
None => {
return json!({
"error": "rust-analyzer is not available. Is this a Rust project with a Cargo.toml?",
"isError": true
});
}
};

match lsp.workspace_symbol(query).await {
Ok(result) => {
// An empty result is ambiguous: either the symbol genuinely
// doesn't exist, or rust-analyzer is still indexing the project
// (workspace/symbol returns nothing until indexing completes).
// Surface that hint rather than implying a definitive "no match".
let is_empty = match &result {
Value::Array(arr) => arr.is_empty(),
Value::Null => true,
_ => false,
};
if is_empty {
return json!({
"content": [{"type": "text", "text": format!(
"No symbols matching '{query}' found. If this is a large project, \
rust-analyzer may still be indexing — retry in a few seconds."
)}]
});
}
let formatted =
serde_json::to_string_pretty(&result).unwrap_or_else(|_| "[]".to_string());
json!({
"content": [{"type": "text", "text": truncate_output(formatted)}]
})
}
Err(e) => json!({
"error": format!("rust-analyzer error: {}", e),
"isError": true
}),
}
}

async fn handle_find_function(&self, args: &Value) -> Value {
// Check if database is empty and return helpful message
if let Some(status_msg) = self.check_database_status().await {
Expand All @@ -2954,7 +3024,9 @@ impl McpServer {
let name = args["name"].as_str().unwrap_or("");
let git_sha_arg = args["git_sha"].as_str();
let branch_arg = args["branch"].as_str();
let git_sha = self.resolve_git_sha_or_branch(git_sha_arg, branch_arg);
let git_sha = self
.resolve_git_sha_or_branch(git_sha_arg, branch_arg)
.await;

match mcp_query_function_or_macro(&self.db, name, &git_sha).await {
Ok(output) => json!({
Expand All @@ -2978,7 +3050,9 @@ impl McpServer {
let name = args["name"].as_str().unwrap_or("");
let git_sha_arg = args["git_sha"].as_str();
let branch_arg = args["branch"].as_str();
let git_sha = self.resolve_git_sha_or_branch(git_sha_arg, branch_arg);
let git_sha = self
.resolve_git_sha_or_branch(git_sha_arg, branch_arg)
.await;

match mcp_query_type_or_typedef(&self.db, name, &git_sha).await {
Ok(output) => json!({
Expand All @@ -3002,7 +3076,9 @@ impl McpServer {
let name = args["name"].as_str().unwrap_or("");
let git_sha_arg = args["git_sha"].as_str();
let branch_arg = args["branch"].as_str();
let git_sha = self.resolve_git_sha_or_branch(git_sha_arg, branch_arg);
let git_sha = self
.resolve_git_sha_or_branch(git_sha_arg, branch_arg)
.await;

match mcp_show_callers(&self.db, name, &git_sha).await {
Ok(output) => json!({
Expand All @@ -3026,7 +3102,9 @@ impl McpServer {
let name = args["name"].as_str().unwrap_or("");
let git_sha_arg = args["git_sha"].as_str();
let branch_arg = args["branch"].as_str();
let git_sha = self.resolve_git_sha_or_branch(git_sha_arg, branch_arg);
let git_sha = self
.resolve_git_sha_or_branch(git_sha_arg, branch_arg)
.await;

match mcp_show_calls(&self.db, name, &git_sha).await {
Ok(output) => json!({
Expand All @@ -3050,7 +3128,9 @@ impl McpServer {
let name = args["name"].as_str().unwrap_or("");
let git_sha_arg = args["git_sha"].as_str();
let branch_arg = args["branch"].as_str();
let git_sha = self.resolve_git_sha_or_branch(git_sha_arg, branch_arg);
let git_sha = self
.resolve_git_sha_or_branch(git_sha_arg, branch_arg)
.await;

// Parse the new parameters with same defaults as query tool
let up_levels = args["up_levels"].as_u64().unwrap_or(2) as usize;
Expand Down Expand Up @@ -3110,7 +3190,9 @@ impl McpServer {
let path_pattern = args["path_pattern"].as_str();
let limit = args["limit"].as_u64().unwrap_or(100) as usize;

let git_sha = self.resolve_git_sha_or_branch(git_sha_arg, branch_arg);
let git_sha = self
.resolve_git_sha_or_branch(git_sha_arg, branch_arg)
.await;

match mcp_grep_function_bodies(&self.db, pattern, verbose, path_pattern, limit, &git_sha)
.await
Expand Down Expand Up @@ -3139,7 +3221,9 @@ impl McpServer {
let path_pattern = args["path_pattern"].as_str();
let limit = args["limit"].as_u64().unwrap_or(10) as usize;

let _git_sha = self.resolve_git_sha_or_branch(git_sha_arg, branch_arg);
let _git_sha = self
.resolve_git_sha_or_branch(git_sha_arg, branch_arg)
.await;

match mcp_vgrep_similar_functions(
&self.db,
Expand Down Expand Up @@ -5903,6 +5987,7 @@ mod tests {
fn test_get_tool_schema_returns_valid_schemas() {
// Test that all known tools return valid schemas
let known_tools = [
"find_rust_symbol",
"find_function",
"find_type",
"find_callers",
Expand Down Expand Up @@ -5947,7 +6032,7 @@ mod tests {
#[test]
fn test_get_all_tool_schemas_returns_16_tools() {
let schemas = get_all_tool_schemas();
assert_eq!(schemas.len(), 16, "Should return all 16 tool schemas");
assert_eq!(schemas.len(), 17, "Should return all 17 tool schemas");
}

#[test]
Expand Down Expand Up @@ -6177,7 +6262,7 @@ mod tests {
let result = server.handle_list_tools().await;
let tools = result["tools"].as_array().unwrap();

// Should return all 16 tools
assert_eq!(tools.len(), 16, "Non-lazy mode should return all 16 tools");
// Should return all 17 tools
assert_eq!(tools.len(), 17, "Non-lazy mode should return all 17 tools");
}
}
22 changes: 22 additions & 0 deletions src/database/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ pub struct DatabaseManager {
symbol_filename_store: SymbolFilenameStore,
branch_store: IndexedBranchStore,
workdir_index: std::sync::RwLock<Option<WorkdirIndex>>,
rust_analyzer: std::sync::RwLock<Option<std::sync::Arc<crate::rust_analyzer::RustAnalyzer>>>,
}

impl DatabaseManager {
Expand All @@ -62,9 +63,30 @@ impl DatabaseManager {
symbol_filename_store: SymbolFilenameStore::new(connection.clone()),
branch_store: IndexedBranchStore::new(connection.clone()),
workdir_index: std::sync::RwLock::new(None),
rust_analyzer: std::sync::RwLock::new(None),
})
}

pub async fn attach_rust_analyzer(&self) -> Result<()> {
if std::path::Path::new(&self.git_repo_path)
.join("Cargo.toml")
.exists()
{
let lsp = crate::rust_analyzer::RustAnalyzer::start(std::path::Path::new(
&self.git_repo_path,
))
.await?;
if let Ok(mut w) = self.rust_analyzer.write() {
*w = Some(std::sync::Arc::new(lsp));
}
}
Ok(())
}

pub fn rust_analyzer(&self) -> Option<std::sync::Arc<crate::rust_analyzer::RustAnalyzer>> {
self.rust_analyzer.read().ok()?.clone()
}

pub async fn list_tables(&self) -> Result<Vec<String>> {
Ok(self.connection.table_names().execute().await?)
}
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ pub mod git_range;
pub mod hash;
pub mod indexer;
pub mod perf_monitor;
pub mod rust_analyzer;
pub mod symbol_walkback;
pub mod text_utils;
mod treesitter_analyzer;
Expand Down
Loading