rename_tool.rs

  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}