use crate::tools::lsp_diagnostics::{detect_language_id, ensure_lsp_started, path_to_uri};
use crate::tools::lsp_goto_def::parse_locations;
use crate::tools::{Tool, ToolContext, ToolResult};
use anyhow::Result;
use async_trait::async_trait;
use serde_json::json;
pub struct LspReferencesTool;
#[async_trait]
impl Tool for LspReferencesTool {
fn name(&self) -> &str {
"lsp_find_references"
}
fn description(&self) -> &str {
"Find all usages/references to a symbol using the Language Server Protocol. \
Provide the file path and cursor position (0-indexed). \
Returns file:line:col for each reference location."
}
fn parameters_schema(&self) -> serde_json::Value {
json!({
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "Path to the source file"
},
"line": {
"type": "integer",
"description": "0-indexed line number of the cursor position"
},
"character": {
"type": "integer",
"description": "0-indexed character offset on the line"
},
"include_declaration": {
"type": "boolean",
"description": "Include the declaration site in results (default: true)"
}
},
"required": ["path", "line", "character"]
})
}
async fn execute(&self, args: serde_json::Value, ctx: &ToolContext) -> Result<ToolResult> {
let path_str = match args["path"].as_str() {
Some(p) => p,
None => {
return Ok(ToolResult {
output: "Missing required parameter: path".into(),
is_error: true,
})
}
};
let line = match args["line"].as_u64() {
Some(l) => l,
None => {
return Ok(ToolResult {
output: "Missing required parameter: line".into(),
is_error: true,
})
}
};
let character = match args["character"].as_u64() {
Some(c) => c,
None => {
return Ok(ToolResult {
output: "Missing required parameter: character".into(),
is_error: true,
})
}
};
let include_declaration = args["include_declaration"].as_bool().unwrap_or(true);
let abs_path = if std::path::Path::new(path_str).is_absolute() {
std::path::PathBuf::from(path_str)
} else {
ctx.working_dir.join(path_str)
};
if !abs_path.exists() {
return Ok(ToolResult {
output: format!("File not found: {}", abs_path.display()),
is_error: true,
});
}
let mut guard = ctx.lsp_client.lock().await;
if guard.is_none() {
match ensure_lsp_started(&ctx.working_dir).await {
Ok(client) => *guard = Some(client),
Err(e) => {
return Ok(ToolResult {
output: format!("LSP server not available: {}", e),
is_error: true,
})
}
}
}
let client = guard.as_mut().unwrap();
let file_uri = path_to_uri(&abs_path);
let lang_id = detect_language_id(&abs_path);
let content = std::fs::read_to_string(&abs_path).unwrap_or_default();
let _ = client
.send_notification(
"textDocument/didOpen",
json!({
"textDocument": {
"uri": file_uri,
"languageId": lang_id,
"version": 1,
"text": content
}
}),
)
.await;
let result = client
.send_request(
"textDocument/references",
json!({
"textDocument": { "uri": file_uri },
"position": { "line": line, "character": character },
"context": { "includeDeclaration": include_declaration }
}),
)
.await;
let _ = client
.send_notification(
"textDocument/didClose",
json!({ "textDocument": { "uri": file_uri } }),
)
.await;
match result {
Err(e) => Ok(ToolResult {
output: format!("LSP references request failed: {}", e),
is_error: true,
}),
Ok(val) => {
let locations = parse_locations(&val);
if locations.is_empty() {
Ok(ToolResult {
output: "No references found.".into(),
is_error: false,
})
} else {
Ok(ToolResult {
output: format!(
"Found {} reference(s):\n{}",
locations.len(),
locations.join("\n")
),
is_error: false,
})
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::NullIO;
use std::sync::Arc;
use tokio::sync::Mutex;
fn ctx() -> ToolContext {
ToolContext {
working_dir: std::path::PathBuf::from("/tmp"),
sandbox_enabled: false,
io: Arc::new(NullIO),
compact_mode: false,
lsp_client: Arc::new(Mutex::new(None)),
mcp_client: None,
nesting_depth: 0,
llm: std::sync::Arc::new(crate::llm::NullLlmProvider),
tools: std::sync::Arc::new(crate::tools::ToolRegistry::new()),
permissions: vec![],
formatters: std::collections::HashMap::new(),
}
}
#[test]
fn test_lsp_references_metadata() {
let tool = LspReferencesTool;
assert_eq!(tool.name(), "lsp_find_references");
assert!(!tool.description().is_empty());
let schema = tool.parameters_schema();
let required = schema["required"].as_array().unwrap();
assert!(required.contains(&json!("path")));
assert!(required.contains(&json!("line")));
assert!(required.contains(&json!("character")));
assert!(!required.contains(&json!("include_declaration")));
}
#[tokio::test]
async fn test_lsp_references_missing_params() {
let tool = LspReferencesTool;
let result = tool.execute(json!({}), &ctx()).await.unwrap();
assert!(result.is_error);
assert!(result.output.contains("Missing required parameter"));
}
#[tokio::test]
async fn test_lsp_references_nonexistent_file() {
let tool = LspReferencesTool;
let result = tool
.execute(
json!({ "path": "/nonexistent/file.rs", "line": 0, "character": 0 }),
&ctx(),
)
.await
.unwrap();
assert!(result.is_error);
}
#[tokio::test]
async fn test_lsp_references_include_declaration_default() {
let tool = LspReferencesTool;
let result = tool
.execute(
json!({ "path": "/nonexistent/file.rs", "line": 5, "character": 10 }),
&ctx(),
)
.await
.unwrap();
assert!(result.is_error);
assert!(!result.output.contains("Missing required parameter"));
}
}