1use anyhow::{Context as _, Result, anyhow};
2use assistant_tool::{ActionLog, Tool};
3use gpui::{App, Entity, Task};
4use language::{self, Buffer, ToPointUtf16};
5use language_model::LanguageModelRequestMessage;
6use project::Project;
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9use std::sync::Arc;
10use ui::IconName;
11
12#[derive(Debug, Serialize, Deserialize, JsonSchema)]
13pub struct RenameToolInput {
14 /// The relative path to the file containing the symbol to rename.
15 ///
16 /// WARNING: you MUST start this path with one of the project's root directories.
17 pub path: String,
18
19 /// The new name to give to the symbol.
20 pub new_name: String,
21
22 /// The text that comes immediately before the symbol in the file.
23 pub context_before_symbol: String,
24
25 /// The symbol to rename. This text must appear in the file right between
26 /// `context_before_symbol` and `context_after_symbol`.
27 ///
28 /// The file must contain exactly one occurrence of `context_before_symbol` followed by
29 /// `symbol` followed by `context_after_symbol`. If the file contains zero occurrences,
30 /// or if it contains more than one occurrence, the tool will fail, so it is absolutely
31 /// critical that you verify ahead of time that the string is unique. You can search
32 /// the file's contents to verify this ahead of time.
33 ///
34 /// To make the string more likely to be unique, include a minimum of 1 line of context
35 /// before the symbol, as well as a minimum of 1 line of context after the symbol.
36 /// If these lines of context are not enough to obtain a string that appears only once
37 /// in the file, then double the number of context lines until the string becomes unique.
38 /// (Start with 1 line before and 1 line after though, because too much context is
39 /// needlessly costly.)
40 ///
41 /// Do not alter the context lines of code in any way, and make sure to preserve all
42 /// whitespace and indentation for all lines of code. The combined string must be exactly
43 /// as it appears in the file, or else this tool call will fail.
44 pub symbol: String,
45
46 /// The text that comes immediately after the symbol in the file.
47 pub context_after_symbol: String,
48}
49
50pub struct RenameTool;
51
52impl Tool for RenameTool {
53 fn name(&self) -> String {
54 "rename".into()
55 }
56
57 fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {
58 false
59 }
60
61 fn description(&self) -> String {
62 include_str!("./rename_tool/description.md").into()
63 }
64
65 fn icon(&self) -> IconName {
66 IconName::Pencil
67 }
68
69 fn input_schema(
70 &self,
71 _format: language_model::LanguageModelToolSchemaFormat,
72 ) -> serde_json::Value {
73 let schema = schemars::schema_for!(RenameToolInput);
74 serde_json::to_value(&schema).unwrap()
75 }
76
77 fn ui_text(&self, input: &serde_json::Value) -> String {
78 match serde_json::from_value::<RenameToolInput>(input.clone()) {
79 Ok(input) => {
80 format!("Rename '{}' to '{}'", input.symbol, input.new_name)
81 }
82 Err(_) => "Rename symbol".to_string(),
83 }
84 }
85
86 fn run(
87 self: Arc<Self>,
88 input: serde_json::Value,
89 _messages: &[LanguageModelRequestMessage],
90 project: Entity<Project>,
91 action_log: Entity<ActionLog>,
92 cx: &mut App,
93 ) -> Task<Result<String>> {
94 let input = match serde_json::from_value::<RenameToolInput>(input) {
95 Ok(input) => input,
96 Err(err) => return Task::ready(Err(anyhow!(err))),
97 };
98
99 cx.spawn(async move |cx| {
100 let buffer = {
101 let project_path = project.read_with(cx, |project, cx| {
102 project
103 .find_project_path(&input.path, cx)
104 .context("Path not found in project")
105 })??;
106
107 project.update(cx, |project, cx| project.open_buffer(project_path, cx))?.await?
108 };
109
110 action_log.update(cx, |action_log, cx| {
111 action_log.buffer_read(buffer.clone(), cx);
112 })?;
113
114 let position = {
115 let Some(position) = buffer.read_with(cx, |buffer, _cx| {
116 find_symbol_position(&buffer, &input.context_before_symbol, &input.symbol, &input.context_after_symbol)
117 })? else {
118 return Err(anyhow!(
119 "Failed to locate the symbol specified by context_before_symbol, symbol, and context_after_symbol. Make sure context_before_symbol and context_after_symbol each match exactly once in the file."
120 ));
121 };
122
123 buffer.read_with(cx, |buffer, _| {
124 position.to_point_utf16(&buffer.snapshot())
125 })?
126 };
127
128 project
129 .update(cx, |project, cx| {
130 project.perform_rename(buffer.clone(), position, input.new_name.clone(), cx)
131 })?
132 .await?;
133
134 project
135 .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
136 .await?;
137
138 action_log.update(cx, |log, cx| {
139 log.buffer_edited(buffer.clone(), cx)
140 })?;
141
142 Ok(format!("Renamed '{}' to '{}'", input.symbol, input.new_name))
143 })
144 }
145}
146
147/// Finds the position of the symbol in the buffer, if it appears between context_before_symbol
148/// and context_after_symbol, and if that combined string has one unique result in the buffer.
149///
150/// If an exact match fails, it tries adding a newline to the end of context_before_symbol and
151/// to the beginning of context_after_symbol to accommodate line-based context matching.
152fn find_symbol_position(
153 buffer: &Buffer,
154 context_before_symbol: &str,
155 symbol: &str,
156 context_after_symbol: &str,
157) -> Option<language::Anchor> {
158 let snapshot = buffer.snapshot();
159 let text = snapshot.text();
160
161 // First try with exact match
162 let search_string = format!("{context_before_symbol}{symbol}{context_after_symbol}");
163 let mut positions = text.match_indices(&search_string);
164 let position_result = positions.next();
165
166 if let Some(position) = position_result {
167 // Check if the matched string is unique
168 if positions.next().is_none() {
169 let symbol_start = position.0 + context_before_symbol.len();
170 let symbol_start_anchor =
171 snapshot.anchor_before(snapshot.offset_to_point(symbol_start));
172
173 return Some(symbol_start_anchor);
174 }
175 }
176
177 // If exact match fails or is not unique, try with line-based context
178 // Add a newline to the end of before context and beginning of after context
179 let line_based_before = if context_before_symbol.ends_with('\n') {
180 context_before_symbol.to_string()
181 } else {
182 format!("{context_before_symbol}\n")
183 };
184
185 let line_based_after = if context_after_symbol.starts_with('\n') {
186 context_after_symbol.to_string()
187 } else {
188 format!("\n{context_after_symbol}")
189 };
190
191 let line_search_string = format!("{line_based_before}{symbol}{line_based_after}");
192 let mut line_positions = text.match_indices(&line_search_string);
193 let line_position = line_positions.next()?;
194
195 // The line-based search string must also appear exactly once
196 if line_positions.next().is_some() {
197 return None;
198 }
199
200 let line_symbol_start = line_position.0 + line_based_before.len();
201 let line_symbol_start_anchor =
202 snapshot.anchor_before(snapshot.offset_to_point(line_symbol_start));
203
204 Some(line_symbol_start_anchor)
205}