agent: Remove edit_files tool (#28041)

Release Notes:

- agent: Remove `edit_files` tool  in favor of `find_replace`
This commit is contained in:
Agus Zubiaga 2025-04-04 13:37:14 -03:00 committed by GitHub
parent 1bc5618f61
commit cc9cc12f7b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 0 additions and 2103 deletions

View file

@ -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

View file

@ -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);

View file

@ -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)
}
}
}
}
}

View file

@ -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.

View file

@ -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")
);
}
}

View file

@ -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*!

View file

@ -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())
}
}