agent: Remove edit_files
tool (#28041)
Release Notes: - agent: Remove `edit_files` tool in favor of `find_replace`
This commit is contained in:
parent
1bc5618f61
commit
cc9cc12f7b
8 changed files with 0 additions and 2103 deletions
|
@ -16,7 +16,6 @@ anyhow.workspace = true
|
|||
assistant_tool.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
feature_flags.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
html_to_markdown.workspace = true
|
||||
|
@ -24,19 +23,14 @@ http_client.workspace = true
|
|||
itertools.workspace = true
|
||||
language.workspace = true
|
||||
language_model.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
project.workspace = true
|
||||
regex.workspace = true
|
||||
release_channel.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
worktree.workspace = true
|
||||
open = { workspace = true }
|
||||
workspace-hack.workspace = true
|
||||
|
|
|
@ -7,7 +7,6 @@ mod create_directory_tool;
|
|||
mod create_file_tool;
|
||||
mod delete_path_tool;
|
||||
mod diagnostics_tool;
|
||||
mod edit_files_tool;
|
||||
mod fetch_tool;
|
||||
mod find_replace_file_tool;
|
||||
mod list_directory_tool;
|
||||
|
@ -37,7 +36,6 @@ use crate::create_directory_tool::CreateDirectoryTool;
|
|||
use crate::create_file_tool::CreateFileTool;
|
||||
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;
|
||||
|
@ -51,7 +49,6 @@ use crate::thinking_tool::ThinkingTool;
|
|||
|
||||
pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
assistant_tool::init(cx);
|
||||
crate::edit_files_tool::log::init(cx);
|
||||
|
||||
let registry = ToolRegistry::global(cx);
|
||||
registry.register_tool(BashTool);
|
||||
|
@ -64,7 +61,6 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
|||
registry.register_tool(SymbolInfoTool);
|
||||
registry.register_tool(MovePathTool);
|
||||
registry.register_tool(DiagnosticsTool);
|
||||
registry.register_tool(EditFilesTool);
|
||||
registry.register_tool(ListDirectoryTool);
|
||||
registry.register_tool(NowTool);
|
||||
registry.register_tool(OpenTool);
|
||||
|
|
|
@ -1,559 +0,0 @@
|
|||
mod edit_action;
|
||||
pub mod log;
|
||||
|
||||
use crate::replace::{replace_exact, replace_with_flexible_indent};
|
||||
use crate::schema::json_schema_for;
|
||||
use anyhow::{Context, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use collections::HashSet;
|
||||
use edit_action::{EditAction, EditActionParser, edit_model_prompt};
|
||||
use futures::{SinkExt, StreamExt, channel::mpsc};
|
||||
use gpui::{App, AppContext, AsyncApp, Entity, Task};
|
||||
use language_model::{ConfiguredModel, LanguageModelToolSchemaFormat};
|
||||
use language_model::{
|
||||
LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage, MessageContent, Role,
|
||||
};
|
||||
use log::{EditToolLog, EditToolRequestId};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Write;
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct EditFilesToolInput {
|
||||
/// High-level edit instructions. These will be interpreted by a smaller
|
||||
/// model, so explain the changes you want that model to make and which
|
||||
/// file paths need changing. The description should be concise and clear.
|
||||
///
|
||||
/// WARNING: When specifying which file paths need changing, you MUST
|
||||
/// start each path with one of the project's root directories.
|
||||
///
|
||||
/// WARNING: NEVER include code blocks or snippets in edit instructions.
|
||||
/// Only provide natural language descriptions of the changes needed! The tool will
|
||||
/// reject any instructions that contain code blocks or snippets.
|
||||
///
|
||||
/// The following examples assume we have two root directories in the project:
|
||||
/// - root-1
|
||||
/// - root-2
|
||||
///
|
||||
/// <example>
|
||||
/// If you want to introduce a new quit function to kill the process, your
|
||||
/// instructions should be: "Add a new `quit` function to
|
||||
/// `root-1/src/main.rs` to kill the process".
|
||||
///
|
||||
/// Notice how the file path starts with root-1. Without that, the path
|
||||
/// would be ambiguous and the call would fail!
|
||||
/// </example>
|
||||
///
|
||||
/// <example>
|
||||
/// If you want to change documentation to always start with a capital
|
||||
/// letter, your instructions should be: "In `root-2/db.js`,
|
||||
/// `root-2/inMemory.js` and `root-2/sql.js`, change all the documentation
|
||||
/// to start with a capital letter".
|
||||
///
|
||||
/// Notice how we never specify code snippets in the instructions!
|
||||
/// </example>
|
||||
pub edit_instructions: String,
|
||||
|
||||
/// A user-friendly description of what changes are being made.
|
||||
/// This will be shown to the user in the UI to describe the edit operation. The screen real estate for this UI will be extremely
|
||||
/// constrained, so make the description extremely terse.
|
||||
///
|
||||
/// <example>
|
||||
/// For fixing a broken authentication system:
|
||||
/// "Fix auth bug in login flow"
|
||||
/// </example>
|
||||
///
|
||||
/// <example>
|
||||
/// For adding unit tests to a module:
|
||||
/// "Add tests for user profile logic"
|
||||
/// </example>
|
||||
pub display_description: String,
|
||||
}
|
||||
|
||||
pub struct EditFilesTool;
|
||||
|
||||
impl Tool for EditFilesTool {
|
||||
fn name(&self) -> String {
|
||||
"edit_files".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./edit_files_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Pencil
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> serde_json::Value {
|
||||
json_schema_for::<EditFilesToolInput>(format)
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<EditFilesToolInput>(input.clone()) {
|
||||
Ok(input) => input.display_description,
|
||||
Err(_) => "Edit files".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::<EditFilesToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
|
||||
match EditToolLog::try_global(cx) {
|
||||
Some(log) => {
|
||||
let req_id = log.update(cx, |log, cx| {
|
||||
log.new_request(input.edit_instructions.clone(), cx)
|
||||
});
|
||||
|
||||
let task = EditToolRequest::new(
|
||||
input,
|
||||
messages,
|
||||
project,
|
||||
action_log,
|
||||
Some((log.clone(), req_id)),
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let result = task.await;
|
||||
|
||||
let str_result = match &result {
|
||||
Ok(out) => Ok(out.clone()),
|
||||
Err(err) => Err(err.to_string()),
|
||||
};
|
||||
|
||||
log.update(cx, |log, cx| log.set_tool_output(req_id, str_result, cx))
|
||||
.log_err();
|
||||
|
||||
result
|
||||
})
|
||||
}
|
||||
|
||||
None => EditToolRequest::new(input, messages, project, action_log, None, cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct EditToolRequest {
|
||||
parser: EditActionParser,
|
||||
editor_response: EditorResponse,
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
tool_log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
|
||||
}
|
||||
|
||||
enum EditorResponse {
|
||||
/// The editor model hasn't produced any actions yet.
|
||||
/// If we don't have any by the end, we'll return its message to the architect model.
|
||||
Message(String),
|
||||
/// The editor model produced at least one action.
|
||||
Actions {
|
||||
applied: Vec<AppliedAction>,
|
||||
search_errors: Vec<SearchError>,
|
||||
},
|
||||
}
|
||||
|
||||
struct AppliedAction {
|
||||
source: String,
|
||||
buffer: Entity<language::Buffer>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum DiffResult {
|
||||
Diff(language::Diff),
|
||||
SearchError(SearchError),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum SearchError {
|
||||
NoMatch {
|
||||
file_path: String,
|
||||
search: String,
|
||||
},
|
||||
EmptyBuffer {
|
||||
file_path: String,
|
||||
search: String,
|
||||
exists: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl EditToolRequest {
|
||||
fn new(
|
||||
input: EditFilesToolInput,
|
||||
messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
action_log: Entity<ActionLog>,
|
||||
tool_log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let model_registry = LanguageModelRegistry::read_global(cx);
|
||||
let Some(ConfiguredModel { model, .. }) = model_registry.default_model() else {
|
||||
return Task::ready(Err(anyhow!("No model configured")));
|
||||
};
|
||||
|
||||
let mut messages = messages.to_vec();
|
||||
// Remove the last tool use (this run) to prevent an invalid request
|
||||
'outer: for message in messages.iter_mut().rev() {
|
||||
for (index, content) in message.content.iter().enumerate().rev() {
|
||||
match content {
|
||||
MessageContent::ToolUse(_) => {
|
||||
message.content.remove(index);
|
||||
break 'outer;
|
||||
}
|
||||
MessageContent::ToolResult(_) => {
|
||||
// If we find any tool results before a tool use, the request is already valid
|
||||
break 'outer;
|
||||
}
|
||||
MessageContent::Text(_) | MessageContent::Image(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
messages.push(LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![edit_model_prompt().into(), input.edit_instructions.into()],
|
||||
cache: false,
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let llm_request = LanguageModelRequest {
|
||||
messages,
|
||||
tools: vec![],
|
||||
stop: vec![],
|
||||
temperature: Some(0.0),
|
||||
};
|
||||
|
||||
let (mut tx, mut rx) = mpsc::channel::<String>(32);
|
||||
let stream = model.stream_completion_text(llm_request, &cx);
|
||||
let reader_task = cx.background_spawn(async move {
|
||||
let mut chunks = stream.await?;
|
||||
|
||||
while let Some(chunk) = chunks.stream.next().await {
|
||||
if let Some(chunk) = chunk.log_err() {
|
||||
// we don't process here because the API fails
|
||||
// if we take too long between reads
|
||||
tx.send(chunk).await?
|
||||
}
|
||||
}
|
||||
tx.close().await?;
|
||||
anyhow::Ok(())
|
||||
});
|
||||
|
||||
let mut request = Self {
|
||||
parser: EditActionParser::new(),
|
||||
editor_response: EditorResponse::Message(String::with_capacity(256)),
|
||||
action_log,
|
||||
project,
|
||||
tool_log,
|
||||
};
|
||||
|
||||
while let Some(chunk) = rx.next().await {
|
||||
request.process_response_chunk(&chunk, cx).await?;
|
||||
}
|
||||
|
||||
reader_task.await?;
|
||||
|
||||
request.finalize(cx).await
|
||||
})
|
||||
}
|
||||
|
||||
async fn process_response_chunk(&mut self, chunk: &str, cx: &mut AsyncApp) -> Result<()> {
|
||||
let new_actions = self.parser.parse_chunk(chunk);
|
||||
|
||||
if let EditorResponse::Message(ref mut message) = self.editor_response {
|
||||
if new_actions.is_empty() {
|
||||
message.push_str(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((ref log, req_id)) = self.tool_log {
|
||||
log.update(cx, |log, cx| {
|
||||
log.push_editor_response_chunk(req_id, chunk, &new_actions, cx)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
for action in new_actions {
|
||||
self.apply_action(action, cx).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn apply_action(
|
||||
&mut self,
|
||||
(action, source): (EditAction, String),
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
let project_path = self.project.read_with(cx, |project, cx| {
|
||||
project
|
||||
.find_project_path(action.file_path(), cx)
|
||||
.context("Path not found in project")
|
||||
})??;
|
||||
|
||||
let buffer = self
|
||||
.project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx))?
|
||||
.await?;
|
||||
|
||||
let result = match action {
|
||||
EditAction::Replace {
|
||||
old,
|
||||
new,
|
||||
file_path,
|
||||
} => {
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(Self::replace_diff(old, new, file_path, snapshot))
|
||||
.await
|
||||
}
|
||||
EditAction::Write { content, .. } => Ok(DiffResult::Diff(
|
||||
buffer
|
||||
.read_with(cx, |buffer, cx| buffer.diff(content, cx))?
|
||||
.await,
|
||||
)),
|
||||
}?;
|
||||
|
||||
match result {
|
||||
DiffResult::SearchError(error) => {
|
||||
self.push_search_error(error);
|
||||
}
|
||||
DiffResult::Diff(diff) => {
|
||||
cx.update(|cx| {
|
||||
self.action_log
|
||||
.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.finalize_last_transaction();
|
||||
buffer.apply_diff(diff, cx);
|
||||
buffer.finalize_last_transaction();
|
||||
});
|
||||
self.action_log
|
||||
.update(cx, |log, cx| log.buffer_edited(buffer.clone(), cx));
|
||||
})?;
|
||||
|
||||
self.push_applied_action(AppliedAction { source, buffer });
|
||||
}
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
|
||||
fn push_search_error(&mut self, error: SearchError) {
|
||||
match &mut self.editor_response {
|
||||
EditorResponse::Message(_) => {
|
||||
self.editor_response = EditorResponse::Actions {
|
||||
applied: Vec::new(),
|
||||
search_errors: vec![error],
|
||||
};
|
||||
}
|
||||
EditorResponse::Actions { search_errors, .. } => {
|
||||
search_errors.push(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_applied_action(&mut self, action: AppliedAction) {
|
||||
match &mut self.editor_response {
|
||||
EditorResponse::Message(_) => {
|
||||
self.editor_response = EditorResponse::Actions {
|
||||
applied: vec![action],
|
||||
search_errors: Vec::new(),
|
||||
};
|
||||
}
|
||||
EditorResponse::Actions { applied, .. } => {
|
||||
applied.push(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn replace_diff(
|
||||
old: String,
|
||||
new: String,
|
||||
file_path: std::path::PathBuf,
|
||||
snapshot: language::BufferSnapshot,
|
||||
) -> Result<DiffResult> {
|
||||
if snapshot.is_empty() {
|
||||
let exists = snapshot
|
||||
.file()
|
||||
.map_or(false, |file| file.disk_state().exists());
|
||||
|
||||
let error = SearchError::EmptyBuffer {
|
||||
file_path: file_path.display().to_string(),
|
||||
exists,
|
||||
search: old,
|
||||
};
|
||||
|
||||
return Ok(DiffResult::SearchError(error));
|
||||
}
|
||||
|
||||
let replace_result =
|
||||
// Try to match exactly
|
||||
replace_exact(&old, &new, &snapshot)
|
||||
.await
|
||||
// If that fails, try being flexible about indentation
|
||||
.or_else(|| replace_with_flexible_indent(&old, &new, &snapshot));
|
||||
|
||||
let Some(diff) = replace_result else {
|
||||
let error = SearchError::NoMatch {
|
||||
search: old,
|
||||
file_path: file_path.display().to_string(),
|
||||
};
|
||||
|
||||
return Ok(DiffResult::SearchError(error));
|
||||
};
|
||||
|
||||
Ok(DiffResult::Diff(diff))
|
||||
}
|
||||
|
||||
async fn finalize(self, cx: &mut AsyncApp) -> Result<String> {
|
||||
match self.editor_response {
|
||||
EditorResponse::Message(message) => Err(anyhow!(
|
||||
"No edits were applied! You might need to provide more context.\n\n{}",
|
||||
message
|
||||
)),
|
||||
EditorResponse::Actions {
|
||||
applied,
|
||||
search_errors,
|
||||
} => {
|
||||
let mut output = String::with_capacity(1024);
|
||||
|
||||
let parse_errors = self.parser.errors();
|
||||
let has_errors = !search_errors.is_empty() || !parse_errors.is_empty();
|
||||
|
||||
if has_errors {
|
||||
let error_count = search_errors.len() + parse_errors.len();
|
||||
|
||||
if applied.is_empty() {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"{} errors occurred! No edits were applied.",
|
||||
error_count,
|
||||
)?;
|
||||
} else {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"{} errors occurred, but {} edits were correctly applied.",
|
||||
error_count,
|
||||
applied.len(),
|
||||
)?;
|
||||
|
||||
writeln!(
|
||||
&mut output,
|
||||
"# {} SEARCH/REPLACE block(s) applied:\n\nDo not re-send these since they are already applied!\n",
|
||||
applied.len()
|
||||
)?;
|
||||
}
|
||||
} else {
|
||||
write!(
|
||||
&mut output,
|
||||
"Successfully applied! Here's a list of applied edits:"
|
||||
)?;
|
||||
}
|
||||
|
||||
let mut changed_buffers = HashSet::default();
|
||||
|
||||
for action in applied {
|
||||
changed_buffers.insert(action.buffer.clone());
|
||||
write!(&mut output, "\n\n{}", action.source)?;
|
||||
}
|
||||
|
||||
for buffer in &changed_buffers {
|
||||
self.project
|
||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
||||
.await?;
|
||||
}
|
||||
|
||||
if !search_errors.is_empty() {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"\n\n## {} SEARCH/REPLACE block(s) failed to match:\n",
|
||||
search_errors.len()
|
||||
)?;
|
||||
|
||||
for error in search_errors {
|
||||
match error {
|
||||
SearchError::NoMatch { file_path, search } => {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"### No exact match in: `{}`\n```\n{}\n```\n",
|
||||
file_path, search,
|
||||
)?;
|
||||
}
|
||||
SearchError::EmptyBuffer {
|
||||
file_path,
|
||||
exists: true,
|
||||
search,
|
||||
} => {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"### No match because `{}` is empty:\n```\n{}\n```\n",
|
||||
file_path, search,
|
||||
)?;
|
||||
}
|
||||
SearchError::EmptyBuffer {
|
||||
file_path,
|
||||
exists: false,
|
||||
search,
|
||||
} => {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"### No match because `{}` does not exist:\n```\n{}\n```\n",
|
||||
file_path, search,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
write!(
|
||||
&mut output,
|
||||
"The SEARCH section must exactly match an existing block of lines including all white \
|
||||
space, comments, indentation, docstrings, etc."
|
||||
)?;
|
||||
}
|
||||
|
||||
if !parse_errors.is_empty() {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"\n\n## {} SEARCH/REPLACE blocks failed to parse:",
|
||||
parse_errors.len()
|
||||
)?;
|
||||
|
||||
for error in parse_errors {
|
||||
writeln!(&mut output, "- {}", error)?;
|
||||
}
|
||||
}
|
||||
|
||||
if has_errors {
|
||||
writeln!(
|
||||
&mut output,
|
||||
"\n\nYou can fix errors by running the tool again. You can include instructions, \
|
||||
but errors are part of the conversation so you don't need to repeat them.",
|
||||
)?;
|
||||
|
||||
Err(anyhow!(output))
|
||||
} else {
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
Edit files in the current project by specifying instructions in natural language.
|
||||
|
||||
IMPORTANT NOTE: If there is a find-replace tool, use that instead of this tool! This tool is only to be used as a fallback in case that tool is unavailable. Always prefer that tool if it is available.
|
||||
|
||||
When using this tool, you should suggest one coherent edit that can be made to the codebase.
|
||||
|
||||
When the set of edits you want to make is large or complex, feel free to invoke this tool multiple times, each time focusing on a specific change you wanna make.
|
||||
|
||||
You should 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, and 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 the `read-files` tool or ask the user to add it to context first.
|
|
@ -1,967 +0,0 @@
|
|||
use std::{
|
||||
mem::take,
|
||||
ops::Range,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use util::ResultExt;
|
||||
|
||||
/// Represents an edit action to be performed on a file.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum EditAction {
|
||||
/// Replace specific content in a file with new content
|
||||
Replace {
|
||||
file_path: PathBuf,
|
||||
old: String,
|
||||
new: String,
|
||||
},
|
||||
/// Write content to a file (create or overwrite)
|
||||
Write { file_path: PathBuf, content: String },
|
||||
}
|
||||
|
||||
impl EditAction {
|
||||
pub fn file_path(&self) -> &Path {
|
||||
match self {
|
||||
EditAction::Replace { file_path, .. } => file_path,
|
||||
EditAction::Write { file_path, .. } => file_path,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses edit actions from an LLM response.
|
||||
/// See system.md for more details on the format.
|
||||
#[derive(Debug)]
|
||||
pub struct EditActionParser {
|
||||
state: State,
|
||||
line: usize,
|
||||
column: usize,
|
||||
marker_ix: usize,
|
||||
action_source: Vec<u8>,
|
||||
fence_start_offset: usize,
|
||||
block_range: Range<usize>,
|
||||
old_range: Range<usize>,
|
||||
new_range: Range<usize>,
|
||||
errors: Vec<ParseError>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum State {
|
||||
/// Anywhere outside an action
|
||||
Default,
|
||||
/// After opening ```, in optional language tag
|
||||
OpenFence,
|
||||
/// In SEARCH marker
|
||||
SearchMarker,
|
||||
/// In search block or divider
|
||||
SearchBlock,
|
||||
/// In replace block or REPLACE marker
|
||||
ReplaceBlock,
|
||||
/// In closing ```
|
||||
CloseFence,
|
||||
}
|
||||
|
||||
/// used to avoid having source code that looks like git-conflict markers
|
||||
macro_rules! marker_sym {
|
||||
($char:expr) => {
|
||||
concat!($char, $char, $char, $char, $char, $char, $char)
|
||||
};
|
||||
}
|
||||
|
||||
const SEARCH_MARKER: &str = concat!(marker_sym!('<'), " SEARCH");
|
||||
const DIVIDER: &str = marker_sym!('=');
|
||||
const NL_DIVIDER: &str = concat!("\n", marker_sym!('='));
|
||||
const REPLACE_MARKER: &str = concat!(marker_sym!('>'), " REPLACE");
|
||||
const NL_REPLACE_MARKER: &str = concat!("\n", marker_sym!('>'), " REPLACE");
|
||||
const FENCE: &str = "```";
|
||||
|
||||
impl EditActionParser {
|
||||
/// Creates a new `EditActionParser`
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
state: State::Default,
|
||||
line: 1,
|
||||
column: 0,
|
||||
action_source: Vec::new(),
|
||||
fence_start_offset: 0,
|
||||
marker_ix: 0,
|
||||
block_range: Range::default(),
|
||||
old_range: Range::default(),
|
||||
new_range: Range::default(),
|
||||
errors: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Processes a chunk of input text and returns any completed edit actions.
|
||||
///
|
||||
/// This method can be called repeatedly with fragments of input. The parser
|
||||
/// maintains its state between calls, allowing you to process streaming input
|
||||
/// as it becomes available. Actions are only inserted once they are fully parsed.
|
||||
///
|
||||
/// If a block fails to parse, it will simply be skipped and an error will be recorded.
|
||||
/// All errors can be accessed through the `EditActionsParser::errors` method.
|
||||
pub fn parse_chunk(&mut self, input: &str) -> Vec<(EditAction, String)> {
|
||||
use State::*;
|
||||
|
||||
let mut actions = Vec::new();
|
||||
|
||||
for byte in input.bytes() {
|
||||
// Update line and column tracking
|
||||
if byte == b'\n' {
|
||||
self.line += 1;
|
||||
self.column = 0;
|
||||
} else {
|
||||
self.column += 1;
|
||||
}
|
||||
|
||||
let action_offset = self.action_source.len();
|
||||
|
||||
match &self.state {
|
||||
Default => match self.match_marker(byte, FENCE, false) {
|
||||
MarkerMatch::Complete => {
|
||||
self.fence_start_offset = action_offset + 1 - FENCE.len();
|
||||
self.to_state(OpenFence);
|
||||
}
|
||||
MarkerMatch::Partial => {}
|
||||
MarkerMatch::None => {
|
||||
if self.marker_ix > 0 {
|
||||
self.marker_ix = 0;
|
||||
} else if self.action_source.ends_with(b"\n") {
|
||||
self.action_source.clear();
|
||||
}
|
||||
}
|
||||
},
|
||||
OpenFence => {
|
||||
// skip language tag
|
||||
if byte == b'\n' {
|
||||
self.to_state(SearchMarker);
|
||||
}
|
||||
}
|
||||
SearchMarker => {
|
||||
if self.expect_marker(byte, SEARCH_MARKER, true) {
|
||||
self.to_state(SearchBlock);
|
||||
}
|
||||
}
|
||||
SearchBlock => {
|
||||
if self.extend_block_range(byte, DIVIDER, NL_DIVIDER) {
|
||||
self.old_range = take(&mut self.block_range);
|
||||
self.to_state(ReplaceBlock);
|
||||
}
|
||||
}
|
||||
ReplaceBlock => {
|
||||
if self.extend_block_range(byte, REPLACE_MARKER, NL_REPLACE_MARKER) {
|
||||
self.new_range = take(&mut self.block_range);
|
||||
self.to_state(CloseFence);
|
||||
}
|
||||
}
|
||||
CloseFence => {
|
||||
if self.expect_marker(byte, FENCE, false) {
|
||||
self.action_source.push(byte);
|
||||
|
||||
if let Some(action) = self.action() {
|
||||
actions.push(action);
|
||||
}
|
||||
|
||||
self.errors();
|
||||
self.reset();
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.action_source.push(byte);
|
||||
}
|
||||
|
||||
actions
|
||||
}
|
||||
|
||||
/// Returns a reference to the errors encountered during parsing.
|
||||
pub fn errors(&self) -> &[ParseError] {
|
||||
&self.errors
|
||||
}
|
||||
|
||||
fn action(&mut self) -> Option<(EditAction, String)> {
|
||||
let old_range = take(&mut self.old_range);
|
||||
let new_range = take(&mut self.new_range);
|
||||
|
||||
let action_source = take(&mut self.action_source);
|
||||
let action_source = String::from_utf8(action_source).log_err()?;
|
||||
|
||||
let mut file_path_bytes = action_source[..self.fence_start_offset].to_owned();
|
||||
|
||||
if file_path_bytes.ends_with("\n") {
|
||||
file_path_bytes.pop();
|
||||
if file_path_bytes.ends_with("\r") {
|
||||
file_path_bytes.pop();
|
||||
}
|
||||
}
|
||||
|
||||
let file_path = PathBuf::from(file_path_bytes);
|
||||
|
||||
if old_range.is_empty() {
|
||||
return Some((
|
||||
EditAction::Write {
|
||||
file_path,
|
||||
content: action_source[new_range].to_owned(),
|
||||
},
|
||||
action_source,
|
||||
));
|
||||
}
|
||||
|
||||
let old = action_source[old_range].to_owned();
|
||||
let new = action_source[new_range].to_owned();
|
||||
|
||||
let action = EditAction::Replace {
|
||||
file_path,
|
||||
old,
|
||||
new,
|
||||
};
|
||||
|
||||
Some((action, action_source))
|
||||
}
|
||||
|
||||
fn to_state(&mut self, state: State) {
|
||||
self.state = state;
|
||||
self.marker_ix = 0;
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.action_source.clear();
|
||||
self.block_range = Range::default();
|
||||
self.old_range = Range::default();
|
||||
self.new_range = Range::default();
|
||||
self.fence_start_offset = 0;
|
||||
self.marker_ix = 0;
|
||||
self.to_state(State::Default);
|
||||
}
|
||||
|
||||
fn expect_marker(&mut self, byte: u8, marker: &'static str, trailing_newline: bool) -> bool {
|
||||
match self.match_marker(byte, marker, trailing_newline) {
|
||||
MarkerMatch::Complete => true,
|
||||
MarkerMatch::Partial => false,
|
||||
MarkerMatch::None => {
|
||||
self.errors.push(ParseError {
|
||||
line: self.line,
|
||||
column: self.column,
|
||||
expected: marker,
|
||||
found: byte,
|
||||
});
|
||||
|
||||
self.reset();
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_block_range(&mut self, byte: u8, marker: &str, nl_marker: &str) -> bool {
|
||||
let marker = if self.block_range.is_empty() {
|
||||
// do not require another newline if block is empty
|
||||
marker
|
||||
} else {
|
||||
nl_marker
|
||||
};
|
||||
|
||||
let offset = self.action_source.len();
|
||||
|
||||
match self.match_marker(byte, marker, true) {
|
||||
MarkerMatch::Complete => {
|
||||
if self.action_source[self.block_range.clone()].ends_with(b"\r") {
|
||||
self.block_range.end -= 1;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
MarkerMatch::Partial => false,
|
||||
MarkerMatch::None => {
|
||||
if self.marker_ix > 0 {
|
||||
self.marker_ix = 0;
|
||||
self.block_range.end = offset;
|
||||
|
||||
// The beginning of marker might match current byte
|
||||
match self.match_marker(byte, marker, true) {
|
||||
MarkerMatch::Complete => return true,
|
||||
MarkerMatch::Partial => return false,
|
||||
MarkerMatch::None => { /* no match, keep collecting */ }
|
||||
}
|
||||
}
|
||||
|
||||
if self.block_range.is_empty() {
|
||||
self.block_range.start = offset;
|
||||
}
|
||||
self.block_range.end = offset + 1;
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn match_marker(&mut self, byte: u8, marker: &str, trailing_newline: bool) -> MarkerMatch {
|
||||
if trailing_newline && self.marker_ix >= marker.len() {
|
||||
if byte == b'\n' {
|
||||
MarkerMatch::Complete
|
||||
} else if byte == b'\r' {
|
||||
MarkerMatch::Partial
|
||||
} else {
|
||||
MarkerMatch::None
|
||||
}
|
||||
} else if byte == marker.as_bytes()[self.marker_ix] {
|
||||
self.marker_ix += 1;
|
||||
|
||||
if self.marker_ix < marker.len() || trailing_newline {
|
||||
MarkerMatch::Partial
|
||||
} else {
|
||||
MarkerMatch::Complete
|
||||
}
|
||||
} else {
|
||||
MarkerMatch::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum MarkerMatch {
|
||||
None,
|
||||
Partial,
|
||||
Complete,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct ParseError {
|
||||
line: usize,
|
||||
column: usize,
|
||||
expected: &'static str,
|
||||
found: u8,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ParseError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"input:{}:{}: Expected marker {:?}, found {:?}",
|
||||
self.line, self.column, self.expected, self.found as char
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn edit_model_prompt() -> String {
|
||||
include_str!("edit_prompt.md")
|
||||
.to_string()
|
||||
.replace("{{SEARCH_MARKER}}", SEARCH_MARKER)
|
||||
.replace("{{DIVIDER}}", DIVIDER)
|
||||
.replace("{{REPLACE_MARKER}}", REPLACE_MARKER)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rand::prelude::*;
|
||||
use util::line_endings;
|
||||
|
||||
const WRONG_MARKER: &str = concat!(marker_sym!('<'), " WRONG_MARKER");
|
||||
|
||||
#[test]
|
||||
fn test_simple_edit_action() {
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"src/main.rs
|
||||
```
|
||||
{}
|
||||
fn original() {{}}
|
||||
{}
|
||||
fn replacement() {{}}
|
||||
{}
|
||||
```
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert_eq!(
|
||||
actions[0].0,
|
||||
EditAction::Replace {
|
||||
file_path: PathBuf::from("src/main.rs"),
|
||||
old: "fn original() {}".to_string(),
|
||||
new: "fn replacement() {}".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_language_tag() {
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"src/main.rs
|
||||
```rust
|
||||
{}
|
||||
fn original() {{}}
|
||||
{}
|
||||
fn replacement() {{}}
|
||||
{}
|
||||
```
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert_eq!(
|
||||
actions[0].0,
|
||||
EditAction::Replace {
|
||||
file_path: PathBuf::from("src/main.rs"),
|
||||
old: "fn original() {}".to_string(),
|
||||
new: "fn replacement() {}".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_surrounding_text() {
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"Here's a modification I'd like to make to the file:
|
||||
|
||||
src/main.rs
|
||||
```rust
|
||||
{}
|
||||
fn original() {{}}
|
||||
{}
|
||||
fn replacement() {{}}
|
||||
{}
|
||||
```
|
||||
|
||||
This change makes the function better.
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert_eq!(
|
||||
actions[0].0,
|
||||
EditAction::Replace {
|
||||
file_path: PathBuf::from("src/main.rs"),
|
||||
old: "fn original() {}".to_string(),
|
||||
new: "fn replacement() {}".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_edit_actions() {
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"First change:
|
||||
src/main.rs
|
||||
```
|
||||
{}
|
||||
fn original() {{}}
|
||||
{}
|
||||
fn replacement() {{}}
|
||||
{}
|
||||
```
|
||||
|
||||
Second change:
|
||||
src/utils.rs
|
||||
```rust
|
||||
{}
|
||||
fn old_util() -> bool {{ false }}
|
||||
{}
|
||||
fn new_util() -> bool {{ true }}
|
||||
{}
|
||||
```
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER, SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions.len(), 2);
|
||||
|
||||
let (action, _) = &actions[0];
|
||||
assert_eq!(
|
||||
action,
|
||||
&EditAction::Replace {
|
||||
file_path: PathBuf::from("src/main.rs"),
|
||||
old: "fn original() {}".to_string(),
|
||||
new: "fn replacement() {}".to_string(),
|
||||
}
|
||||
);
|
||||
let (action2, _) = &actions[1];
|
||||
assert_eq!(
|
||||
action2,
|
||||
&EditAction::Replace {
|
||||
file_path: PathBuf::from("src/utils.rs"),
|
||||
old: "fn old_util() -> bool { false }".to_string(),
|
||||
new: "fn new_util() -> bool { true }".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiline() {
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"src/main.rs
|
||||
```rust
|
||||
{}
|
||||
fn original() {{
|
||||
println!("This is the original function");
|
||||
let x = 42;
|
||||
if x > 0 {{
|
||||
println!("Positive number");
|
||||
}}
|
||||
}}
|
||||
{}
|
||||
fn replacement() {{
|
||||
println!("This is the replacement function");
|
||||
let x = 100;
|
||||
if x > 50 {{
|
||||
println!("Large number");
|
||||
}} else {{
|
||||
println!("Small number");
|
||||
}}
|
||||
}}
|
||||
{}
|
||||
```
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions.len(), 1);
|
||||
|
||||
let (action, _) = &actions[0];
|
||||
assert_eq!(
|
||||
action,
|
||||
&EditAction::Replace {
|
||||
file_path: PathBuf::from("src/main.rs"),
|
||||
old: "fn original() {\n println!(\"This is the original function\");\n let x = 42;\n if x > 0 {\n println!(\"Positive number\");\n }\n}".to_string(),
|
||||
new: "fn replacement() {\n println!(\"This is the replacement function\");\n let x = 100;\n if x > 50 {\n println!(\"Large number\");\n } else {\n println!(\"Small number\");\n }\n}".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_write_action() {
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"Create a new main.rs file:
|
||||
|
||||
src/main.rs
|
||||
```rust
|
||||
{}
|
||||
{}
|
||||
fn new_function() {{
|
||||
println!("This function is being added");
|
||||
}}
|
||||
{}
|
||||
```
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert_eq!(
|
||||
actions[0].0,
|
||||
EditAction::Write {
|
||||
file_path: PathBuf::from("src/main.rs"),
|
||||
content: "fn new_function() {\n println!(\"This function is being added\");\n}"
|
||||
.to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_replace() {
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"src/main.rs
|
||||
```rust
|
||||
{}
|
||||
fn this_will_be_deleted() {{
|
||||
println!("Deleting this function");
|
||||
}}
|
||||
{}
|
||||
{}
|
||||
```
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert_eq!(
|
||||
actions[0].0,
|
||||
EditAction::Replace {
|
||||
file_path: PathBuf::from("src/main.rs"),
|
||||
old: "fn this_will_be_deleted() {\n println!(\"Deleting this function\");\n}"
|
||||
.to_string(),
|
||||
new: "".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(&input.replace("\n", "\r\n"));
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert_eq!(
|
||||
actions[0].0,
|
||||
EditAction::Replace {
|
||||
file_path: PathBuf::from("src/main.rs"),
|
||||
old:
|
||||
"fn this_will_be_deleted() {\r\n println!(\"Deleting this function\");\r\n}"
|
||||
.to_string(),
|
||||
new: "".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_empty_both() {
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"src/main.rs
|
||||
```rust
|
||||
{}
|
||||
{}
|
||||
{}
|
||||
```
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
assert_eq!(actions.len(), 1);
|
||||
assert_eq!(
|
||||
actions[0].0,
|
||||
EditAction::Write {
|
||||
file_path: PathBuf::from("src/main.rs"),
|
||||
content: String::new(),
|
||||
}
|
||||
);
|
||||
assert_no_errors(&parser);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resumability() {
|
||||
// Construct test input using format with multiline string literals
|
||||
let input_part1 = format!("src/main.rs\n```rust\n{}\nfn ori", SEARCH_MARKER);
|
||||
|
||||
let input_part2 = format!("ginal() {{}}\n{}\nfn replacement() {{}}", DIVIDER);
|
||||
|
||||
let input_part3 = format!("\n{}\n```\n", REPLACE_MARKER);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions1 = parser.parse_chunk(&input_part1);
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions1.len(), 0);
|
||||
|
||||
let actions2 = parser.parse_chunk(&input_part2);
|
||||
// No actions should be complete yet
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions2.len(), 0);
|
||||
|
||||
let actions3 = parser.parse_chunk(&input_part3);
|
||||
// The third chunk should complete the action
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(actions3.len(), 1);
|
||||
let (action, _) = &actions3[0];
|
||||
assert_eq!(
|
||||
action,
|
||||
&EditAction::Replace {
|
||||
file_path: PathBuf::from("src/main.rs"),
|
||||
old: "fn original() {}".to_string(),
|
||||
new: "fn replacement() {}".to_string(),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parser_state_preservation() {
|
||||
let mut parser = EditActionParser::new();
|
||||
let first_chunk = format!("src/main.rs\n```rust\n{}\n", SEARCH_MARKER);
|
||||
let actions1 = parser.parse_chunk(&first_chunk);
|
||||
|
||||
// Check parser is in the correct state
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(parser.state, State::SearchBlock);
|
||||
assert_eq!(parser.action_source, first_chunk.as_bytes());
|
||||
|
||||
// Continue parsing
|
||||
let second_chunk = format!("original code\n{}\n", DIVIDER);
|
||||
let actions2 = parser.parse_chunk(&second_chunk);
|
||||
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(parser.state, State::ReplaceBlock);
|
||||
assert_eq!(
|
||||
&parser.action_source[parser.old_range.clone()],
|
||||
b"original code"
|
||||
);
|
||||
|
||||
let third_chunk = format!("replacement code\n{}\n```\n", REPLACE_MARKER);
|
||||
let actions3 = parser.parse_chunk(&third_chunk);
|
||||
|
||||
// After complete parsing, state should reset
|
||||
assert_no_errors(&parser);
|
||||
assert_eq!(parser.state, State::Default);
|
||||
assert_eq!(parser.action_source, b"\n");
|
||||
assert!(parser.old_range.is_empty());
|
||||
assert!(parser.new_range.is_empty());
|
||||
|
||||
assert_eq!(actions1.len(), 0);
|
||||
assert_eq!(actions2.len(), 0);
|
||||
assert_eq!(actions3.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_search_marker() {
|
||||
let input = format!(
|
||||
r#"src/main.rs
|
||||
```rust
|
||||
{}
|
||||
fn original() {{}}
|
||||
{}
|
||||
fn replacement() {{}}
|
||||
{}
|
||||
```
|
||||
"#,
|
||||
WRONG_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(&input);
|
||||
assert_eq!(actions.len(), 0);
|
||||
|
||||
assert_eq!(parser.errors().len(), 1);
|
||||
let error = &parser.errors()[0];
|
||||
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
format!(
|
||||
"input:3:9: Expected marker \"{}\", found 'W'",
|
||||
SEARCH_MARKER
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_missing_closing_fence() {
|
||||
// Construct test input using format with multiline string literals
|
||||
let input = format!(
|
||||
r#"src/main.rs
|
||||
```rust
|
||||
{}
|
||||
fn original() {{}}
|
||||
{}
|
||||
fn replacement() {{}}
|
||||
{}
|
||||
<!-- Missing closing fence -->
|
||||
|
||||
src/utils.rs
|
||||
```rust
|
||||
{}
|
||||
fn utils_func() {{}}
|
||||
{}
|
||||
fn new_utils_func() {{}}
|
||||
{}
|
||||
```
|
||||
"#,
|
||||
SEARCH_MARKER, DIVIDER, REPLACE_MARKER, SEARCH_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(&input);
|
||||
|
||||
// Only the second block should be parsed
|
||||
assert_eq!(actions.len(), 1);
|
||||
let (action, _) = &actions[0];
|
||||
assert_eq!(
|
||||
action,
|
||||
&EditAction::Replace {
|
||||
file_path: PathBuf::from("src/utils.rs"),
|
||||
old: "fn utils_func() {}".to_string(),
|
||||
new: "fn new_utils_func() {}".to_string(),
|
||||
}
|
||||
);
|
||||
assert_eq!(parser.errors().len(), 1);
|
||||
assert_eq!(
|
||||
parser.errors()[0].to_string(),
|
||||
"input:8:1: Expected marker \"```\", found '<'"
|
||||
);
|
||||
|
||||
// The parser should continue after an error
|
||||
assert_eq!(parser.state, State::Default);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_examples_in_edit_prompt() {
|
||||
let mut parser = EditActionParser::new();
|
||||
let actions = parser.parse_chunk(&edit_model_prompt());
|
||||
assert_examples_in_edit_prompt(&actions, parser.errors());
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
fn test_random_chunking_of_edit_prompt(mut rng: StdRng) {
|
||||
let mut parser = EditActionParser::new();
|
||||
let mut remaining: &str = &edit_model_prompt();
|
||||
let mut actions = Vec::with_capacity(5);
|
||||
|
||||
while !remaining.is_empty() {
|
||||
let chunk_size = rng.gen_range(1..=std::cmp::min(remaining.len(), 100));
|
||||
|
||||
let (chunk, rest) = remaining.split_at(chunk_size);
|
||||
|
||||
let chunk_actions = parser.parse_chunk(chunk);
|
||||
actions.extend(chunk_actions);
|
||||
remaining = rest;
|
||||
}
|
||||
|
||||
assert_examples_in_edit_prompt(&actions, parser.errors());
|
||||
}
|
||||
|
||||
fn assert_examples_in_edit_prompt(actions: &[(EditAction, String)], errors: &[ParseError]) {
|
||||
assert_eq!(actions.len(), 5);
|
||||
|
||||
assert_eq!(
|
||||
actions[0].0,
|
||||
EditAction::Replace {
|
||||
file_path: PathBuf::from("mathweb/flask/app.py"),
|
||||
old: "from flask import Flask".to_string(),
|
||||
new: line_endings!("import math\nfrom flask import Flask").to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
actions[1].0,
|
||||
EditAction::Replace {
|
||||
file_path: PathBuf::from("mathweb/flask/app.py"),
|
||||
old: line_endings!("def factorial(n):\n \"compute factorial\"\n\n if n == 0:\n return 1\n else:\n return n * factorial(n-1)\n").to_string(),
|
||||
new: "".to_string(),
|
||||
}
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
actions[2].0,
|
||||
EditAction::Replace {
|
||||
file_path: PathBuf::from("mathweb/flask/app.py"),
|
||||
old: " return str(factorial(n))".to_string(),
|
||||
new: " return str(math.factorial(n))".to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
actions[3].0,
|
||||
EditAction::Write {
|
||||
file_path: PathBuf::from("hello.py"),
|
||||
content: line_endings!(
|
||||
"def hello():\n \"print a greeting\"\n\n print(\"hello\")"
|
||||
)
|
||||
.to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
actions[4].0,
|
||||
EditAction::Replace {
|
||||
file_path: PathBuf::from("main.py"),
|
||||
old: line_endings!(
|
||||
"def hello():\n \"print a greeting\"\n\n print(\"hello\")"
|
||||
)
|
||||
.to_string(),
|
||||
new: "from hello import hello".to_string(),
|
||||
},
|
||||
);
|
||||
|
||||
// The system prompt includes some text that would produce errors
|
||||
assert_eq!(
|
||||
errors[0].to_string(),
|
||||
format!(
|
||||
"input:102:1: Expected marker \"{}\", found '3'",
|
||||
SEARCH_MARKER
|
||||
)
|
||||
);
|
||||
#[cfg(not(windows))]
|
||||
assert_eq!(
|
||||
errors[1].to_string(),
|
||||
format!(
|
||||
"input:109:0: Expected marker \"{}\", found '\\n'",
|
||||
SEARCH_MARKER
|
||||
)
|
||||
);
|
||||
#[cfg(windows)]
|
||||
assert_eq!(
|
||||
errors[1].to_string(),
|
||||
format!(
|
||||
"input:108:1: Expected marker \"{}\", found '\\r'",
|
||||
SEARCH_MARKER
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_print_error() {
|
||||
let input = format!(
|
||||
r#"src/main.rs
|
||||
```rust
|
||||
{}
|
||||
fn original() {{}}
|
||||
{}
|
||||
fn replacement() {{}}
|
||||
{}
|
||||
```
|
||||
"#,
|
||||
WRONG_MARKER, DIVIDER, REPLACE_MARKER
|
||||
);
|
||||
|
||||
let mut parser = EditActionParser::new();
|
||||
parser.parse_chunk(&input);
|
||||
|
||||
assert_eq!(parser.errors().len(), 1);
|
||||
let error = &parser.errors()[0];
|
||||
let expected_error = format!(
|
||||
r#"input:3:9: Expected marker "{}", found 'W'"#,
|
||||
SEARCH_MARKER
|
||||
);
|
||||
|
||||
assert_eq!(format!("{}", error), expected_error);
|
||||
}
|
||||
|
||||
// helpers
|
||||
|
||||
fn assert_no_errors(parser: &EditActionParser) {
|
||||
let errors = parser.errors();
|
||||
|
||||
assert!(
|
||||
errors.is_empty(),
|
||||
"Expected no errors, but found:\n\n{}",
|
||||
errors
|
||||
.iter()
|
||||
.map(|e| e.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join("\n")
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,134 +0,0 @@
|
|||
Act as an expert software developer.
|
||||
Always use best practices when coding.
|
||||
Respect and use existing conventions, libraries, etc that are already present in the code base.
|
||||
|
||||
Take requests for changes to the supplied code.
|
||||
Always reply to the user in the same language they are using.
|
||||
|
||||
Once you understand the request, you MUST:
|
||||
|
||||
1. Decide if you need to propose *SEARCH/REPLACE* edits to any files that haven't been added to the chat. You can create new files without asking!
|
||||
|
||||
2. Think step-by-step and explain the needed changes in a few short sentences.
|
||||
|
||||
3. Describe each change with a *SEARCH/REPLACE block* per the examples below.
|
||||
|
||||
All changes to files must use this *SEARCH/REPLACE block* format.
|
||||
ONLY EVER RETURN CODE IN A *SEARCH/REPLACE BLOCK*!
|
||||
|
||||
# Example conversations:
|
||||
|
||||
## USER: Change get_factorial() to use math.factorial
|
||||
|
||||
## ASSISTANT: To make this change we need to modify `mathweb/flask/app.py` to:
|
||||
|
||||
1. Import the math package.
|
||||
2. Remove the existing factorial() function.
|
||||
3. Update get_factorial() to call math.factorial instead.
|
||||
|
||||
Here are the *SEARCH/REPLACE* blocks:
|
||||
|
||||
mathweb/flask/app.py
|
||||
```python
|
||||
{{SEARCH_MARKER}}
|
||||
from flask import Flask
|
||||
{{DIVIDER}}
|
||||
import math
|
||||
from flask import Flask
|
||||
{{REPLACE_MARKER}}
|
||||
```
|
||||
|
||||
mathweb/flask/app.py
|
||||
```python
|
||||
{{SEARCH_MARKER}}
|
||||
def factorial(n):
|
||||
"compute factorial"
|
||||
|
||||
if n == 0:
|
||||
return 1
|
||||
else:
|
||||
return n * factorial(n-1)
|
||||
|
||||
{{DIVIDER}}
|
||||
{{REPLACE_MARKER}}
|
||||
```
|
||||
|
||||
mathweb/flask/app.py
|
||||
```python
|
||||
{{SEARCH_MARKER}}
|
||||
return str(factorial(n))
|
||||
{{DIVIDER}}
|
||||
return str(math.factorial(n))
|
||||
{{REPLACE_MARKER}}
|
||||
```
|
||||
|
||||
|
||||
## USER: Refactor hello() into its own file.
|
||||
|
||||
## ASSISTANT: To make this change we need to modify `main.py` and make a new file `hello.py`:
|
||||
|
||||
1. Make a new hello.py file with hello() in it.
|
||||
2. Remove hello() from main.py and replace it with an import.
|
||||
|
||||
Here are the *SEARCH/REPLACE* blocks:
|
||||
|
||||
hello.py
|
||||
```python
|
||||
{{SEARCH_MARKER}}
|
||||
{{DIVIDER}}
|
||||
def hello():
|
||||
"print a greeting"
|
||||
|
||||
print("hello")
|
||||
{{REPLACE_MARKER}}
|
||||
```
|
||||
|
||||
main.py
|
||||
```python
|
||||
{{SEARCH_MARKER}}
|
||||
def hello():
|
||||
"print a greeting"
|
||||
|
||||
print("hello")
|
||||
{{DIVIDER}}
|
||||
from hello import hello
|
||||
{{REPLACE_MARKER}}
|
||||
```
|
||||
# *SEARCH/REPLACE block* Rules:
|
||||
|
||||
Every *SEARCH/REPLACE block* must use this format:
|
||||
1. The *FULL* file path alone on a line, verbatim. No bold asterisks, no quotes around it, no escaping of characters, etc.
|
||||
2. The opening fence and code language, eg: ```python
|
||||
3. The start of search block: {{SEARCH_MARKER}}
|
||||
4. A contiguous chunk of lines to search for in the existing source code
|
||||
5. The dividing line: {{DIVIDER}}
|
||||
6. The lines to replace into the source code
|
||||
7. The end of the replace block: {{REPLACE_MARKER}}
|
||||
8. The closing fence: ```
|
||||
|
||||
Use the *FULL* file path, as shown to you by the user. Make sure to include the project's root directory name at the start of the path. *NEVER* specify the absolute path of the file!
|
||||
|
||||
Every *SEARCH* section must *EXACTLY MATCH* the existing file content, character for character, including all comments, docstrings, etc.
|
||||
If the file contains code or other data wrapped/escaped in json/xml/quotes or other containers, you need to propose edits to the literal contents of the file, including the container markup.
|
||||
|
||||
*SEARCH/REPLACE* blocks will *only* replace the first match occurrence.
|
||||
Including multiple unique *SEARCH/REPLACE* blocks if needed.
|
||||
Include enough lines in each SEARCH section to uniquely match each set of lines that need to change.
|
||||
|
||||
Keep *SEARCH/REPLACE* blocks concise.
|
||||
Break large *SEARCH/REPLACE* blocks into a series of smaller blocks that each change a small portion of the file.
|
||||
Include just the changing lines, and a few surrounding lines if needed for uniqueness.
|
||||
Do not include long runs of unchanging lines in *SEARCH/REPLACE* blocks.
|
||||
|
||||
Only create *SEARCH/REPLACE* blocks for files that have been read! Even though the conversation includes `read-file` tool results, you *CANNOT* issue your own reads. If the conversation doesn't include the code you need to edit, ask for it to be read explicitly.
|
||||
|
||||
To move code within a file, use 2 *SEARCH/REPLACE* blocks: 1 to delete it from its current location, 1 to insert it in the new location.
|
||||
|
||||
Pay attention to which filenames the user wants you to edit, especially if they are asking you to create a new file.
|
||||
|
||||
If you want to put code in a new file, use a *SEARCH/REPLACE block* with:
|
||||
- A new file path, including dir name if needed
|
||||
- An empty `SEARCH` section
|
||||
- The new file's contents in the `REPLACE` section
|
||||
|
||||
ONLY EVER RETURN CODE IN A *SEARCH/REPLACE BLOCK*!
|
|
@ -1,417 +0,0 @@
|
|||
use std::path::Path;
|
||||
|
||||
use collections::HashSet;
|
||||
use feature_flags::FeatureFlagAppExt;
|
||||
use gpui::{
|
||||
App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState,
|
||||
SharedString, Subscription, Window, actions, list, prelude::*,
|
||||
};
|
||||
use release_channel::ReleaseChannel;
|
||||
use settings::Settings;
|
||||
use ui::prelude::*;
|
||||
use workspace::{Item, Workspace, WorkspaceId, item::ItemEvent};
|
||||
|
||||
use super::edit_action::EditAction;
|
||||
|
||||
actions!(debug, [EditTool]);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
if cx.is_staff() || ReleaseChannel::global(cx) == ReleaseChannel::Dev {
|
||||
// Track events even before opening the log
|
||||
EditToolLog::global(cx);
|
||||
}
|
||||
|
||||
cx.observe_new(|workspace: &mut Workspace, _, _| {
|
||||
workspace.register_action(|workspace, _: &EditTool, window, cx| {
|
||||
let viewer = cx.new(EditToolLogViewer::new);
|
||||
workspace.add_item_to_active_pane(Box::new(viewer), None, true, window, cx)
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub struct GlobalEditToolLog(Entity<EditToolLog>);
|
||||
|
||||
impl Global for GlobalEditToolLog {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EditToolLog {
|
||||
requests: Vec<EditToolRequest>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Hash, Eq, PartialEq)]
|
||||
pub struct EditToolRequestId(u32);
|
||||
|
||||
impl EditToolLog {
|
||||
pub fn global(cx: &mut App) -> Entity<Self> {
|
||||
match Self::try_global(cx) {
|
||||
Some(entity) => entity,
|
||||
None => {
|
||||
let entity = cx.new(|_cx| Self::default());
|
||||
cx.set_global(GlobalEditToolLog(entity.clone()));
|
||||
entity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
|
||||
cx.try_global::<GlobalEditToolLog>()
|
||||
.map(|log| log.0.clone())
|
||||
}
|
||||
|
||||
pub fn new_request(
|
||||
&mut self,
|
||||
instructions: String,
|
||||
cx: &mut Context<Self>,
|
||||
) -> EditToolRequestId {
|
||||
let id = EditToolRequestId(self.requests.len() as u32);
|
||||
self.requests.push(EditToolRequest {
|
||||
id,
|
||||
instructions,
|
||||
editor_response: None,
|
||||
tool_output: None,
|
||||
parsed_edits: Vec::new(),
|
||||
});
|
||||
cx.emit(EditToolLogEvent::Inserted);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn push_editor_response_chunk(
|
||||
&mut self,
|
||||
id: EditToolRequestId,
|
||||
chunk: &str,
|
||||
new_actions: &[(EditAction, String)],
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(request) = self.requests.get_mut(id.0 as usize) {
|
||||
match &mut request.editor_response {
|
||||
None => {
|
||||
request.editor_response = Some(chunk.to_string());
|
||||
}
|
||||
Some(response) => {
|
||||
response.push_str(chunk);
|
||||
}
|
||||
}
|
||||
request
|
||||
.parsed_edits
|
||||
.extend(new_actions.iter().cloned().map(|(action, _)| action));
|
||||
|
||||
cx.emit(EditToolLogEvent::Updated);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_tool_output(
|
||||
&mut self,
|
||||
id: EditToolRequestId,
|
||||
tool_output: Result<String, String>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(request) = self.requests.get_mut(id.0 as usize) {
|
||||
request.tool_output = Some(tool_output);
|
||||
cx.emit(EditToolLogEvent::Updated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum EditToolLogEvent {
|
||||
Inserted,
|
||||
Updated,
|
||||
}
|
||||
|
||||
impl EventEmitter<EditToolLogEvent> for EditToolLog {}
|
||||
|
||||
pub struct EditToolRequest {
|
||||
id: EditToolRequestId,
|
||||
instructions: String,
|
||||
// we don't use a result here because the error might have occurred after we got a response
|
||||
editor_response: Option<String>,
|
||||
parsed_edits: Vec<EditAction>,
|
||||
tool_output: Option<Result<String, String>>,
|
||||
}
|
||||
|
||||
pub struct EditToolLogViewer {
|
||||
focus_handle: FocusHandle,
|
||||
log: Entity<EditToolLog>,
|
||||
list_state: ListState,
|
||||
expanded_edits: HashSet<(EditToolRequestId, usize)>,
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl EditToolLogViewer {
|
||||
pub fn new(cx: &mut Context<Self>) -> Self {
|
||||
let log = EditToolLog::global(cx);
|
||||
|
||||
let subscription = cx.subscribe(&log, Self::handle_log_event);
|
||||
|
||||
Self {
|
||||
focus_handle: cx.focus_handle(),
|
||||
log: log.clone(),
|
||||
list_state: ListState::new(
|
||||
log.read(cx).requests.len(),
|
||||
ListAlignment::Bottom,
|
||||
px(1024.),
|
||||
{
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
this.update(cx, |this, cx| this.render_request(ix, window, cx))
|
||||
.unwrap()
|
||||
}
|
||||
},
|
||||
),
|
||||
expanded_edits: HashSet::default(),
|
||||
_subscription: subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_log_event(
|
||||
&mut self,
|
||||
_: Entity<EditToolLog>,
|
||||
event: &EditToolLogEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
EditToolLogEvent::Inserted => {
|
||||
let count = self.list_state.item_count();
|
||||
self.list_state.splice(count..count, 1);
|
||||
}
|
||||
EditToolLogEvent::Updated => {}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_request(
|
||||
&self,
|
||||
index: usize,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let requests = &self.log.read(cx).requests;
|
||||
let request = &requests[index];
|
||||
|
||||
v_flex()
|
||||
.gap_3()
|
||||
.child(Self::render_section(IconName::ArrowRight, "Tool Input"))
|
||||
.child(request.instructions.clone())
|
||||
.py_5()
|
||||
.when(index + 1 < requests.len(), |element| {
|
||||
element
|
||||
.border_b_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
})
|
||||
.map(|parent| match &request.editor_response {
|
||||
None => {
|
||||
if request.tool_output.is_none() {
|
||||
parent.child("...")
|
||||
} else {
|
||||
parent
|
||||
}
|
||||
}
|
||||
Some(response) => parent
|
||||
.child(Self::render_section(
|
||||
IconName::ZedAssistant,
|
||||
"Editor Response",
|
||||
))
|
||||
.child(Label::new(response.clone()).buffer_font(cx)),
|
||||
})
|
||||
.when(!request.parsed_edits.is_empty(), |parent| {
|
||||
parent
|
||||
.child(Self::render_section(IconName::Microscope, "Parsed Edits"))
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.children(request.parsed_edits.iter().enumerate().map(
|
||||
|(index, edit)| {
|
||||
self.render_edit_action(edit, request.id, index, cx)
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
.when_some(request.tool_output.as_ref(), |parent, output| {
|
||||
parent
|
||||
.child(Self::render_section(IconName::ArrowLeft, "Tool Output"))
|
||||
.child(match output {
|
||||
Ok(output) => Label::new(output.clone()).color(Color::Success),
|
||||
Err(error) => Label::new(error.clone()).color(Color::Error),
|
||||
})
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_section(icon: IconName, title: &'static str) -> AnyElement {
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(icon).color(Color::Muted))
|
||||
.child(Label::new(title).size(LabelSize::Small).color(Color::Muted))
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_edit_action(
|
||||
&self,
|
||||
edit_action: &EditAction,
|
||||
request_id: EditToolRequestId,
|
||||
index: usize,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let expanded_id = (request_id, index);
|
||||
|
||||
match edit_action {
|
||||
EditAction::Replace {
|
||||
file_path,
|
||||
old,
|
||||
new,
|
||||
} => self
|
||||
.render_edit_action_container(
|
||||
expanded_id,
|
||||
&file_path,
|
||||
[
|
||||
Self::render_block(IconName::MagnifyingGlass, "Search", old.clone(), cx)
|
||||
.border_r_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.into_any(),
|
||||
Self::render_block(IconName::Replace, "Replace", new.clone(), cx)
|
||||
.into_any(),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.into_any(),
|
||||
EditAction::Write { file_path, content } => self
|
||||
.render_edit_action_container(
|
||||
expanded_id,
|
||||
&file_path,
|
||||
[
|
||||
Self::render_block(IconName::Pencil, "Write", content.clone(), cx)
|
||||
.into_any(),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
.into_any(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_edit_action_container(
|
||||
&self,
|
||||
expanded_id: (EditToolRequestId, usize),
|
||||
file_path: &Path,
|
||||
content: impl IntoIterator<Item = AnyElement>,
|
||||
cx: &Context<Self>,
|
||||
) -> AnyElement {
|
||||
let is_expanded = self.expanded_edits.contains(&expanded_id);
|
||||
|
||||
v_flex()
|
||||
.child(
|
||||
h_flex()
|
||||
.bg(cx.theme().colors().element_background)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_t_md()
|
||||
.when(!is_expanded, |el| el.rounded_b_md())
|
||||
.py_1()
|
||||
.px_2()
|
||||
.gap_1()
|
||||
.child(
|
||||
ui::Disclosure::new(ElementId::Integer(expanded_id.1), is_expanded)
|
||||
.on_click(cx.listener(move |this, _ev, _window, cx| {
|
||||
if is_expanded {
|
||||
this.expanded_edits.remove(&expanded_id);
|
||||
} else {
|
||||
this.expanded_edits.insert(expanded_id);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})),
|
||||
)
|
||||
.child(Label::new(file_path.display().to_string()).size(LabelSize::Small)),
|
||||
)
|
||||
.child(if is_expanded {
|
||||
h_flex()
|
||||
.border_1()
|
||||
.border_t_0()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_b_md()
|
||||
.children(content)
|
||||
.into_any()
|
||||
} else {
|
||||
Empty.into_any()
|
||||
})
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn render_block(icon: IconName, title: &'static str, content: String, cx: &App) -> Div {
|
||||
v_flex()
|
||||
.p_1()
|
||||
.gap_1()
|
||||
.flex_1()
|
||||
.h_full()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(icon).color(Color::Muted))
|
||||
.child(Label::new(title).size(LabelSize::Small).color(Color::Muted)),
|
||||
)
|
||||
.font(theme::ThemeSettings::get_global(cx).buffer_font.clone())
|
||||
.text_sm()
|
||||
.child(content)
|
||||
.child(div().flex_1())
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<()> for EditToolLogViewer {}
|
||||
|
||||
impl Focusable for EditToolLogViewer {
|
||||
fn focus_handle(&self, _: &App) -> gpui::FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for EditToolLogViewer {
|
||||
type Event = ();
|
||||
|
||||
fn to_item_events(_: &Self::Event, _: impl FnMut(ItemEvent)) {}
|
||||
|
||||
fn tab_content_text(&self, _window: &Window, _cx: &App) -> Option<SharedString> {
|
||||
Some("Edit Tool Log".into())
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
None
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<WorkspaceId>,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(cx.new(Self::new))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for EditToolLogViewer {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self.list_state.item_count() == 0 {
|
||||
return v_flex()
|
||||
.justify_center()
|
||||
.size_full()
|
||||
.gap_1()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.text_center()
|
||||
.text_lg()
|
||||
.child("No requests yet")
|
||||
.child(
|
||||
div()
|
||||
.text_ui(cx)
|
||||
.child("Go ask the assistant to perform some edits"),
|
||||
);
|
||||
}
|
||||
|
||||
v_flex()
|
||||
.p_4()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.size_full()
|
||||
.child(list(self.list_state.clone()).flex_grow())
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue