1use anyhow::{anyhow, Result};
2use assistant_tool::{ActionLog, Tool};
3use gpui::{App, AppContext, Entity, Task};
4use language_model::LanguageModelRequestMessage;
5use project::Project;
6use schemars::JsonSchema;
7use serde::{Deserialize, Serialize};
8use std::{path::Path, sync::Arc};
9use ui::IconName;
10
11#[derive(Debug, Serialize, Deserialize, JsonSchema)]
12pub struct MovePathToolInput {
13 /// The source path of the file or directory to move/rename.
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 move the first file by providing a source_path of "directory1/a/something.txt"
23 /// </example>
24 pub source_path: String,
25
26 /// The destination path where the file or directory should be moved/renamed to.
27 /// If the paths are the same except for the filename, then this will be a rename.
28 ///
29 /// <example>
30 /// To move "directory1/a/something.txt" to "directory2/b/renamed.txt",
31 /// provide a destination_path of "directory2/b/renamed.txt"
32 /// </example>
33 pub destination_path: String,
34}
35
36pub struct MovePathTool;
37
38impl Tool for MovePathTool {
39 fn name(&self) -> String {
40 "move-path".into()
41 }
42
43 fn needs_confirmation(&self) -> bool {
44 true
45 }
46
47 fn description(&self) -> String {
48 include_str!("./move_path_tool/description.md").into()
49 }
50
51 fn icon(&self) -> IconName {
52 IconName::ArrowRightLeft
53 }
54
55 fn input_schema(&self) -> serde_json::Value {
56 let schema = schemars::schema_for!(MovePathToolInput);
57 serde_json::to_value(&schema).unwrap()
58 }
59
60 fn ui_text(&self, input: &serde_json::Value) -> String {
61 match serde_json::from_value::<MovePathToolInput>(input.clone()) {
62 Ok(input) => {
63 let src = input.source_path.as_str();
64 let dest = input.destination_path.as_str();
65 let src_path = Path::new(src);
66 let dest_path = Path::new(dest);
67
68 match dest_path
69 .file_name()
70 .and_then(|os_str| os_str.to_os_string().into_string().ok())
71 {
72 Some(filename) if src_path.parent() == dest_path.parent() => {
73 format!("Rename `{src}` to `{filename}`")
74 }
75 _ => {
76 format!("Move `{src}` to `{dest}`")
77 }
78 }
79 }
80 Err(_) => "Move path".to_string(),
81 }
82 }
83
84 fn run(
85 self: Arc<Self>,
86 input: serde_json::Value,
87 _messages: &[LanguageModelRequestMessage],
88 project: Entity<Project>,
89 _action_log: Entity<ActionLog>,
90 cx: &mut App,
91 ) -> Task<Result<String>> {
92 let input = match serde_json::from_value::<MovePathToolInput>(input) {
93 Ok(input) => input,
94 Err(err) => return Task::ready(Err(anyhow!(err))),
95 };
96 let rename_task = project.update(cx, |project, cx| {
97 match project
98 .find_project_path(&input.source_path, cx)
99 .and_then(|project_path| project.entry_for_path(&project_path, cx))
100 {
101 Some(entity) => match project.find_project_path(&input.destination_path, cx) {
102 Some(project_path) => project.rename_entry(entity.id, project_path.path, cx),
103 None => Task::ready(Err(anyhow!(
104 "Destination path {} was outside the project.",
105 input.destination_path
106 ))),
107 },
108 None => Task::ready(Err(anyhow!(
109 "Source path {} was not found in the project.",
110 input.source_path
111 ))),
112 }
113 });
114
115 cx.background_spawn(async move {
116 match rename_task.await {
117 Ok(_) => Ok(format!(
118 "Moved {} to {}",
119 input.source_path, input.destination_path
120 )),
121 Err(err) => Err(anyhow!(
122 "Failed to move {} to {}: {}",
123 input.source_path,
124 input.destination_path,
125 err
126 )),
127 }
128 })
129 }
130}