1use anyhow::{anyhow, Result};
2use assistant_tool::{ActionLog, Tool};
3use futures::{channel::mpsc, SinkExt, StreamExt};
4use gpui::{App, AppContext, Entity, Task};
5use language_model::LanguageModelRequestMessage;
6use project::{Project, ProjectPath};
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10use ui::IconName;
11
12#[derive(Debug, Serialize, Deserialize, JsonSchema)]
13pub struct DeletePathToolInput {
14 /// The path of the file or directory to delete.
15 ///
16 /// <example>
17 /// If the project has the following files:
18 ///
19 /// - directory1/a/something.txt
20 /// - directory2/a/things.txt
21 /// - directory3/a/other.txt
22 ///
23 /// You can delete the first file by providing a path of "directory1/a/something.txt"
24 /// </example>
25 pub path: String,
26}
27
28pub struct DeletePathTool;
29
30impl Tool for DeletePathTool {
31 fn name(&self) -> String {
32 "delete-path".into()
33 }
34
35 fn needs_confirmation(&self) -> bool {
36 true
37 }
38
39 fn description(&self) -> String {
40 include_str!("./delete_path_tool/description.md").into()
41 }
42
43 fn icon(&self) -> IconName {
44 IconName::FileDelete
45 }
46
47 fn input_schema(&self) -> serde_json::Value {
48 let schema = schemars::schema_for!(DeletePathToolInput);
49 serde_json::to_value(&schema).unwrap()
50 }
51
52 fn ui_text(&self, input: &serde_json::Value) -> String {
53 match serde_json::from_value::<DeletePathToolInput>(input.clone()) {
54 Ok(input) => format!("Delete “`{}`”", input.path),
55 Err(_) => "Delete path".to_string(),
56 }
57 }
58
59 fn run(
60 self: Arc<Self>,
61 input: serde_json::Value,
62 _messages: &[LanguageModelRequestMessage],
63 project: Entity<Project>,
64 action_log: Entity<ActionLog>,
65 cx: &mut App,
66 ) -> Task<Result<String>> {
67 let path_str = match serde_json::from_value::<DeletePathToolInput>(input) {
68 Ok(input) => input.path,
69 Err(err) => return Task::ready(Err(anyhow!(err))),
70 };
71 let Some(project_path) = project.read(cx).find_project_path(&path_str, cx) else {
72 return Task::ready(Err(anyhow!(
73 "Couldn't delete {path_str} because that path isn't in this project."
74 )));
75 };
76
77 let Some(worktree) = project
78 .read(cx)
79 .worktree_for_id(project_path.worktree_id, cx)
80 else {
81 return Task::ready(Err(anyhow!(
82 "Couldn't delete {path_str} because that path isn't in this project."
83 )));
84 };
85
86 let worktree_snapshot = worktree.read(cx).snapshot();
87 let (mut paths_tx, mut paths_rx) = mpsc::channel(256);
88 cx.background_spawn({
89 let project_path = project_path.clone();
90 async move {
91 for entry in
92 worktree_snapshot.traverse_from_path(true, false, false, &project_path.path)
93 {
94 if !entry.path.starts_with(&project_path.path) {
95 break;
96 }
97 paths_tx
98 .send(ProjectPath {
99 worktree_id: project_path.worktree_id,
100 path: entry.path.clone(),
101 })
102 .await?;
103 }
104 anyhow::Ok(())
105 }
106 })
107 .detach();
108
109 cx.spawn(async move |cx| {
110 while let Some(path) = paths_rx.next().await {
111 if let Ok(buffer) = project
112 .update(cx, |project, cx| project.open_buffer(path, cx))?
113 .await
114 {
115 action_log.update(cx, |action_log, cx| {
116 action_log.will_delete_buffer(buffer.clone(), cx)
117 })?;
118 }
119 }
120
121 let delete = project.update(cx, |project, cx| {
122 project.delete_file(project_path, false, cx)
123 })?;
124
125 match delete {
126 Some(deletion_task) => match deletion_task.await {
127 Ok(()) => Ok(format!("Deleted {path_str}")),
128 Err(err) => Err(anyhow!("Failed to delete {path_str}: {err}")),
129 },
130 None => Err(anyhow!(
131 "Couldn't delete {path_str} because that path isn't in this project."
132 )),
133 }
134 })
135 }
136}