coderlib 0.1.0

A Rust library for AI-powered code assistance and agentic system
Documentation
//! Diagnostics tool for accessing LSP diagnostics

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};

/// Tool for getting code diagnostics from LSP servers
pub struct DiagnosticsTool {
    lsp_service: Option<Arc<dyn LspService>>,
}

/// Parameters for the diagnostics tool
#[derive(Debug, Deserialize)]
struct DiagnosticsParams {
    /// File path to get diagnostics for
    file_path: PathBuf,
    /// Whether to include only errors (default: false, includes all)
    errors_only: Option<bool>,
}

impl DiagnosticsTool {
    /// Create a new diagnostics tool
    pub fn new() -> Self {
        Self {
            lsp_service: None,
        }
    }
    
    /// Set the LSP service for this tool
    pub fn set_lsp_service(&mut self, lsp_service: Arc<dyn LspService>) {
        self.lsp_service = Some(lsp_service);
    }
    
    /// Format diagnostics for display
    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()));
        }
        
        // Add summary
        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)))?;
        
        // Check if LSP service is available
        let lsp_service = self.lsp_service.as_ref()
            .ok_or_else(|| ToolError::ExecutionFailed("LSP service not available".to_string()))?;
        
        // Check if the file is supported by LSP
        if !lsp_service.supports_file(&params.file_path) {
            return Ok(ToolResponse::success(format!(
                "LSP support not available for file: {}",
                params.file_path.display()
            )));
        }
        
        // Get diagnostics from LSP
        let diagnostics = lsp_service.get_diagnostics(&params.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 // Diagnostics are read-only
    }
    
    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"));
    }
}