Reapply #27200 after bad conflict resolution (#27446)

Release Notes:

- N/A
This commit is contained in:
Agus Zubiaga 2025-03-25 15:06:45 -03:00 committed by GitHub
parent 3ba624391f
commit 24d76a64c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -148,22 +148,36 @@ impl Tool for EditFilesTool {
struct EditToolRequest { struct EditToolRequest {
parser: EditActionParser, parser: EditActionParser,
output: String, editor_response: EditorResponse,
changed_buffers: HashSet<Entity<language::Buffer>>,
bad_searches: Vec<BadSearch>,
project: Entity<Project>, project: Entity<Project>,
action_log: Entity<ActionLog>, action_log: Entity<ActionLog>,
tool_log: Option<(Entity<EditToolLog>, EditToolRequestId)>, tool_log: Option<(Entity<EditToolLog>, EditToolRequestId)>,
} }
#[derive(Debug)] enum EditorResponse {
enum DiffResult { /// The editor model hasn't produced any actions yet.
BadSearch(BadSearch), /// If we don't have any by the end, we'll return its message to the architect model.
Diff(language::Diff), 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)] #[derive(Debug)]
enum BadSearch { enum DiffResult {
Diff(language::Diff),
SearchError(SearchError),
}
#[derive(Debug)]
enum SearchError {
NoMatch { NoMatch {
file_path: String, file_path: String,
search: String, search: String,
@ -242,9 +256,7 @@ impl EditToolRequest {
let mut request = Self { let mut request = Self {
parser: EditActionParser::new(), parser: EditActionParser::new(),
output: Self::SUCCESS_OUTPUT_HEADER.to_string(), editor_response: EditorResponse::Message(String::with_capacity(256)),
changed_buffers: HashSet::default(),
bad_searches: Vec::new(),
action_log, action_log,
project, project,
tool_log, tool_log,
@ -263,6 +275,12 @@ impl EditToolRequest {
async fn process_response_chunk(&mut self, chunk: &str, cx: &mut AsyncApp) -> Result<()> { async fn process_response_chunk(&mut self, chunk: &str, cx: &mut AsyncApp) -> Result<()> {
let new_actions = self.parser.parse_chunk(chunk); 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 { if let Some((ref log, req_id)) = self.tool_log {
log.update(cx, |log, cx| { log.update(cx, |log, cx| {
log.push_editor_response_chunk(req_id, chunk, &new_actions, cx) log.push_editor_response_chunk(req_id, chunk, &new_actions, cx)
@ -313,18 +331,45 @@ impl EditToolRequest {
}?; }?;
match result { match result {
DiffResult::BadSearch(invalid_replace) => { DiffResult::SearchError(error) => {
self.bad_searches.push(invalid_replace); self.push_search_error(error);
} }
DiffResult::Diff(diff) => { DiffResult::Diff(diff) => {
let _clock = buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx))?; let _clock = buffer.update(cx, |buffer, cx| buffer.apply_diff(diff, cx))?;
write!(&mut self.output, "\n\n{}", source)?; self.push_applied_action(AppliedAction { source, buffer });
self.changed_buffers.insert(buffer);
} }
} }
Ok(()) 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( async fn replace_diff(
@ -338,152 +383,166 @@ impl EditToolRequest {
.file() .file()
.map_or(false, |file| file.disk_state().exists()); .map_or(false, |file| file.disk_state().exists());
return Ok(DiffResult::BadSearch(BadSearch::EmptyBuffer { let error = SearchError::EmptyBuffer {
file_path: file_path.display().to_string(), file_path: file_path.display().to_string(),
exists, exists,
search: old, search: old,
})); };
return Ok(DiffResult::SearchError(error));
} }
let result = let replace_result =
// Try to match exactly // Try to match exactly
replace_exact(&old, &new, &snapshot) replace_exact(&old, &new, &snapshot)
.await .await
// If that fails, try being flexible about indentation // If that fails, try being flexible about indentation
.or_else(|| replace_with_flexible_indent(&old, &new, &snapshot)); .or_else(|| replace_with_flexible_indent(&old, &new, &snapshot));
let Some(diff) = result else { let Some(diff) = replace_result else {
return anyhow::Ok(DiffResult::BadSearch(BadSearch::NoMatch { let error = SearchError::NoMatch {
search: old, search: old,
file_path: file_path.display().to_string(), file_path: file_path.display().to_string(),
})); };
return Ok(DiffResult::SearchError(error));
}; };
anyhow::Ok(DiffResult::Diff(diff)) Ok(DiffResult::Diff(diff))
} }
const SUCCESS_OUTPUT_HEADER: &str = "Successfully applied. Here's a list of changes:";
const ERROR_OUTPUT_HEADER_NO_EDITS: &str = "I couldn't apply any edits!";
const ERROR_OUTPUT_HEADER_WITH_EDITS: &str =
"Errors occurred. First, here's a list of the edits we managed to apply:";
async fn finalize(self, cx: &mut AsyncApp) -> Result<String> { async fn finalize(self, cx: &mut AsyncApp) -> Result<String> {
let changed_buffer_count = self.changed_buffers.len(); 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);
// Save each buffer once at the end let parse_errors = self.parser.errors();
for buffer in &self.changed_buffers { let has_errors = !search_errors.is_empty() || !parse_errors.is_empty();
self.project
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
.await?;
}
self.action_log if has_errors {
.update(cx, |log, cx| log.buffer_edited(self.changed_buffers, cx)) let error_count = search_errors.len() + parse_errors.len();
.log_err();
let errors = self.parser.errors(); 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(),
)?;
if errors.is_empty() && self.bad_searches.is_empty() { writeln!(
if changed_buffer_count == 0 { &mut output,
return Err(anyhow!( "# {} SEARCH/REPLACE block(s) applied:\n\nDo not re-send these since they are already applied!\n",
"The instructions didn't lead to any changes. You might need to consult the file contents first." applied.len()
)); )?;
} }
} else {
write!(
&mut output,
"Successfully applied! Here's a list of applied edits:"
)?;
}
Ok(self.output) let mut changed_buffers = HashSet::default();
} else {
let mut output = self.output;
if output.is_empty() { for action in applied {
output.replace_range( changed_buffers.insert(action.buffer);
0..Self::SUCCESS_OUTPUT_HEADER.len(), write!(&mut output, "\n\n{}", action.source)?;
Self::ERROR_OUTPUT_HEADER_NO_EDITS, }
);
} else {
output.replace_range(
0..Self::SUCCESS_OUTPUT_HEADER.len(),
Self::ERROR_OUTPUT_HEADER_WITH_EDITS,
);
}
if !self.bad_searches.is_empty() { for buffer in &changed_buffers {
writeln!( self.project
&mut output, .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
"\n\n# {} SEARCH/REPLACE block(s) failed to match:\n", .await?;
self.bad_searches.len() }
)?;
for bad_search in self.bad_searches { self.action_log
match bad_search { .update(cx, |log, cx| log.buffer_edited(changed_buffers.clone(), cx))
BadSearch::NoMatch { file_path, search } => { .log_err();
writeln!(
&mut output, if !search_errors.is_empty() {
"## No exact match in: `{}`\n```\n{}\n```\n", writeln!(
file_path, search, &mut output,
)?; "\n\n## {} SEARCH/REPLACE block(s) failed to match:\n",
} search_errors.len()
BadSearch::EmptyBuffer { )?;
file_path,
exists: true, for error in search_errors {
search, match error {
} => { SearchError::NoMatch { file_path, search } => {
writeln!( writeln!(
&mut output, &mut output,
"## No match because `{}` is empty:\n```\n{}\n```\n", "### No exact match in: `{}`\n```\n{}\n```\n",
file_path, search, file_path, search,
)?; )?;
} }
BadSearch::EmptyBuffer { SearchError::EmptyBuffer {
file_path, file_path,
exists: false, exists: true,
search, search,
} => { } => {
writeln!( writeln!(
&mut output, &mut output,
"## No match because `{}` does not exist:\n```\n{}\n```\n", "### No match because `{}` is empty:\n```\n{}\n```\n",
file_path, search, 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)?;
}
} }
write!(&mut output, if has_errors {
"The SEARCH section must exactly match an existing block of lines including all white \ writeln!(&mut output,
space, comments, indentation, docstrings, etc." "\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.",
} )?;
if !errors.is_empty() { Err(anyhow!(output))
writeln!(
&mut output,
"\n\n# {} SEARCH/REPLACE blocks failed to parse:",
errors.len()
)?;
for error in errors {
writeln!(&mut output, "- {}", error)?;
}
}
if changed_buffer_count > 0 {
writeln!(
&mut output,
"\n\nThe other SEARCH/REPLACE blocks were applied successfully. Do not re-send them!",
)?;
}
writeln!(
&mut output,
"{}You 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.",
if changed_buffer_count == 0 {
"\n\n"
} else { } else {
"" Ok(output)
} }
)?; }
Err(anyhow!(output))
} }
} }
} }