terminal: Don’t include line breaks for soft wrap in Assistant terminal context (#25415)

Richard Hao and Peter Tripp created

> Detects and combines wrapped lines into single logical lines, more
accurately representing the actual terminal content.


```shell
perl -i -pe \
    's/"vscode-languageserver(\/node)?"/"\@zed-industries\/vscode-languageserver$1"/g' packages/css/lib/node/cssServerMain.js
```

<img width="518" alt="image"
src="https://github.com/user-attachments/assets/52d9327c-c381-4e5f-a676-0cf84c824388"
/>

<img width="1314" alt="image"
src="https://github.com/user-attachments/assets/0a32e1f9-7e95-482e-9beb-2e8a6c40584c"
/>




Closes https://github.com/zed-industries/zed/issues/25341

Release Notes:

- Fixed a bug where context for the terminal assistant would add line
breaks in the presence of soft wrapped lines.

---------

Co-authored-by: Peter Tripp <peter@zed.dev>

Change summary

crates/terminal/src/terminal.rs | 71 +++++++++++++++++++++++++++-------
1 file changed, 55 insertions(+), 16 deletions(-)

Detailed changes

crates/terminal/src/terminal.rs 🔗

@@ -8,12 +8,12 @@ pub mod terminal_settings;
 use alacritty_terminal::{
     event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
     event_loop::{EventLoop, Msg, Notifier},
-    grid::{Dimensions, Scroll as AlacScroll},
+    grid::{Dimensions, Grid, Row, Scroll as AlacScroll},
     index::{Boundary, Column, Direction as AlacDirection, Line, Point as AlacPoint},
     selection::{Selection, SelectionRange, SelectionType},
     sync::FairMutex,
     term::{
-        cell::Cell,
+        cell::{Cell, Flags},
         search::{Match, RegexIter, RegexSearch},
         Config, RenderableCursor, TermMode,
     },
@@ -1386,28 +1386,59 @@ impl Terminal {
     pub fn last_n_non_empty_lines(&self, n: usize) -> Vec<String> {
         let term = self.term.clone();
         let terminal = term.lock_unfair();
-
+        let grid = terminal.grid();
         let mut lines = Vec::new();
-        let mut current_line = terminal.bottommost_line();
-        while lines.len() < n {
-            let mut line_buffer = String::new();
-            for cell in &terminal.grid()[current_line] {
-                line_buffer.push(cell.c);
-            }
-            let line = line_buffer.trim_end();
-            if !line.is_empty() {
-                lines.push(line.to_string());
-            }
 
-            if current_line == terminal.topmost_line() {
-                break;
+        let mut current_line = grid.bottommost_line().0;
+        let topmost_line = grid.topmost_line().0;
+
+        while current_line >= topmost_line && lines.len() < n {
+            let logical_line_start = self.find_logical_line_start(grid, current_line, topmost_line);
+            let logical_line = self.construct_logical_line(grid, logical_line_start, current_line);
+
+            if let Some(line) = self.process_line(logical_line) {
+                lines.push(line);
             }
-            current_line = Line(current_line.0 - 1);
+
+            // Move to the line above the start of the current logical line
+            current_line = logical_line_start - 1;
         }
+
         lines.reverse();
         lines
     }
 
+    fn find_logical_line_start(&self, grid: &Grid<Cell>, current: i32, topmost: i32) -> i32 {
+        let mut line_start = current;
+        while line_start > topmost {
+            let prev_line = Line(line_start - 1);
+            let last_cell = &grid[prev_line][Column(grid.columns() - 1)];
+            if !last_cell.flags.contains(Flags::WRAPLINE) {
+                break;
+            }
+            line_start -= 1;
+        }
+        line_start
+    }
+
+    fn construct_logical_line(&self, grid: &Grid<Cell>, start: i32, end: i32) -> String {
+        let mut logical_line = String::new();
+        for row in start..=end {
+            let grid_row = &grid[Line(row)];
+            logical_line.push_str(&row_to_string(grid_row));
+        }
+        logical_line
+    }
+
+    fn process_line(&self, line: String) -> Option<String> {
+        let trimmed = line.trim_end().to_string();
+        if !trimmed.is_empty() {
+            Some(trimmed)
+        } else {
+            None
+        }
+    }
+
     pub fn focus_in(&self) {
         if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
             self.write_to_pty("\x1b[I".to_string());
@@ -1838,6 +1869,14 @@ impl Terminal {
     }
 }
 
+// Helper function to convert a grid row to a string
+pub fn row_to_string(row: &Row<Cell>) -> String {
+    row[..Column(row.len())]
+        .iter()
+        .map(|cell| cell.c)
+        .collect::<String>()
+}
+
 fn is_path_surrounded_by_common_symbols(path: &str) -> bool {
     // Avoid detecting `[]` or `()` strings as paths, surrounded by common symbols
     path.len() > 2