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