Fix potential subtraction overflow (#29697)

Richard Feldman created

I saw this come up in an eval, where the LLM provided a start line of 0.

Release Notes:

- N/A

Change summary

crates/assistant_tools/src/read_file_tool.rs | 64 +++++++++++++++++++++
1 file changed, 63 insertions(+), 1 deletion(-)

Detailed changes

crates/assistant_tools/src/read_file_tool.rs 🔗

@@ -122,7 +122,8 @@ impl Tool for ReadFileTool {
             if input.start_line.is_some() || input.end_line.is_some() {
                 let result = buffer.read_with(cx, |buffer, _cx| {
                     let text = buffer.text();
-                    let start = input.start_line.unwrap_or(1);
+                    // .max(1) because despite instructions to be 1-indexed, sometimes the model passes 0.
+                    let start = input.start_line.unwrap_or(1).max(1);
                     let lines = text.split('\n').skip(start - 1);
                     if let Some(end) = input.end_line {
                         let count = end.saturating_sub(start).saturating_add(1); // Ensure at least 1 line
@@ -329,6 +330,67 @@ mod test {
         assert_eq!(result.unwrap(), "Line 2\nLine 3\nLine 4");
     }
 
+    #[gpui::test]
+    async fn test_read_file_line_range_edge_cases(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree(
+            "/root",
+            json!({
+                "multiline.txt": "Line 1\nLine 2\nLine 3\nLine 4\nLine 5"
+            }),
+        )
+        .await;
+        let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
+        let action_log = cx.new(|_| ActionLog::new(project.clone()));
+
+        // start_line of 0 should be treated as 1
+        let result = cx
+            .update(|cx| {
+                let input = json!({
+                    "path": "root/multiline.txt",
+                    "start_line": 0,
+                    "end_line": 2
+                });
+                Arc::new(ReadFileTool)
+                    .run(input, &[], project.clone(), action_log.clone(), None, cx)
+                    .output
+            })
+            .await;
+        assert_eq!(result.unwrap(), "Line 1\nLine 2");
+
+        // end_line of 0 should result in at least 1 line
+        let result = cx
+            .update(|cx| {
+                let input = json!({
+                    "path": "root/multiline.txt",
+                    "start_line": 1,
+                    "end_line": 0
+                });
+                Arc::new(ReadFileTool)
+                    .run(input, &[], project.clone(), action_log.clone(), None, cx)
+                    .output
+            })
+            .await;
+        assert_eq!(result.unwrap(), "Line 1");
+
+        // when start_line > end_line, should still return at least 1 line
+        let result = cx
+            .update(|cx| {
+                let input = json!({
+                    "path": "root/multiline.txt",
+                    "start_line": 3,
+                    "end_line": 2
+                });
+                Arc::new(ReadFileTool)
+                    .run(input, &[], project.clone(), action_log, None, cx)
+                    .output
+            })
+            .await;
+        assert_eq!(result.unwrap(), "Line 3");
+    }
+
     fn init_test(cx: &mut TestAppContext) {
         cx.update(|cx| {
             let settings_store = SettingsStore::test(cx);