coderlib 0.1.0

A Rust library for AI-powered code assistance and agentic system
Documentation
//! Code completion tool using LSP

use async_trait::async_trait;
use serde::Deserialize;
use std::path::PathBuf;
use std::sync::Arc;

use crate::integration::HostIntegration;
use crate::lsp::{LspService, CompletionItem, Position};
use crate::tools::{Tool, ToolError, ToolResponse, Permission};

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

/// Parameters for the completion tool
#[derive(Debug, Deserialize)]
struct CompletionParams {
    /// File path to get completions for
    file_path: PathBuf,
    /// Line number (0-based)
    line: u32,
    /// Column number (0-based)
    character: u32,
    /// Maximum number of completions to return (default: 20)
    max_results: Option<usize>,
}

impl CompletionTool {
    /// Create a new completion 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 completions for display
    fn format_completions(&self, completions: &[CompletionItem], max_results: usize) -> String {
        if completions.is_empty() {
            return "No completions found at this position.".to_string();
        }
        
        let limited_completions: Vec<_> = completions.iter().take(max_results).collect();
        
        let mut result = String::new();
        result.push_str(&format!("Found {} completion(s) (showing {}):\n\n", 
                                completions.len(), limited_completions.len()));
        
        for (i, completion) in limited_completions.iter().enumerate() {
            result.push_str(&format!("{}. {}", i + 1, completion.label));
            
            if let Some(kind) = &completion.kind {
                result.push_str(&format!(" ({:?})", kind));
            }
            
            if let Some(detail) = &completion.detail {
                result.push_str(&format!(" - {}", detail));
            }
            
            result.push('\n');
            
            if let Some(doc) = &completion.documentation {
                if !doc.is_empty() {
                    result.push_str(&format!("   {}\n", doc));
                }
            }
        }
        
        if completions.len() > max_results {
            result.push_str(&format!("\n... and {} more completions", 
                                   completions.len() - max_results));
        }
        
        result
    }
}

impl Default for CompletionTool {
    fn default() -> Self {
        Self::new()
    }
}

#[async_trait]
impl Tool for CompletionTool {
    async fn execute(
        &self,
        parameters: serde_json::Value,
        _host: &dyn HostIntegration,
    ) -> Result<ToolResponse, ToolError> {
        let params: CompletionParams = 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()
            )));
        }
        
        let position = Position {
            line: params.line,
            character: params.character,
        };
        
        // Get completions from LSP
        let completions = lsp_service.get_completions(&params.file_path, position).await
            .map_err(|e| ToolError::ExecutionFailed(format!("Failed to get completions: {}", e)))?;
        
        let max_results = params.max_results.unwrap_or(20);
        let formatted = self.format_completions(&completions, max_results);
        
        Ok(ToolResponse::success(formatted))
    }
    
    fn name(&self) -> &str {
        "completion"
    }
    
    fn description(&self) -> &str {
        "Get code completions at a specific position using LSP"
    }
    
    fn requires_permission(&self) -> Permission {
        Permission::None // Completions 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 completions for"
                },
                "line": {
                    "type": "integer",
                    "description": "Line number (0-based) where to get completions",
                    "minimum": 0
                },
                "character": {
                    "type": "integer", 
                    "description": "Column number (0-based) where to get completions",
                    "minimum": 0
                },
                "max_results": {
                    "type": "integer",
                    "description": "Maximum number of completions to return (default: 20)",
                    "minimum": 1,
                    "maximum": 100,
                    "default": 20
                }
            },
            "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::CompletionItemKind;
    
    #[test]
    fn test_tool_creation() {
        let tool = CompletionTool::new();
        assert_eq!(tool.name(), "completion");
        assert!(!tool.description().is_empty());
    }
    
    #[test]
    fn test_parameter_schema() {
        let tool = CompletionTool::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_completions_empty() {
        let tool = CompletionTool::new();
        let completions = vec![];
        let formatted = tool.format_completions(&completions, 20);
        
        assert!(formatted.contains("No completions found"));
    }
    
    #[test]
    fn test_format_completions_with_data() {
        let tool = CompletionTool::new();
        let completions = vec![
            CompletionItem {
                label: "println!".to_string(),
                kind: Some(CompletionItemKind::Function),
                detail: Some("macro".to_string()),
                documentation: Some("Print to stdout with newline".to_string()),
                insert_text: Some("println!($0)".to_string()),
                filter_text: None,
                sort_text: None,
            },
            CompletionItem {
                label: "String".to_string(),
                kind: Some(CompletionItemKind::Struct),
                detail: Some("std::string::String".to_string()),
                documentation: Some("UTF-8 encoded string".to_string()),
                insert_text: None,
                filter_text: None,
                sort_text: None,
            },
        ];
        
        let formatted = tool.format_completions(&completions, 20);
        
        assert!(formatted.contains("Found 2 completion(s)"));
        assert!(formatted.contains("println!"));
        assert!(formatted.contains("String"));
        assert!(formatted.contains("Function"));
        assert!(formatted.contains("Struct"));
    }
    
    #[test]
    fn test_format_completions_max_results() {
        let tool = CompletionTool::new();
        let completions = vec![
            CompletionItem {
                label: "item1".to_string(),
                kind: None,
                detail: None,
                documentation: None,
                insert_text: None,
                filter_text: None,
                sort_text: None,
            },
            CompletionItem {
                label: "item2".to_string(),
                kind: None,
                detail: None,
                documentation: None,
                insert_text: None,
                filter_text: None,
                sort_text: None,
            },
            CompletionItem {
                label: "item3".to_string(),
                kind: None,
                detail: None,
                documentation: None,
                insert_text: None,
                filter_text: None,
                sort_text: None,
            },
        ];
        
        let formatted = tool.format_completions(&completions, 2);
        
        assert!(formatted.contains("Found 3 completion(s) (showing 2)"));
        assert!(formatted.contains("item1"));
        assert!(formatted.contains("item2"));
        assert!(!formatted.contains("item3"));
        assert!(formatted.contains("... and 1 more completions"));
    }
}