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::sync::Arc;
9
10#[derive(Debug, Serialize, Deserialize, JsonSchema)]
11pub struct CreateFileToolInput {
12 /// The path where the file should be created.
13 ///
14 /// <example>
15 /// If the project has the following structure:
16 ///
17 /// - directory1/
18 /// - directory2/
19 ///
20 /// You can create a new file by providing a path of "directory1/new_file.txt"
21 /// </example>
22 pub path: String,
23
24 /// The text contents of the file to create.
25 ///
26 /// <example>
27 /// To create a file with the text "Hello, World!", provide contents of "Hello, World!"
28 /// </example>
29 pub contents: String,
30}
31
32pub struct CreateFileTool;
33
34impl Tool for CreateFileTool {
35 fn name(&self) -> String {
36 "create-file".into()
37 }
38
39 fn needs_confirmation(&self) -> bool {
40 true
41 }
42
43 fn description(&self) -> String {
44 include_str!("./create_file_tool/description.md").into()
45 }
46
47 fn input_schema(&self) -> serde_json::Value {
48 let schema = schemars::schema_for!(CreateFileToolInput);
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::<CreateFileToolInput>(input.clone()) {
54 Ok(input) => {
55 let path = input.path.as_str();
56 format!("Create file `{path}`")
57 }
58 Err(_) => "Create file".to_string(),
59 }
60 }
61
62 fn run(
63 self: Arc<Self>,
64 input: serde_json::Value,
65 _messages: &[LanguageModelRequestMessage],
66 project: Entity<Project>,
67 _action_log: Entity<ActionLog>,
68 cx: &mut App,
69 ) -> Task<Result<String>> {
70 let input = match serde_json::from_value::<CreateFileToolInput>(input) {
71 Ok(input) => input,
72 Err(err) => return Task::ready(Err(anyhow!(err))),
73 };
74 let project_path = match project.read(cx).find_project_path(&input.path, cx) {
75 Some(project_path) => project_path,
76 None => return Task::ready(Err(anyhow!("Path to create was outside the project"))),
77 };
78 let contents: Arc<str> = input.contents.as_str().into();
79 let destination_path: Arc<str> = input.path.as_str().into();
80
81 cx.spawn(async move |cx| {
82 project
83 .update(cx, |project, cx| {
84 project.create_entry(project_path.clone(), false, cx)
85 })?
86 .await
87 .map_err(|err| anyhow!("Unable to create {destination_path}: {err}"))?;
88 let buffer = project
89 .update(cx, |project, cx| {
90 project.open_buffer(project_path.clone(), cx)
91 })?
92 .await
93 .map_err(|err| anyhow!("Unable to open buffer for {destination_path}: {err}"))?;
94 buffer.update(cx, |buffer, cx| {
95 buffer.set_text(contents, cx);
96 })?;
97
98 project
99 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
100 .await
101 .map_err(|err| anyhow!("Unable to save buffer for {destination_path}: {err}"))?;
102
103 Ok(format!("Created file {destination_path}"))
104 })
105 }
106}