
This is just a refactor which adds no functionality. We now return a `ToolResult` from `Tool > run(...)`. For now this just wraps the output task in a struct. We'll use this to implement custom rendering of tools, see #28621. Release Notes: - N/A
203 lines
7.5 KiB
Rust
203 lines
7.5 KiB
Rust
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)
|
|
}
|