coderlib 0.1.0

A Rust library for AI-powered code assistance and agentic system
Documentation
//! Go to definition 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, Location, Position};
use crate::tools::{Tool, ToolError, ToolResponse, Permission};

/// Tool for going to definition using LSP servers
pub struct GotoDefinitionTool {
    lsp_service: Option<Arc<dyn LspService>>,
}

/// Parameters for the goto definition tool
#[derive(Debug, Deserialize)]
struct GotoDefinitionParams {
    /// File path to start from
    file_path: PathBuf,
    /// Line number (0-based)
    line: u32,
    /// Column number (0-based)
    character: u32,
}

impl GotoDefinitionTool {
    /// Create a new goto definition 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 locations for display
    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 the range spans multiple characters, show the end position too
            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');
        }
        
        // Add helpful information
        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)))?;
        
        // 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 definition locations from LSP
        let locations = lsp_service.goto_definition(&params.file_path, position).await
            .map_err(|e| ToolError::ExecutionFailed(format!("Failed to get definition: {}", e)))?;
        
        let formatted = self.format_locations(&locations, &params.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 // Going to definition is read-only
    }
    
    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 }, // Same position
                },
            }
        ];
        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);

        // Debug print to see actual output
        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);
    }
}