Release Notes: - N/A
This commit is contained in:
parent
3ba624391f
commit
24d76a64c3
1 changed files with 188 additions and 129 deletions
|
@ -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,109 +383,131 @@ 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(),
|
||||||
}));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
anyhow::Ok(DiffResult::Diff(diff))
|
return Ok(DiffResult::SearchError(error));
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
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);
|
||||||
|
write!(&mut output, "\n\n{}", action.source)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
for buffer in &changed_buffers {
|
||||||
self.project
|
self.project
|
||||||
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
.update(cx, |project, cx| project.save_buffer(buffer.clone(), cx))?
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.action_log
|
self.action_log
|
||||||
.update(cx, |log, cx| log.buffer_edited(self.changed_buffers, cx))
|
.update(cx, |log, cx| log.buffer_edited(changed_buffers.clone(), cx))
|
||||||
.log_err();
|
.log_err();
|
||||||
|
|
||||||
let errors = self.parser.errors();
|
if !search_errors.is_empty() {
|
||||||
|
|
||||||
if errors.is_empty() && self.bad_searches.is_empty() {
|
|
||||||
if changed_buffer_count == 0 {
|
|
||||||
return Err(anyhow!(
|
|
||||||
"The instructions didn't lead to any changes. You might need to consult the file contents first."
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(self.output)
|
|
||||||
} else {
|
|
||||||
let mut output = self.output;
|
|
||||||
|
|
||||||
if output.is_empty() {
|
|
||||||
output.replace_range(
|
|
||||||
0..Self::SUCCESS_OUTPUT_HEADER.len(),
|
|
||||||
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() {
|
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut output,
|
&mut output,
|
||||||
"\n\n# {} SEARCH/REPLACE block(s) failed to match:\n",
|
"\n\n## {} SEARCH/REPLACE block(s) failed to match:\n",
|
||||||
self.bad_searches.len()
|
search_errors.len()
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
for bad_search in self.bad_searches {
|
for error in search_errors {
|
||||||
match bad_search {
|
match error {
|
||||||
BadSearch::NoMatch { file_path, search } => {
|
SearchError::NoMatch { file_path, search } => {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut output,
|
&mut output,
|
||||||
"## No exact match in: `{}`\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: true,
|
exists: true,
|
||||||
search,
|
search,
|
||||||
} => {
|
} => {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut output,
|
&mut output,
|
||||||
"## No match because `{}` is empty:\n```\n{}\n```\n",
|
"### No match because `{}` is empty:\n```\n{}\n```\n",
|
||||||
file_path, search,
|
file_path, search,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
BadSearch::EmptyBuffer {
|
SearchError::EmptyBuffer {
|
||||||
file_path,
|
file_path,
|
||||||
exists: false,
|
exists: false,
|
||||||
search,
|
search,
|
||||||
} => {
|
} => {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut output,
|
&mut output,
|
||||||
"## No match because `{}` does not exist:\n```\n{}\n```\n",
|
"### No match because `{}` does not exist:\n```\n{}\n```\n",
|
||||||
file_path, search,
|
file_path, search,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
@ -453,37 +520,29 @@ impl EditToolRequest {
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !errors.is_empty() {
|
if !parse_errors.is_empty() {
|
||||||
writeln!(
|
writeln!(
|
||||||
&mut output,
|
&mut output,
|
||||||
"\n\n# {} SEARCH/REPLACE blocks failed to parse:",
|
"\n\n## {} SEARCH/REPLACE blocks failed to parse:",
|
||||||
errors.len()
|
parse_errors.len()
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
for error in errors {
|
for error in parse_errors {
|
||||||
writeln!(&mut output, "- {}", error)?;
|
writeln!(&mut output, "- {}", error)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if changed_buffer_count > 0 {
|
if has_errors {
|
||||||
writeln!(
|
writeln!(&mut output,
|
||||||
&mut output,
|
"\n\nYou can fix errors by running the tool again. You can include instructions, \
|
||||||
"\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.",
|
but errors are part of the conversation so you don't need to repeat them.",
|
||||||
if changed_buffer_count == 0 {
|
|
||||||
"\n\n"
|
|
||||||
} else {
|
|
||||||
""
|
|
||||||
}
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Err(anyhow!(output))
|
Err(anyhow!(output))
|
||||||
|
} else {
|
||||||
|
Ok(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue