From be8f3b37918c3d2aded950f561e619ac95bcbd3b Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Wed, 12 Mar 2025 16:16:26 -0400 Subject: [PATCH] Add delete-path tool (#26590) Release Notes: - N/A --- crates/assistant_tools/src/assistant_tools.rs | 3 + .../assistant_tools/src/delete_path_tool.rs | 165 ++++++++++++++++++ .../src/delete_path_tool/description.md | 1 + 3 files changed, 169 insertions(+) create mode 100644 crates/assistant_tools/src/delete_path_tool.rs create mode 100644 crates/assistant_tools/src/delete_path_tool/description.md diff --git a/crates/assistant_tools/src/assistant_tools.rs b/crates/assistant_tools/src/assistant_tools.rs index c68c227786d4523dc24c412bddd3d53bb6170f54..22ae7a0a3a3953848f97614d3167b8df5aac7c13 100644 --- a/crates/assistant_tools/src/assistant_tools.rs +++ b/crates/assistant_tools/src/assistant_tools.rs @@ -1,3 +1,4 @@ +mod delete_path_tool; mod edit_files_tool; mod list_directory_tool; mod now_tool; @@ -7,6 +8,7 @@ mod regex_search; use assistant_tool::ToolRegistry; use gpui::App; +use crate::delete_path_tool::DeletePathTool; use crate::edit_files_tool::EditFilesTool; use crate::list_directory_tool::ListDirectoryTool; use crate::now_tool::NowTool; @@ -22,4 +24,5 @@ pub fn init(cx: &mut App) { registry.register_tool(ListDirectoryTool); registry.register_tool(EditFilesTool); registry.register_tool(RegexSearchTool); + registry.register_tool(DeletePathTool); } diff --git a/crates/assistant_tools/src/delete_path_tool.rs b/crates/assistant_tools/src/delete_path_tool.rs new file mode 100644 index 0000000000000000000000000000000000000000..6a5b3a7ad53aeeec2f040fde21c60a2e92dd4b8d --- /dev/null +++ b/crates/assistant_tools/src/delete_path_tool.rs @@ -0,0 +1,165 @@ +use anyhow::{anyhow, Result}; +use assistant_tool::Tool; +use gpui::{App, Entity, Task}; +use language_model::LanguageModelRequestMessage; +use project::Project; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::{fs, path::PathBuf, sync::Arc}; +use util::paths::PathMatcher; + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +pub struct DeletePathToolInput { + /// The glob to match files in the project to delete. + /// + /// + /// If the project has the following files: + /// + /// - directory1/a/something.txt + /// - directory2/a/things.txt + /// - directory3/a/other.txt + /// + /// You can delete the first two files by providing a glob of "*thing*.txt" + /// + pub glob: String, +} + +pub struct DeletePathTool; + +impl Tool for DeletePathTool { + fn name(&self) -> String { + "delete-path".into() + } + + fn description(&self) -> String { + include_str!("./delete_path_tool/description.md").into() + } + + fn input_schema(&self) -> serde_json::Value { + let schema = schemars::schema_for!(DeletePathToolInput); + serde_json::to_value(&schema).unwrap() + } + + fn run( + self: Arc, + input: serde_json::Value, + _messages: &[LanguageModelRequestMessage], + project: Entity, + cx: &mut App, + ) -> Task> { + let glob = match serde_json::from_value::(input) { + Ok(input) => input.glob, + Err(err) => return Task::ready(Err(anyhow!(err))), + }; + let path_matcher = match PathMatcher::new(&[glob.clone()]) { + Ok(matcher) => matcher, + Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {}", err))), + }; + + struct Match { + display_path: String, + path: PathBuf, + } + + let mut matches = Vec::new(); + let mut deleted_paths = Vec::new(); + let mut errors = Vec::new(); + + for worktree_handle in project.read(cx).worktrees(cx) { + let worktree = worktree_handle.read(cx); + let worktree_root = worktree.abs_path().to_path_buf(); + + // Don't consider ignored entries. + for entry in worktree.entries(false, 0) { + if path_matcher.is_match(&entry.path) { + matches.push(Match { + path: worktree_root.join(&entry.path), + display_path: entry.path.display().to_string(), + }); + } + } + } + + if matches.is_empty() { + return Task::ready(Ok(format!("No paths in the project matched {glob:?}"))); + } + + let paths_matched = matches.len(); + + // Delete the files + for Match { path, display_path } in matches { + match fs::remove_file(&path) { + Ok(()) => { + deleted_paths.push(display_path); + } + Err(file_err) => { + // Try to remove directory if it's not a file. Retrying as a directory + // on error saves a syscall compared to checking whether it's + // a directory up front for every single file. + if let Err(dir_err) = fs::remove_dir_all(&path) { + let error = if path.is_dir() { + format!("Failed to delete directory {}: {dir_err}", display_path) + } else { + format!("Failed to delete file {}: {file_err}", display_path) + }; + + errors.push(error); + } else { + deleted_paths.push(display_path); + } + } + } + } + + if errors.is_empty() { + // 0 deleted paths should never happen if there were no errors; + // we already returned if matches was empty. + let answer = if deleted_paths.len() == 1 { + format!( + "Deleted {}", + deleted_paths.first().unwrap_or(&String::new()) + ) + } else { + // Sort to group entries in the same directory together + deleted_paths.sort(); + + let mut buf = format!("Deleted these {} paths:\n", deleted_paths.len()); + + for path in deleted_paths.iter() { + buf.push('\n'); + buf.push_str(path); + } + + buf + }; + + Task::ready(Ok(answer)) + } else { + if deleted_paths.is_empty() { + Task::ready(Err(anyhow!( + "{glob:?} matched {} deleted because of {}:\n{}", + if paths_matched == 1 { + "1 path, but it was not".to_string() + } else { + format!("{} paths, but none were", paths_matched) + }, + if errors.len() == 1 { + "this error".to_string() + } else { + format!("{} errors", errors.len()) + }, + errors.join("\n") + ))) + } else { + // Sort to group entries in the same directory together + deleted_paths.sort(); + Task::ready(Ok(format!( + "Deleted {} paths matching glob {glob:?}:\n{}\n\nErrors:\n{}", + deleted_paths.len(), + deleted_paths.join("\n"), + errors.join("\n") + ))) + } + } + } +} diff --git a/crates/assistant_tools/src/delete_path_tool/description.md b/crates/assistant_tools/src/delete_path_tool/description.md new file mode 100644 index 0000000000000000000000000000000000000000..52b4444a2988546279838d9cd6f1268f18996c6f --- /dev/null +++ b/crates/assistant_tools/src/delete_path_tool/description.md @@ -0,0 +1 @@ +Deletes all files and directories in the project which match the given glob, and returns a list of the paths that were deleted. \ No newline at end of file