delete_path_tool.rs

  1use anyhow::{anyhow, Result};
  2use assistant_tool::Tool;
  3use gpui::{App, Entity, Task};
  4use language_model::LanguageModelRequestMessage;
  5use project::Project;
  6use schemars::JsonSchema;
  7use serde::{Deserialize, Serialize};
  8use std::{fs, path::PathBuf, sync::Arc};
  9use util::paths::PathMatcher;
 10
 11#[derive(Debug, Serialize, Deserialize, JsonSchema)]
 12pub struct DeletePathToolInput {
 13    /// The glob to match files in the project to delete.
 14    ///
 15    /// <example>
 16    /// If the project has the following files:
 17    ///
 18    /// - directory1/a/something.txt
 19    /// - directory2/a/things.txt
 20    /// - directory3/a/other.txt
 21    ///
 22    /// You can delete the first two files by providing a glob of "*thing*.txt"
 23    /// </example>
 24    pub glob: String,
 25}
 26
 27pub struct DeletePathTool;
 28
 29impl Tool for DeletePathTool {
 30    fn name(&self) -> String {
 31        "delete-path".into()
 32    }
 33
 34    fn description(&self) -> String {
 35        include_str!("./delete_path_tool/description.md").into()
 36    }
 37
 38    fn input_schema(&self) -> serde_json::Value {
 39        let schema = schemars::schema_for!(DeletePathToolInput);
 40        serde_json::to_value(&schema).unwrap()
 41    }
 42
 43    fn run(
 44        self: Arc<Self>,
 45        input: serde_json::Value,
 46        _messages: &[LanguageModelRequestMessage],
 47        project: Entity<Project>,
 48        cx: &mut App,
 49    ) -> Task<Result<String>> {
 50        let glob = match serde_json::from_value::<DeletePathToolInput>(input) {
 51            Ok(input) => input.glob,
 52            Err(err) => return Task::ready(Err(anyhow!(err))),
 53        };
 54        let path_matcher = match PathMatcher::new(&[glob.clone()]) {
 55            Ok(matcher) => matcher,
 56            Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {}", err))),
 57        };
 58
 59        struct Match {
 60            display_path: String,
 61            path: PathBuf,
 62        }
 63
 64        let mut matches = Vec::new();
 65        let mut deleted_paths = Vec::new();
 66        let mut errors = Vec::new();
 67
 68        for worktree_handle in project.read(cx).worktrees(cx) {
 69            let worktree = worktree_handle.read(cx);
 70            let worktree_root = worktree.abs_path().to_path_buf();
 71
 72            // Don't consider ignored entries.
 73            for entry in worktree.entries(false, 0) {
 74                if path_matcher.is_match(&entry.path) {
 75                    matches.push(Match {
 76                        path: worktree_root.join(&entry.path),
 77                        display_path: entry.path.display().to_string(),
 78                    });
 79                }
 80            }
 81        }
 82
 83        if matches.is_empty() {
 84            return Task::ready(Ok(format!("No paths in the project matched {glob:?}")));
 85        }
 86
 87        let paths_matched = matches.len();
 88
 89        // Delete the files
 90        for Match { path, display_path } in matches {
 91            match fs::remove_file(&path) {
 92                Ok(()) => {
 93                    deleted_paths.push(display_path);
 94                }
 95                Err(file_err) => {
 96                    // Try to remove directory if it's not a file. Retrying as a directory
 97                    // on error saves a syscall compared to checking whether it's
 98                    // a directory up front for every single file.
 99                    if let Err(dir_err) = fs::remove_dir_all(&path) {
100                        let error = if path.is_dir() {
101                            format!("Failed to delete directory {}: {dir_err}", display_path)
102                        } else {
103                            format!("Failed to delete file {}: {file_err}", display_path)
104                        };
105
106                        errors.push(error);
107                    } else {
108                        deleted_paths.push(display_path);
109                    }
110                }
111            }
112        }
113
114        if errors.is_empty() {
115            // 0 deleted paths should never happen if there were no errors;
116            // we already returned if matches was empty.
117            let answer = if deleted_paths.len() == 1 {
118                format!(
119                    "Deleted {}",
120                    deleted_paths.first().unwrap_or(&String::new())
121                )
122            } else {
123                // Sort to group entries in the same directory together
124                deleted_paths.sort();
125
126                let mut buf = format!("Deleted these {} paths:\n", deleted_paths.len());
127
128                for path in deleted_paths.iter() {
129                    buf.push('\n');
130                    buf.push_str(path);
131                }
132
133                buf
134            };
135
136            Task::ready(Ok(answer))
137        } else {
138            if deleted_paths.is_empty() {
139                Task::ready(Err(anyhow!(
140                    "{glob:?} matched {} deleted because of {}:\n{}",
141                    if paths_matched == 1 {
142                        "1 path, but it was not".to_string()
143                    } else {
144                        format!("{} paths, but none were", paths_matched)
145                    },
146                    if errors.len() == 1 {
147                        "this error".to_string()
148                    } else {
149                        format!("{} errors", errors.len())
150                    },
151                    errors.join("\n")
152                )))
153            } else {
154                // Sort to group entries in the same directory together
155                deleted_paths.sort();
156                Task::ready(Ok(format!(
157                    "Deleted {} paths matching glob {glob:?}:\n{}\n\nErrors:\n{}",
158                    deleted_paths.len(),
159                    deleted_paths.join("\n"),
160                    errors.join("\n")
161                )))
162            }
163        }
164    }
165}