delete_path_tool.rs

  1use anyhow::{anyhow, Result};
  2use assistant_tool::{ActionLog, 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        _action_log: Entity<ActionLog>,
 49        cx: &mut App,
 50    ) -> Task<Result<String>> {
 51        let glob = match serde_json::from_value::<DeletePathToolInput>(input) {
 52            Ok(input) => input.glob,
 53            Err(err) => return Task::ready(Err(anyhow!(err))),
 54        };
 55        let path_matcher = match PathMatcher::new(&[glob.clone()]) {
 56            Ok(matcher) => matcher,
 57            Err(err) => return Task::ready(Err(anyhow!("Invalid glob: {}", err))),
 58        };
 59
 60        struct Match {
 61            display_path: String,
 62            path: PathBuf,
 63        }
 64
 65        let mut matches = Vec::new();
 66        let mut deleted_paths = Vec::new();
 67        let mut errors = Vec::new();
 68
 69        for worktree_handle in project.read(cx).worktrees(cx) {
 70            let worktree = worktree_handle.read(cx);
 71            let worktree_root = worktree.abs_path().to_path_buf();
 72
 73            // Don't consider ignored entries.
 74            for entry in worktree.entries(false, 0) {
 75                if path_matcher.is_match(&entry.path) {
 76                    matches.push(Match {
 77                        path: worktree_root.join(&entry.path),
 78                        display_path: entry.path.display().to_string(),
 79                    });
 80                }
 81            }
 82        }
 83
 84        if matches.is_empty() {
 85            return Task::ready(Ok(format!("No paths in the project matched {glob:?}")));
 86        }
 87
 88        let paths_matched = matches.len();
 89
 90        // Delete the files
 91        for Match { path, display_path } in matches {
 92            match fs::remove_file(&path) {
 93                Ok(()) => {
 94                    deleted_paths.push(display_path);
 95                }
 96                Err(file_err) => {
 97                    // Try to remove directory if it's not a file. Retrying as a directory
 98                    // on error saves a syscall compared to checking whether it's
 99                    // a directory up front for every single file.
100                    if let Err(dir_err) = fs::remove_dir_all(&path) {
101                        let error = if path.is_dir() {
102                            format!("Failed to delete directory {}: {dir_err}", display_path)
103                        } else {
104                            format!("Failed to delete file {}: {file_err}", display_path)
105                        };
106
107                        errors.push(error);
108                    } else {
109                        deleted_paths.push(display_path);
110                    }
111                }
112            }
113        }
114
115        if errors.is_empty() {
116            // 0 deleted paths should never happen if there were no errors;
117            // we already returned if matches was empty.
118            let answer = if deleted_paths.len() == 1 {
119                format!(
120                    "Deleted {}",
121                    deleted_paths.first().unwrap_or(&String::new())
122                )
123            } else {
124                // Sort to group entries in the same directory together
125                deleted_paths.sort();
126
127                let mut buf = format!("Deleted these {} paths:\n", deleted_paths.len());
128
129                for path in deleted_paths.iter() {
130                    buf.push('\n');
131                    buf.push_str(path);
132                }
133
134                buf
135            };
136
137            Task::ready(Ok(answer))
138        } else {
139            if deleted_paths.is_empty() {
140                Task::ready(Err(anyhow!(
141                    "{glob:?} matched {} deleted because of {}:\n{}",
142                    if paths_matched == 1 {
143                        "1 path, but it was not".to_string()
144                    } else {
145                        format!("{} paths, but none were", paths_matched)
146                    },
147                    if errors.len() == 1 {
148                        "this error".to_string()
149                    } else {
150                        format!("{} errors", errors.len())
151                    },
152                    errors.join("\n")
153                )))
154            } else {
155                // Sort to group entries in the same directory together
156                deleted_paths.sort();
157                Task::ready(Ok(format!(
158                    "Deleted {} paths matching glob {glob:?}:\n{}\n\nErrors:\n{}",
159                    deleted_paths.len(),
160                    deleted_paths.join("\n"),
161                    errors.join("\n")
162                )))
163            }
164        }
165    }
166}