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};
pub struct CompletionTool {
lsp_service: Option<Arc<dyn LspService>>,
}
#[derive(Debug, Deserialize)]
struct CompletionParams {
file_path: PathBuf,
line: u32,
character: u32,
max_results: Option<usize>,
}
impl CompletionTool {
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_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)))?;
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 completions = lsp_service.get_completions(¶ms.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 }
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"));
}
}