use async_trait::async_trait;
use serde::Deserialize;
use std::path::PathBuf;
use std::sync::Arc;
use crate::integration::HostIntegration;
use crate::lsp::{LspService, Location, Position};
use crate::tools::{Tool, ToolError, ToolResponse, Permission};
pub struct GotoDefinitionTool {
lsp_service: Option<Arc<dyn LspService>>,
}
#[derive(Debug, Deserialize)]
struct GotoDefinitionParams {
file_path: PathBuf,
line: u32,
character: u32,
}
impl GotoDefinitionTool {
pub fn new() -> Self {
Self {
lsp_service: None,
}
}
pub fn set_lsp_service(&mut self, lsp_service: Arc<dyn LspService>) {
self.lsp_service = Some(lsp_service);
}
fn format_locations(&self, locations: &[Location], source_file: &std::path::Path, source_position: &Position) -> String {
if locations.is_empty() {
return format!("No definition found for symbol at {}:{}:{}",
source_file.display(), source_position.line + 1, source_position.character + 1);
}
let mut result = String::new();
if locations.len() == 1 {
result.push_str("Definition found:\n\n");
} else {
result.push_str(&format!("Found {} definitions:\n\n", locations.len()));
}
for (i, location) in locations.iter().enumerate() {
if locations.len() > 1 {
result.push_str(&format!("{}. ", i + 1));
}
result.push_str(&format!("{}:{}:{}",
location.file_path.display(),
location.range.start.line + 1,
location.range.start.character + 1));
if location.range.start.line != location.range.end.line ||
location.range.start.character != location.range.end.character {
result.push_str(&format!(" to {}:{}",
location.range.end.line + 1,
location.range.end.character + 1));
}
result.push('\n');
}
result.push_str("\nYou can navigate to these locations to see the definitions.");
result
}
}
impl Default for GotoDefinitionTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Tool for GotoDefinitionTool {
async fn execute(
&self,
parameters: serde_json::Value,
_host: &dyn HostIntegration,
) -> Result<ToolResponse, ToolError> {
let params: GotoDefinitionParams = serde_json::from_value(parameters)
.map_err(|e| ToolError::InvalidParameters(format!("Invalid parameters: {}", e)))?;
let lsp_service = self.lsp_service.as_ref()
.ok_or_else(|| ToolError::ExecutionFailed("LSP service not available".to_string()))?;
if !lsp_service.supports_file(¶ms.file_path) {
return Ok(ToolResponse::success(format!(
"LSP support not available for file: {}",
params.file_path.display()
)));
}
let position = Position {
line: params.line,
character: params.character,
};
let locations = lsp_service.goto_definition(¶ms.file_path, position).await
.map_err(|e| ToolError::ExecutionFailed(format!("Failed to get definition: {}", e)))?;
let formatted = self.format_locations(&locations, ¶ms.file_path, &position);
Ok(ToolResponse::success(formatted))
}
fn name(&self) -> &str {
"goto_definition"
}
fn description(&self) -> &str {
"Go to the definition of a symbol at a specific position using LSP"
}
fn requires_permission(&self) -> Permission {
Permission::None }
fn parameter_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"file_path": {
"type": "string",
"description": "Path to the file containing the symbol"
},
"line": {
"type": "integer",
"description": "Line number (0-based) of the symbol",
"minimum": 0
},
"character": {
"type": "integer",
"description": "Column number (0-based) of the symbol",
"minimum": 0
}
},
"required": ["file_path", "line", "character"]
})
}
fn clone_box(&self) -> Box<dyn Tool> {
Box::new(Self {
lsp_service: self.lsp_service.clone(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lsp::types::Range;
use std::path::PathBuf;
#[test]
fn test_tool_creation() {
let tool = GotoDefinitionTool::new();
assert_eq!(tool.name(), "goto_definition");
assert!(!tool.description().is_empty());
}
#[test]
fn test_parameter_schema() {
let tool = GotoDefinitionTool::new();
let schema = tool.parameter_schema();
assert!(schema.get("type").is_some());
assert!(schema.get("properties").is_some());
assert!(schema.get("required").is_some());
}
#[test]
fn test_format_locations_empty() {
let tool = GotoDefinitionTool::new();
let locations = vec![];
let position = Position { line: 5, character: 10 };
let file_path = std::path::Path::new("main.rs");
let formatted = tool.format_locations(&locations, file_path, &position);
assert!(formatted.contains("No definition found"));
assert!(formatted.contains("main.rs:6:11"));
}
#[test]
fn test_format_locations_single() {
let tool = GotoDefinitionTool::new();
let locations = vec![
Location {
file_path: PathBuf::from("lib.rs"),
range: Range {
start: Position { line: 10, character: 4 },
end: Position { line: 10, character: 12 },
},
}
];
let position = Position { line: 5, character: 10 };
let file_path = std::path::Path::new("main.rs");
let formatted = tool.format_locations(&locations, file_path, &position);
assert!(formatted.contains("Definition found:"));
assert!(formatted.contains("lib.rs:11:5 to 11:13"));
assert!(!formatted.contains("1."));
}
#[test]
fn test_format_locations_multiple() {
let tool = GotoDefinitionTool::new();
let locations = vec![
Location {
file_path: PathBuf::from("lib.rs"),
range: Range {
start: Position { line: 10, character: 4 },
end: Position { line: 10, character: 12 },
},
},
Location {
file_path: PathBuf::from("mod.rs"),
range: Range {
start: Position { line: 5, character: 0 },
end: Position { line: 5, character: 8 },
},
}
];
let position = Position { line: 5, character: 10 };
let file_path = std::path::Path::new("main.rs");
let formatted = tool.format_locations(&locations, file_path, &position);
assert!(formatted.contains("Found 2 definitions:"));
assert!(formatted.contains("1. lib.rs:11:5"));
assert!(formatted.contains("2. mod.rs:6:1"));
}
#[test]
fn test_format_locations_single_character() {
let tool = GotoDefinitionTool::new();
let locations = vec![
Location {
file_path: PathBuf::from("lib.rs"),
range: Range {
start: Position { line: 10, character: 4 },
end: Position { line: 10, character: 4 }, },
}
];
let position = Position { line: 5, character: 10 };
let file_path = std::path::Path::new("main.rs");
let formatted = tool.format_locations(&locations, file_path, &position);
println!("Formatted output: {}", formatted);
assert!(formatted.contains("lib.rs:11:5"));
assert!(!formatted.contains(" to "), "Output should not contain ' to ' for single character range, but got: {}", formatted);
}
}