Try replacing find-replace tool with code action tool
This commit is contained in:
@@ -10,7 +10,6 @@ mod delete_path_tool;
|
||||
mod diagnostics_tool;
|
||||
mod edit_files_tool;
|
||||
mod fetch_tool;
|
||||
mod find_replace_file_tool;
|
||||
mod list_directory_tool;
|
||||
mod move_path_tool;
|
||||
mod now_tool;
|
||||
@@ -41,7 +40,6 @@ use crate::delete_path_tool::DeletePathTool;
|
||||
use crate::diagnostics_tool::DiagnosticsTool;
|
||||
use crate::edit_files_tool::EditFilesTool;
|
||||
use crate::fetch_tool::FetchTool;
|
||||
use crate::find_replace_file_tool::FindReplaceFileTool;
|
||||
use crate::list_directory_tool::ListDirectoryTool;
|
||||
use crate::now_tool::NowTool;
|
||||
use crate::open_tool::OpenTool;
|
||||
@@ -62,7 +60,6 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
registry.register_tool(CreateFileTool);
|
||||
registry.register_tool(CopyPathTool);
|
||||
registry.register_tool(DeletePathTool);
|
||||
registry.register_tool(FindReplaceFileTool);
|
||||
registry.register_tool(SymbolInfoTool);
|
||||
registry.register_tool(CodeActionTool);
|
||||
registry.register_tool(MovePathTool);
|
||||
|
||||
@@ -10,6 +10,8 @@ use serde::{Deserialize, Serialize};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
use ui::IconName;
|
||||
|
||||
use crate::replace::{replace_exact, replace_with_flexible_indent};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CodeActionToolInput {
|
||||
/// The relative path to the file containing the text range.
|
||||
@@ -38,13 +40,23 @@ pub struct CodeActionToolInput {
|
||||
/// - "source.generateAccessors" - creates getter/setter methods
|
||||
/// - "source.convertToAsyncFunction" - converts callback-style code to async/await
|
||||
///
|
||||
/// Also, there is a special case: if you specify exactly "textDocument/rename" as the action,
|
||||
/// then this will rename the symbol to whatever string you specified for the `arguments` field.
|
||||
/// Special cases:
|
||||
/// - If you specify exactly "textDocument/rename" as the action,
|
||||
/// then this will rename the symbol to whatever string you specified for the `arguments` field.
|
||||
/// When a rename is desired, you should always choose this over the "textDocument/findReplace" action.
|
||||
/// - If you specify exactly "textDocument/findReplace" as the action,
|
||||
/// then this will find and replace text within the file. For this action, you must provide
|
||||
/// the `text_range` as the text to find, and `arguments` should be a string containing
|
||||
/// the text to replace it with. The text must appear exactly once in the file when combined
|
||||
/// with the before and after context. IMPORTANT: you must never, EVER use this for a rename
|
||||
/// operation unless you have already tried the action "textDocument/rename" and have seen it fail too
|
||||
/// many times.
|
||||
pub action: Option<String>,
|
||||
|
||||
/// Optional arguments to pass to the code action.
|
||||
///
|
||||
/// For rename operations (when action="textDocument/rename"), this should contain the new name.
|
||||
/// For findReplace operations (when action="textDocument/findReplace"), this should contain the replacement text.
|
||||
/// For other code actions, these arguments may be passed to the language server.
|
||||
pub arguments: Option<serde_json::Value>,
|
||||
|
||||
@@ -70,6 +82,8 @@ pub struct CodeActionToolInput {
|
||||
/// 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.
|
||||
///
|
||||
/// For findReplace operations, this represents the text to find and replace.
|
||||
pub text_range: String,
|
||||
|
||||
/// The text that comes immediately after the text range in the file.
|
||||
@@ -122,6 +136,21 @@ impl Tool for CodeActionTool {
|
||||
None => "missing name".to_string(),
|
||||
};
|
||||
format!("Rename '{}' to '{}'", input.text_range, new_name)
|
||||
} else if action == "textDocument/findReplace" {
|
||||
let replacement = match &input.arguments {
|
||||
Some(serde_json::Value::String(replacement)) => replacement.clone(),
|
||||
Some(value) => {
|
||||
if let Ok(replacement) =
|
||||
serde_json::from_value::<String>(value.clone())
|
||||
{
|
||||
replacement
|
||||
} else {
|
||||
"invalid replacement".to_string()
|
||||
}
|
||||
}
|
||||
None => "missing replacement".to_string(),
|
||||
};
|
||||
format!("Replace '{}' with '{}'", input.text_range, replacement)
|
||||
} else {
|
||||
format!(
|
||||
"Execute code action '{}' for '{}'",
|
||||
@@ -212,6 +241,77 @@ impl Tool for CodeActionTool {
|
||||
})?;
|
||||
|
||||
Ok(format!("Renamed '{}' to '{}'", input.text_range, new_name))
|
||||
} else if action_type == "textDocument/findReplace" {
|
||||
// Handle find and replace operation
|
||||
let replacement = match &input.arguments {
|
||||
Some(serde_json::Value::String(replacement)) => replacement.clone(),
|
||||
Some(value) => {
|
||||
if let Ok(replacement) = serde_json::from_value::<String>(value.clone()) {
|
||||
replacement
|
||||
} else {
|
||||
return Err(anyhow!("For findReplace operations, 'arguments' must be a string containing the replacement text"));
|
||||
}
|
||||
},
|
||||
None => return Err(anyhow!("For findReplace operations, 'arguments' must contain the replacement text")),
|
||||
};
|
||||
|
||||
if input.text_range.is_empty() {
|
||||
return Err(anyhow!("`text_range` string cannot be empty. Use a different tool if you want to create a file."));
|
||||
}
|
||||
|
||||
if input.text_range == replacement {
|
||||
return Err(anyhow!("The text to find and the replacement are identical, so no changes would be made."));
|
||||
}
|
||||
|
||||
// Construct the find string by combining context and text_range
|
||||
let find_string = format!("{}{}{}", input.context_before_range, input.text_range, input.context_after_range);
|
||||
let replace_string = format!("{}{}{}", input.context_before_range, replacement, input.context_after_range);
|
||||
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
// Try exact match first
|
||||
let result_exact = replace_exact(&find_string, &replace_string, &snapshot).await;
|
||||
|
||||
// If exact match fails, try flexible indent
|
||||
let result = result_exact.or_else(|| replace_with_flexible_indent(&find_string, &replace_string, &snapshot));
|
||||
|
||||
if let Some(diff) = result {
|
||||
let edit_ids = buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.apply_diff(diff, false, cx);
|
||||
let transaction = buffer.finalize_last_transaction();
|
||||
transaction.map_or(Vec::new(), |transaction| transaction.edit_ids.clone())
|
||||
})?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_edited(buffer.clone(), edit_ids, cx)
|
||||
})?;
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
project.save_buffer(buffer, cx)
|
||||
})?.await?;
|
||||
|
||||
Ok(format!("Replaced '{}' with '{}'", input.text_range, replacement))
|
||||
} else {
|
||||
let err = buffer.read_with(cx, |buffer, _cx| {
|
||||
let file_exists = buffer
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state().exists());
|
||||
|
||||
if !file_exists {
|
||||
anyhow!("{} does not exist", input.path)
|
||||
} else if buffer.is_empty() {
|
||||
anyhow!(
|
||||
"{} is empty, so the provided text wasn't found.",
|
||||
input.path
|
||||
)
|
||||
} else {
|
||||
anyhow!("Failed to match the provided text with its context")
|
||||
}
|
||||
})?;
|
||||
|
||||
Err(err)
|
||||
}
|
||||
} else {
|
||||
// Handle execute specific code action
|
||||
// Get code actions for the range
|
||||
|
||||
@@ -4,16 +4,21 @@ This tool can:
|
||||
- List all available code actions for a selected text range
|
||||
- Execute a specific code action on that range
|
||||
- Rename symbols across your codebase. This tool is the preferred way to rename things, and you should always prefer to rename code symbols using this tool rather than using textual find/replace when both are available.
|
||||
- Find and replace specific text within a file with precise context matching. This is the preferred way to make text edits when no other code action is appropriate.
|
||||
|
||||
Use this tool when you want to:
|
||||
- Discover what code actions are available for a piece of code
|
||||
- Apply automatic fixes and code transformations
|
||||
- Rename variables, functions, or other symbols consistently throughout your project
|
||||
- Clean up imports, implement interfaces, or perform other language-specific operations
|
||||
- Make precise text edits within a file
|
||||
|
||||
- If unsure what actions are available, call the tool without specifying an action to get a list
|
||||
- For common operations, you can directly specify actions like "quickfix.all" or "source.organizeImports"
|
||||
- For renaming, use the special "textDocument/rename" action and provide the new name in the arguments field
|
||||
- For finding and replacing text, use the special "textDocument/findReplace" action, with text_range as the text to find and arguments as the replacement text
|
||||
- Be specific with your text range and context to ensure the tool identifies the correct code location
|
||||
|
||||
The tool will automatically save any changes it makes to your files.
|
||||
|
||||
For find and replace operations, only use this tool when you want to edit a subset of a file's contents, but not the entire file. You should not use this when you want to replace the entire contents of a file with completely different contents. You also should not use this when you want to move or rename a file. You absolutely must NEVER use this to create new files from scratch.
|
||||
|
||||
@@ -1,248 +0,0 @@
|
||||
use crate::{replace::replace_with_flexible_indent, schema::json_schema_for};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use gpui::{App, AppContext, Entity, Task};
|
||||
use language_model::{LanguageModelRequestMessage, LanguageModelToolSchemaFormat};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::PathBuf, sync::Arc};
|
||||
use ui::IconName;
|
||||
|
||||
use crate::replace::replace_exact;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct FindReplaceFileToolInput {
|
||||
/// The path of the file to modify.
|
||||
///
|
||||
/// WARNING: When specifying which file path need changing, you MUST
|
||||
/// start each path with one of the project's root directories.
|
||||
///
|
||||
/// The following examples assume we have two root directories in the project:
|
||||
/// - backend
|
||||
/// - frontend
|
||||
///
|
||||
/// <example>
|
||||
/// `backend/src/main.rs`
|
||||
///
|
||||
/// Notice how the file path starts with root-1. Without that, the path
|
||||
/// would be ambiguous and the call would fail!
|
||||
/// </example>
|
||||
///
|
||||
/// <example>
|
||||
/// `frontend/db.js`
|
||||
/// </example>
|
||||
pub path: PathBuf,
|
||||
|
||||
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
|
||||
///
|
||||
/// <example>Fix API endpoint URLs</example>
|
||||
/// <example>Update copyright year in `page_footer`</example>
|
||||
pub display_description: String,
|
||||
|
||||
/// The unique string to find in the file. This string cannot be empty;
|
||||
/// if the string is empty, the tool call will fail. Remember, do not use this tool
|
||||
/// to create new files from scratch, or to overwrite existing files! Use a different
|
||||
/// approach if you want to do that.
|
||||
///
|
||||
/// If this string appears more than once in the file, this tool call will fail,
|
||||
/// so it is absolutely critical that you verify ahead of time that the string
|
||||
/// is unique. You can search within the file to verify this.
|
||||
///
|
||||
/// To make the string more likely to be unique, include a minimum of 3 lines of context
|
||||
/// before the string you actually want to find, as well as a minimum of 3 lines of
|
||||
/// context after the string you want to find. (These lines of context should appear
|
||||
/// in the `replace` string as well.) If 3 lines of context is 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 3 lines before and 3 lines 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. This string must be exactly as
|
||||
/// it appears in the file, because this tool will do a literal find/replace, and if
|
||||
/// even one character in this string is different in any way from how it appears
|
||||
/// in the file, then the tool call will fail.
|
||||
///
|
||||
/// If you get an error that the `find` string was not found, this means that either
|
||||
/// you made a mistake, or that the file has changed since you last looked at it.
|
||||
/// Either way, when this happens, you should retry doing this tool call until it
|
||||
/// succeeds, up to 3 times. Each time you retry, you should take another look at
|
||||
/// the exact text of the file in question, to make sure that you are searching for
|
||||
/// exactly the right string. Regardless of whether it was because you made a mistake
|
||||
/// or because the file changed since you last looked at it, you should be extra
|
||||
/// careful when retrying in this way. It's a bad expereience for the user if
|
||||
/// this `find` string isn't found, so be super careful to get it exactly right!
|
||||
///
|
||||
/// <example>
|
||||
/// If a file contains this code:
|
||||
///
|
||||
/// ```ignore
|
||||
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
|
||||
/// // Check if user exists first
|
||||
/// let user = database.find_user(user_id)?;
|
||||
///
|
||||
/// // This is the part we want to modify
|
||||
/// if user.role == "admin" {
|
||||
/// return Ok(true);
|
||||
/// }
|
||||
///
|
||||
/// // Check other permissions
|
||||
/// check_custom_permissions(user_id)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Your find string should include at least 3 lines of context before and after the part
|
||||
/// you want to change:
|
||||
///
|
||||
/// ```ignore
|
||||
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
|
||||
/// // Check if user exists first
|
||||
/// let user = database.find_user(user_id)?;
|
||||
///
|
||||
/// // This is the part we want to modify
|
||||
/// if user.role == "admin" {
|
||||
/// return Ok(true);
|
||||
/// }
|
||||
///
|
||||
/// // Check other permissions
|
||||
/// check_custom_permissions(user_id)
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// And your replace string might look like:
|
||||
///
|
||||
/// ```ignore
|
||||
/// fn check_user_permissions(user_id: &str) -> Result<bool> {
|
||||
/// // Check if user exists first
|
||||
/// let user = database.find_user(user_id)?;
|
||||
///
|
||||
/// // This is the part we want to modify
|
||||
/// if user.role == "admin" || user.role == "superuser" {
|
||||
/// return Ok(true);
|
||||
/// }
|
||||
///
|
||||
/// // Check other permissions
|
||||
/// check_custom_permissions(user_id)
|
||||
/// }
|
||||
/// ```
|
||||
/// </example>
|
||||
pub find: String,
|
||||
|
||||
/// The string to replace the one unique occurrence of the find string with.
|
||||
pub replace: String,
|
||||
}
|
||||
|
||||
pub struct FindReplaceFileTool;
|
||||
|
||||
impl Tool for FindReplaceFileTool {
|
||||
fn name(&self) -> String {
|
||||
"find-replace-file".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("find_replace_tool/description.md").to_string()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Pencil
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
|
||||
json_schema_for::<FindReplaceFileToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<FindReplaceFileToolInput>(input.clone()) {
|
||||
Ok(input) => input.display_description,
|
||||
Err(_) => "Edit file".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input = match serde_json::from_value::<FindReplaceFileToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let project_path = project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.find_project_path(&input.path, cx)
|
||||
.context("Path not found in project")
|
||||
})??;
|
||||
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.await?;
|
||||
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
if input.find.is_empty() {
|
||||
return Err(anyhow!("`find` string cannot be empty. Use a different tool if you want to create a file."));
|
||||
}
|
||||
|
||||
if input.find == input.replace {
|
||||
return Err(anyhow!("The `find` and `replace` strings are identical, so no changes would be made."));
|
||||
}
|
||||
|
||||
let result = cx
|
||||
.background_spawn(async move {
|
||||
// Try to match exactly
|
||||
replace_exact(&input.find, &input.replace, &snapshot)
|
||||
.await
|
||||
// If that fails, try being flexible about indentation
|
||||
.or_else(|| replace_with_flexible_indent(&input.find, &input.replace, &snapshot))
|
||||
})
|
||||
.await;
|
||||
|
||||
if let Some(diff) = result {
|
||||
let edit_ids = buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.apply_diff(diff, false, cx);
|
||||
let transaction = buffer.finalize_last_transaction();
|
||||
transaction.map_or(Vec::new(), |transaction| transaction.edit_ids.clone())
|
||||
})?;
|
||||
|
||||
action_log.update(cx, |log, cx| {
|
||||
log.buffer_edited(buffer.clone(), edit_ids, cx)
|
||||
})?;
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
project.save_buffer(buffer, cx)
|
||||
})?.await?;
|
||||
|
||||
Ok(format!("Edited {}", input.path.display()))
|
||||
} else {
|
||||
let err = buffer.read_with(cx, |buffer, _cx| {
|
||||
let file_exists = buffer
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state().exists());
|
||||
|
||||
if !file_exists {
|
||||
anyhow!("{} does not exist", input.path.display())
|
||||
} else if buffer.is_empty() {
|
||||
anyhow!(
|
||||
"{} is empty, so the provided `find` string wasn't found.",
|
||||
input.path.display()
|
||||
)
|
||||
} else {
|
||||
anyhow!("Failed to match the provided `find` string")
|
||||
}
|
||||
})?;
|
||||
|
||||
Err(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
Find one unique part of a file in the project and replace that text with new text.
|
||||
|
||||
This tool is the preferred way to make edits to files, *unless* those edits could be done by a code action. For example, you must always use a code action (if available) to rename something, to add missing imports, or to remove unused imports, instead of using this tool. You should only use this tool for one of those purposes if you already tried and failed to use a code action tool to accomplish those tasks. If you are renaming, adding missing imports, removing unused imports, or doing anything else that a code action tool could have done, and you have not already tried and failed to use a code action tool for that instead (assuming one was available), then you have made a mistake. Always prefer code action tools over this tool when both are available. The same rule applies to all other code actions that are available to you.
|
||||
|
||||
To be extremely direct about this, you must not use this for renaming if there is a code action available. If your task involves renaming something, do not use this tool unless you have already tried and failed to use another tool for that purpose.
|
||||
|
||||
If you have multiple edits to make, including edits across multiple files, then make a plan to respond with a single message containing a batch of calls to this tool - one call for each find/replace operation.
|
||||
|
||||
You should only use this tool when you want to edit a subset of a file's contents, but not the entire file. You should not use this tool when you want to replace the entire contents of a file with completely different contents. You also should not use this tool when you want to move or rename a file. You absolutely must NEVER use this tool to create new files from scratch. If you ever consider using this tool to create a new file from scratch, for any reason, instead you must reconsider and choose a different approach.
|
||||
|
||||
DO NOT call this tool until the code to be edited appears in the conversation! You must use another tool to read the file's contents into the conversation, or ask the user to add it to context first.
|
||||
|
||||
Never call this tool with identical "find" and "replace" strings. Instead, stop and think about what you actually want to do.
|
||||
Reference in New Issue
Block a user