use async_trait::async_trait;
use serde::Deserialize;
use std::path::PathBuf;
use std::sync::Arc;
use crate::integration::HostIntegration;
use crate::lsp::{LspService, LspDiagnostic};
use crate::tools::{Tool, ToolError, ToolResponse, Permission};
pub struct DiagnosticsTool {
lsp_service: Option<Arc<dyn LspService>>,
}
#[derive(Debug, Deserialize)]
struct DiagnosticsParams {
file_path: PathBuf,
errors_only: Option<bool>,
}
impl DiagnosticsTool {
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_diagnostics(&self, diagnostics: &[LspDiagnostic], errors_only: bool) -> String {
if diagnostics.is_empty() {
return "No diagnostics found for this file.".to_string();
}
let filtered_diagnostics: Vec<_> = if errors_only {
diagnostics.iter().filter(|d| d.is_error()).collect()
} else {
diagnostics.iter().collect()
};
if filtered_diagnostics.is_empty() {
return if errors_only {
"No errors found for this file.".to_string()
} else {
"No diagnostics found for this file.".to_string()
};
}
let mut result = String::new();
result.push_str(&format!("Found {} diagnostic(s):\n\n", filtered_diagnostics.len()));
for (i, diagnostic) in filtered_diagnostics.iter().enumerate() {
result.push_str(&format!("{}. {}\n", i + 1, diagnostic.format()));
}
let error_count = filtered_diagnostics.iter().filter(|d| d.is_error()).count();
let warning_count = filtered_diagnostics.iter().filter(|d| d.is_warning()).count();
result.push_str(&format!("\nSummary: {} error(s), {} warning(s)", error_count, warning_count));
result
}
}
impl Default for DiagnosticsTool {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Tool for DiagnosticsTool {
async fn execute(
&self,
parameters: serde_json::Value,
_host: &dyn HostIntegration,
) -> Result<ToolResponse, ToolError> {
let params: DiagnosticsParams = 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 diagnostics = lsp_service.get_diagnostics(¶ms.file_path).await
.map_err(|e| ToolError::ExecutionFailed(format!("Failed to get diagnostics: {}", e)))?;
let errors_only = params.errors_only.unwrap_or(false);
let formatted = self.format_diagnostics(&diagnostics, errors_only);
Ok(ToolResponse::success(formatted))
}
fn name(&self) -> &str {
"diagnostics"
}
fn description(&self) -> &str {
"Get code diagnostics (errors, warnings, etc.) for a file 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 diagnostics for"
},
"errors_only": {
"type": "boolean",
"description": "Whether to include only errors (default: false, includes all diagnostics)",
"default": false
}
},
"required": ["file_path"]
})
}
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::DiagnosticSeverity;
use std::path::PathBuf;
#[test]
fn test_tool_creation() {
let tool = DiagnosticsTool::new();
assert_eq!(tool.name(), "diagnostics");
assert!(!tool.description().is_empty());
}
#[test]
fn test_parameter_schema() {
let tool = DiagnosticsTool::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_diagnostics_empty() {
let tool = DiagnosticsTool::new();
let diagnostics = vec![];
let formatted = tool.format_diagnostics(&diagnostics, false);
assert!(formatted.contains("No diagnostics found"));
}
#[test]
fn test_format_diagnostics_with_data() {
let tool = DiagnosticsTool::new();
let diagnostics = vec![
LspDiagnostic {
file_path: PathBuf::from("test.rs"),
line: 10,
column: 5,
end_line: 10,
end_column: 15,
message: "unused variable".to_string(),
severity: DiagnosticSeverity::Warning,
source: Some("rust-analyzer".to_string()),
code: Some("unused_variables".to_string()),
},
LspDiagnostic {
file_path: PathBuf::from("test.rs"),
line: 20,
column: 0,
end_line: 20,
end_column: 10,
message: "syntax error".to_string(),
severity: DiagnosticSeverity::Error,
source: Some("rust-analyzer".to_string()),
code: None,
},
];
let formatted = tool.format_diagnostics(&diagnostics, false);
assert!(formatted.contains("Found 2 diagnostic(s)"));
assert!(formatted.contains("unused variable"));
assert!(formatted.contains("syntax error"));
assert!(formatted.contains("1 error(s), 1 warning(s)"));
}
#[test]
fn test_format_diagnostics_errors_only() {
let tool = DiagnosticsTool::new();
let diagnostics = vec![
LspDiagnostic {
file_path: PathBuf::from("test.rs"),
line: 10,
column: 5,
end_line: 10,
end_column: 15,
message: "unused variable".to_string(),
severity: DiagnosticSeverity::Warning,
source: Some("rust-analyzer".to_string()),
code: Some("unused_variables".to_string()),
},
LspDiagnostic {
file_path: PathBuf::from("test.rs"),
line: 20,
column: 0,
end_line: 20,
end_column: 10,
message: "syntax error".to_string(),
severity: DiagnosticSeverity::Error,
source: Some("rust-analyzer".to_string()),
code: None,
},
];
let formatted = tool.format_diagnostics(&diagnostics, true);
assert!(formatted.contains("Found 1 diagnostic(s)"));
assert!(!formatted.contains("unused variable"));
assert!(formatted.contains("syntax error"));
}
}