assistant edit tool: Report when file is empty or doesn't exist (#27190)

Agus Zubiaga created

Instead of just reporting a search match failure, we'll now indicate
whether the file is empty or exists to help the model recover better
from bad edits.


Release Notes:

- N/A

Change summary

crates/assistant_tools/src/edit_files_tool.rs | 65 +++++++++++++++++---
1 file changed, 55 insertions(+), 10 deletions(-)

Detailed changes

crates/assistant_tools/src/edit_files_tool.rs 🔗

@@ -160,9 +160,16 @@ enum DiffResult {
 }
 
 #[derive(Debug)]
-struct BadSearch {
-    file_path: String,
-    search: String,
+enum BadSearch {
+    NoMatch {
+        file_path: String,
+        search: String,
+    },
+    EmptyBuffer {
+        file_path: String,
+        search: String,
+        exists: bool,
+    },
 }
 
 impl EditToolRequest {
@@ -309,6 +316,18 @@ impl EditToolRequest {
         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());
+
+            return Ok(DiffResult::BadSearch(BadSearch::EmptyBuffer {
+                file_path: file_path.display().to_string(),
+                exists,
+                search: old,
+            }));
+        }
+
         let result =
             // Try to match exactly
             replace_exact(&old, &new, &snapshot)
@@ -317,7 +336,7 @@ impl EditToolRequest {
             .or_else(|| replace_with_flexible_indent(&old, &new, &snapshot));
 
         let Some(diff) = result else {
-            return anyhow::Ok(DiffResult::BadSearch(BadSearch {
+            return anyhow::Ok(DiffResult::BadSearch(BadSearch::NoMatch {
                 search: old,
                 file_path: file_path.display().to_string(),
             }));
@@ -377,12 +396,38 @@ impl EditToolRequest {
                     self.bad_searches.len()
                 )?;
 
-                for replace in self.bad_searches {
-                    writeln!(
-                        &mut output,
-                        "## No exact match in: {}\n```\n{}\n```\n",
-                        replace.file_path, replace.search,
-                    )?;
+                for bad_search in self.bad_searches {
+                    match bad_search {
+                        BadSearch::NoMatch { file_path, search } => {
+                            writeln!(
+                                &mut output,
+                                "## No exact match in: `{}`\n```\n{}\n```\n",
+                                file_path, search,
+                            )?;
+                        }
+                        BadSearch::EmptyBuffer {
+                            file_path,
+                            exists: true,
+                            search,
+                        } => {
+                            writeln!(
+                                &mut output,
+                                "## No match because `{}` is empty:\n```\n{}\n```\n",
+                                file_path, search,
+                            )?;
+                        }
+                        BadSearch::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,