203 lines · 7.5 KB · Rust
RawDownload
use anyhow::{Context as _, Result, anyhow};use assistant_tool::{ActionLog, Tool, ToolResult};use gpui::{App, Entity, Task};use language::{self, Buffer, ToPointUtf16};use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};use project::Project;use schemars::JsonSchema;use serde::{Deserialize, Serialize};use std::sync::Arc;use ui::IconName; use crate::schema::json_schema_for; #[derive(Debug, Serialize, Deserialize, JsonSchema)]pub struct RenameToolInput {    /// The relative path to the file containing the symbol to rename.    ///    /// WARNING: you MUST start this path with one of the project's root directories.    pub path: String,     /// The new name to give to the symbol.    pub new_name: String,     /// The text that comes immediately before the symbol in the file.    pub context_before_symbol: String,     /// The symbol to rename. This text must appear in the file right between    /// `context_before_symbol` and `context_after_symbol`.    ///    /// The file must contain exactly one occurrence of `context_before_symbol` followed by    /// `symbol` followed by `context_after_symbol`. If the file contains zero occurrences,    /// or if it contains more than one occurrence, the tool will fail, so it is absolutely    /// critical that you verify ahead of time that the string is unique. You can search    /// the file's contents to verify this ahead of time.    ///    /// To make the string more likely to be unique, include a minimum of 1 line of context    /// before the symbol, as well as a minimum of 1 line of context after the symbol.    /// If these lines of context are not enough to obtain a string that appears only once    /// in the file, then double the number of context lines until the string becomes unique.    /// (Start with 1 line before and 1 line after though, because too much context is    /// needlessly costly.)    ///    /// Do not alter the context lines of code in any way, and make sure to preserve all    /// whitespace and indentation for all lines of code. The combined string must be exactly    /// as it appears in the file, or else this tool call will fail.    pub symbol: String,     /// The text that comes immediately after the symbol in the file.    pub context_after_symbol: String,} pub struct RenameTool; impl Tool for RenameTool {    fn name(&self) -> String {        "rename".into()    }     fn needs_confirmation(&self, _input: &serde_json::Value, _cx: &App) -> bool {        false    }     fn description(&self) -> String {        include_str!("./rename_tool/description.md").into()    }     fn icon(&self) -> IconName {        IconName::Pencil    }     fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {        json_schema_for::<RenameToolInput>(format)    }     fn ui_text(&self, input: &serde_json::Value) -> String {        match serde_json::from_value::<RenameToolInput>(input.clone()) {            Ok(input) => {                format!("Rename '{}' to '{}'", input.symbol, input.new_name)            }            Err(_) => "Rename symbol".to_string(),        }    }     fn run(        self: Arc<Self>,        input: serde_json::Value,        _messages: &[LanguageModelRequestMessage],        project: Entity<Project>,        action_log: Entity<ActionLog>,        cx: &mut App,    ) -> ToolResult {        let input = match serde_json::from_value::<RenameToolInput>(input) {            Ok(input) => input,            Err(err) => return Task::ready(Err(anyhow!(err))).into(),        };         cx.spawn(async move |cx| {            let buffer = {                let project_path = project.read_with(cx, |project, cx| {                    project                        .find_project_path(&input.path, cx)                        .context("Path not found in project")                })??;                 project.update(cx, |project, cx| project.open_buffer(project_path, cx))?.await?            };             action_log.update(cx, |action_log, cx| {                action_log.buffer_read(buffer.clone(), cx);            })?;             let position = {                let Some(position) = buffer.read_with(cx, |buffer, _cx| {                    find_symbol_position(&buffer, &input.context_before_symbol, &input.symbol, &input.context_after_symbol)                })? else {                    return Err(anyhow!(                        "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."                    ));                };                 buffer.read_with(cx, |buffer, _| {                    position.to_point_utf16(&buffer.snapshot())                })?            };             project                .update(cx, |project, cx| {                    project.perform_rename(buffer.clone(), position, input.new_name.clone(), cx)                })?                .await?;             project                .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?                .await?;             action_log.update(cx, |log, cx| {                log.buffer_edited(buffer.clone(), cx)            })?;             Ok(format!("Renamed '{}' to '{}'", input.symbol, input.new_name))        }).into()    }} /// Finds the position of the symbol in the buffer, if it appears between context_before_symbol/// and context_after_symbol, and if that combined string has one unique result in the buffer.////// If an exact match fails, it tries adding a newline to the end of context_before_symbol and/// to the beginning of context_after_symbol to accommodate line-based context matching.fn find_symbol_position(    buffer: &Buffer,    context_before_symbol: &str,    symbol: &str,    context_after_symbol: &str,) -> Option<language::Anchor> {    let snapshot = buffer.snapshot();    let text = snapshot.text();     // First try with exact match    let search_string = format!("{context_before_symbol}{symbol}{context_after_symbol}");    let mut positions = text.match_indices(&search_string);    let position_result = positions.next();     if let Some(position) = position_result {        // Check if the matched string is unique        if positions.next().is_none() {            let symbol_start = position.0 + context_before_symbol.len();            let symbol_start_anchor =                snapshot.anchor_before(snapshot.offset_to_point(symbol_start));             return Some(symbol_start_anchor);        }    }     // If exact match fails or is not unique, try with line-based context    // Add a newline to the end of before context and beginning of after context    let line_based_before = if context_before_symbol.ends_with('\n') {        context_before_symbol.to_string()    } else {        format!("{context_before_symbol}\n")    };     let line_based_after = if context_after_symbol.starts_with('\n') {        context_after_symbol.to_string()    } else {        format!("\n{context_after_symbol}")    };     let line_search_string = format!("{line_based_before}{symbol}{line_based_after}");    let mut line_positions = text.match_indices(&line_search_string);    let line_position = line_positions.next()?;     // The line-based search string must also appear exactly once    if line_positions.next().is_some() {        return None;    }     let line_symbol_start = line_position.0 + line_based_before.len();    let line_symbol_start_anchor =        snapshot.anchor_before(snapshot.offset_to_point(line_symbol_start));     Some(line_symbol_start_anchor)}

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t