use super::{Tool, ToolInfo};
use anyhow::{anyhow, Result};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tokio::fs;
#[derive(Debug, Clone)]
pub struct EditorTool;
#[derive(Debug, Deserialize, Serialize)]
struct EditorParams {
command: String,
path: String,
#[serde(skip_serializing_if = "Option::is_none")]
file_text: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
view_range: Option<[i32; 2]>,
#[serde(skip_serializing_if = "Option::is_none")]
old_str: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
new_str: Option<String>,
}
impl Default for EditorTool {
fn default() -> Self {
Self::new()
}
}
impl EditorTool {
pub fn new() -> Self {
Self
}
async fn view_file(&self, path: &Path, range: Option<[i32; 2]>) -> Result<String> {
let content = fs::read_to_string(path).await
.map_err(|e| anyhow!("Failed to read file: {}", e))?;
let lines: Vec<&str> = content.lines().collect();
let total_lines = lines.len();
let (start, end) = if let Some([start, end]) = range {
let start = if start < 1 { 1 } else { start as usize };
let end = if end == -1 || end as usize > total_lines {
total_lines
} else {
end as usize
};
(start, end)
} else {
(1, total_lines)
};
let mut result = format!("File: {} (lines {}-{} of {})\n", path.display(), start, end, total_lines);
result.push_str(&"─".repeat(60));
result.push('\n');
for (idx, line) in lines.iter().enumerate() {
let line_num = idx + 1;
if line_num >= start && line_num <= end {
result.push_str(&format!("{line_num:6} │ {line}\n"));
}
}
Ok(result)
}
async fn create_file(&self, path: &Path, content: &str) -> Result<String> {
if path.exists() {
return Err(anyhow!("File already exists: {}", path.display()));
}
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await
.map_err(|e| anyhow!("Failed to create parent directories: {}", e))?;
}
fs::write(path, content).await
.map_err(|e| anyhow!("Failed to write file: {}", e))?;
Ok(format!("File created successfully at: {}", path.display()))
}
async fn str_replace(&self, path: &Path, old_str: &str, new_str: &str) -> Result<String> {
let content = fs::read_to_string(path).await
.map_err(|e| anyhow!("Failed to read file: {}", e))?;
let occurrences = content.matches(old_str).count();
if occurrences == 0 {
return Err(anyhow!("Could not find the exact text to replace in {}", path.display()));
} else if occurrences > 1 {
return Err(anyhow!(
"Found multiple ({}) occurrences of the text in {}. Must be unique.",
occurrences,
path.display()
));
}
let new_content = content.replace(old_str, new_str);
fs::write(path, new_content).await
.map_err(|e| anyhow!("Failed to write file: {}", e))?;
Ok(format!("Successfully replaced text in {}", path.display()))
}
}
#[async_trait]
impl Tool for EditorTool {
fn info(&self) -> ToolInfo {
ToolInfo {
name: "editor".to_string(),
description: r#"File viewing and editing tool
* Use 'view' to display file contents with line numbers
* Use 'create' to create new files (fails if file exists)
* Use 'str_replace' to replace unique text occurrences
* view_range: [start, end] where lines are 1-based, use -1 for end to read until EOF"#.to_string(),
input_schema: serde_json::json!({
"type": "object",
"properties": {
"command": {
"type": "string",
"enum": ["view", "create", "str_replace"],
"description": "The command to run"
},
"path": {
"type": "string",
"description": "Path to the file"
},
"file_text": {
"type": "string",
"description": "Content for create command"
},
"view_range": {
"type": "array",
"items": {"type": "integer"},
"minItems": 2,
"maxItems": 2,
"description": "Line range [start, end] for view command"
},
"old_str": {
"type": "string",
"description": "Text to find for str_replace"
},
"new_str": {
"type": "string",
"description": "Replacement text for str_replace"
}
},
"required": ["command", "path"]
}),
}
}
async fn execute(&self, params: serde_json::Value) -> Result<String> {
let editor_params: EditorParams = serde_json::from_value(params)
.map_err(|e| anyhow!("Invalid parameters: {}", e))?;
let path = PathBuf::from(&editor_params.path);
match editor_params.command.as_str() {
"view" => self.view_file(&path, editor_params.view_range).await,
"create" => {
let content = editor_params.file_text
.ok_or_else(|| anyhow!("Missing file_text for create command"))?;
self.create_file(&path, &content).await
}
"str_replace" => {
let old_str = editor_params.old_str
.ok_or_else(|| anyhow!("Missing old_str for str_replace"))?;
let new_str = editor_params.new_str
.ok_or_else(|| anyhow!("Missing new_str for str_replace"))?;
self.str_replace(&path, &old_str, &new_str).await
}
_ => Err(anyhow!("Unknown command: {}", editor_params.command)),
}
}
}