use async_trait::async_trait;
use serde::Deserialize;
use std::path::PathBuf;
use std::sync::Arc;
use crate::integration::HostIntegration;
use crate::lsp::{LspService, Hover, Position};
use crate::tools::{Tool, ToolError, ToolResponse, Permission};
pub struct HoverTool {
lsp_service: Option<Arc<dyn LspService>>,
}
#[derive(Debug, Deserialize)]
struct HoverParams {
file_path: PathBuf,
line: u32,
character: u32,
}
impl HoverTool {
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_hover(&self, hover: &Hover, file_path: &std::path::Path, position: &Position) -> String {
let mut result = String::new();
result.push_str(&format!("Hover information for {}:{}:{}\n\n",
file_path.display(), position.line + 1, position.character + 1));
result.push_str(&hover.contents);
if let Some(range) = &hover.range {
result.push_str(&format!("\n\nRange: {}:{} to {}:{}",
range.start.line + 1, range.start.character + 1,
range.end.line + 1, range.end.character + 1));
}
result
}
}
impl Default for HoverTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Tool for HoverTool {
async fn execute(
&self,
parameters: serde_json::Value,
_host: &dyn HostIntegration,
) -> Result<ToolResponse, ToolError> {
let params: HoverParams = 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 hover = lsp_service.get_hover(¶ms.file_path, position).await
.map_err(|e| ToolError::ExecutionFailed(format!("Failed to get hover information: {}", e)))?;
let formatted = if let Some(hover) = hover {
self.format_hover(&hover, ¶ms.file_path, &position)
} else {
format!("No hover information available at {}:{}:{}",
params.file_path.display(), params.line + 1, params.character + 1)
};
Ok(ToolResponse::success(formatted))
}
fn name(&self) -> &str {
"hover"
}
fn description(&self) -> &str {
"Get hover information (documentation, type info) 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 to get hover information for"
},
"line": {
"type": "integer",
"description": "Line number (0-based) where to get hover information",
"minimum": 0
},
"character": {
"type": "integer",
"description": "Column number (0-based) where to get hover information",
"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;
#[test]
fn test_tool_creation() {
let tool = HoverTool::new();
assert_eq!(tool.name(), "hover");
assert!(!tool.description().is_empty());
}
#[test]
fn test_parameter_schema() {
let tool = HoverTool::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_hover_basic() {
let tool = HoverTool::new();
let hover = Hover {
contents: "fn main() -> ()\nThe main function".to_string(),
range: None,
};
let position = Position { line: 0, character: 3 };
let file_path = std::path::Path::new("main.rs");
let formatted = tool.format_hover(&hover, file_path, &position);
assert!(formatted.contains("main.rs:1:4"));
assert!(formatted.contains("fn main() -> ()"));
assert!(formatted.contains("The main function"));
}
#[test]
fn test_format_hover_with_range() {
let tool = HoverTool::new();
let hover = Hover {
contents: "struct String".to_string(),
range: Some(Range {
start: Position { line: 5, character: 10 },
end: Position { line: 5, character: 16 },
}),
};
let position = Position { line: 5, character: 12 };
let file_path = std::path::Path::new("lib.rs");
let formatted = tool.format_hover(&hover, file_path, &position);
assert!(formatted.contains("lib.rs:6:13"));
assert!(formatted.contains("struct String"));
assert!(formatted.contains("Range: 6:11 to 6:17"));
}
}