Files
codex/prs/bolinfest/PR-1710.md
2025-09-02 15:17:45 -07:00

11 KiB

PR #1710: fix: long lines incorrectly wrapped

Description

fix to #1685.

Full Diff

diff --git a/codex-rs/tui/src/insert_history.rs b/codex-rs/tui/src/insert_history.rs
index 7948436cd8..1e8b1f5392 100644
--- a/codex-rs/tui/src/insert_history.rs
+++ b/codex-rs/tui/src/insert_history.rs
@@ -1,7 +1,9 @@
+use std::fmt;
 use std::io;
 use std::io::Write;
 
 use crate::tui;
+use crossterm::Command;
 use crossterm::queue;
 use crossterm::style::Color as CColor;
 use crossterm::style::Colors;
@@ -11,46 +13,127 @@ use crossterm::style::SetBackgroundColor;
 use crossterm::style::SetColors;
 use crossterm::style::SetForegroundColor;
 use ratatui::layout::Position;
+use ratatui::layout::Size;
 use ratatui::prelude::Backend;
 use ratatui::style::Color;
 use ratatui::style::Modifier;
 use ratatui::text::Line;
 use ratatui::text::Span;
 
+/// Insert `lines` above the viewport.
 pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec<Line<'static>>) {
-    let screen_height = terminal
-        .backend()
-        .size()
-        .map(|s| s.height)
-        .unwrap_or(0xffffu16);
+    let screen_size = terminal.backend().size().unwrap_or(Size::new(0, 0));
+
     let mut area = terminal.get_frame().area();
-    // We scroll up one line at a time because we can't position the cursor
-    // above the top of the screen. i.e. if
-    //   lines.len() > screen_height - area.top()
-    // we would need to print the first line above the top of the screen, which
-    // can't be done.
-    for line in lines.into_iter() {
-        // 1. Scroll everything above the viewport up by one line
-        if area.bottom() >= screen_height {
-            let top = area.top();
-            terminal.backend_mut().scroll_region_up(0..top, 1).ok();
-            // 2. Move the cursor to the blank line
-            terminal.set_cursor_position(Position::new(0, top - 1)).ok();
-        } else {
-            // If the viewport isn't at the bottom of the screen, scroll down instead
-            terminal
-                .backend_mut()
-                .scroll_region_down(area.top()..area.bottom() + 1, 1)
-                .ok();
-            terminal
-                .set_cursor_position(Position::new(0, area.top()))
-                .ok();
-            area.y += 1;
-        }
-        // 3. Write the line
+
+    let wrapped_lines = wrapped_line_count(&lines, area.width);
+    let cursor_top = if area.bottom() < screen_size.height {
+        // If the viewport is not at the bottom of the screen, scroll it down to make room.
+        // Don't scroll it past the bottom of the screen.
+        let scroll_amount = wrapped_lines.min(screen_size.height - area.bottom());
+        terminal
+            .backend_mut()
+            .scroll_region_down(area.top()..screen_size.height, scroll_amount)
+            .ok();
+        let cursor_top = area.top() - 1;
+        area.y += scroll_amount;
+        terminal.set_viewport_area(area);
+        cursor_top
+    } else {
+        area.top() - 1
+    };
+
+    // Limit the scroll region to the lines from the top of the screen to the
+    // top of the viewport. With this in place, when we add lines inside this
+    // area, only the lines in this area will be scrolled. We place the cursor
+    // at the end of the scroll region, and add lines starting there.
+    //
+    // ┌─Screen───────────────────────┐
+    // │┌╌Scroll region╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐│
+    // │┆                            ┆│
+    // │┆                            ┆│
+    // │┆                            ┆│
+    // │█╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘│
+    // │╭─Viewport───────────────────╮│
+    // ││                            ││
+    // │╰────────────────────────────╯│
+    // └──────────────────────────────┘
+    queue!(std::io::stdout(), SetScrollRegion(1..area.top())).ok();
+
+    terminal
+        .set_cursor_position(Position::new(0, cursor_top))
+        .ok();
+
+    for line in lines {
+        queue!(std::io::stdout(), Print("\r\n")).ok();
         write_spans(&mut std::io::stdout(), line.iter()).ok();
     }
-    terminal.set_viewport_area(area);
+
+    queue!(std::io::stdout(), ResetScrollRegion).ok();
+}
+
+fn wrapped_line_count(lines: &[Line], width: u16) -> u16 {
+    let mut count = 0;
+    for line in lines {
+        count += line_height(line, width);
+    }
+    count
+}
+
+fn line_height(line: &Line, width: u16) -> u16 {
+    use unicode_width::UnicodeWidthStr;
+    // get the total display width of the line, accounting for double-width chars
+    let total_width = line
+        .spans
+        .iter()
+        .map(|span| span.content.width())
+        .sum::<usize>();
+    // divide by width to get the number of lines, rounding up
+    if width == 0 {
+        1
+    } else {
+        (total_width as u16).div_ceil(width).max(1)
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct SetScrollRegion(pub std::ops::Range<u16>);
+
+impl Command for SetScrollRegion {
+    fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
+        write!(f, "\x1b[{};{}r", self.0.start, self.0.end)
+    }
+
+    #[cfg(windows)]
+    fn execute_winapi(&self) -> std::io::Result<()> {
+        panic!("tried to execute SetScrollRegion command using WinAPI, use ANSI instead");
+    }
+
+    #[cfg(windows)]
+    fn is_ansi_code_supported(&self) -> bool {
+        // TODO(nornagon): is this supported on Windows?
+        true
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct ResetScrollRegion;
+
+impl Command for ResetScrollRegion {
+    fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
+        write!(f, "\x1b[r")
+    }
+
+    #[cfg(windows)]
+    fn execute_winapi(&self) -> std::io::Result<()> {
+        panic!("tried to execute ResetScrollRegion command using WinAPI, use ANSI instead");
+    }
+
+    #[cfg(windows)]
+    fn is_ansi_code_supported(&self) -> bool {
+        // TODO(nornagon): is this supported on Windows?
+        true
+    }
 }
 
 struct ModifierDiff {

Review Comments

codex-rs/tui/src/insert_history.rs

@@ -11,46 +11,26 @@ use crossterm::style::SetBackgroundColor;
 use crossterm::style::SetColors;
 use crossterm::style::SetForegroundColor;
 use ratatui::layout::Position;
-use ratatui::prelude::Backend;
 use ratatui::style::Color;
 use ratatui::style::Modifier;
 use ratatui::text::Line;
 use ratatui::text::Span;
 
 pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec<Line<'static>>) {
-    let screen_height = terminal
-        .backend()
-        .size()
-        .map(|s| s.height)
-        .unwrap_or(0xffffu16);
-    let mut area = terminal.get_frame().area();
-    // We scroll up one line at a time because we can't position the cursor
-    // above the top of the screen. i.e. if
-    //   lines.len() > screen_height - area.top()
-    // we would need to print the first line above the top of the screen, which
-    // can't be done.
-    for line in lines.into_iter() {
-        // 1. Scroll everything above the viewport up by one line
-        if area.bottom() >= screen_height {
-            let top = area.top();
-            terminal.backend_mut().scroll_region_up(0..top, 1).ok();
-            // 2. Move the cursor to the blank line
-            terminal.set_cursor_position(Position::new(0, top - 1)).ok();
-        } else {
-            // If the viewport isn't at the bottom of the screen, scroll down instead
-            terminal
-                .backend_mut()
-                .scroll_region_down(area.top()..area.bottom() + 1, 1)
-                .ok();
-            terminal
-                .set_cursor_position(Position::new(0, area.top()))
-                .ok();
-            area.y += 1;
-        }
-        // 3. Write the line
+    let area = terminal.get_frame().area();
+
+    queue!(std::io::stdout(), Print(format!("\x1b[1;{}r", area.top()))).ok();

Can you add a comment because I don't know ANSI codes offhand?

@@ -11,46 +11,26 @@ use crossterm::style::SetBackgroundColor;
 use crossterm::style::SetColors;
 use crossterm::style::SetForegroundColor;
 use ratatui::layout::Position;
-use ratatui::prelude::Backend;
 use ratatui::style::Color;
 use ratatui::style::Modifier;
 use ratatui::text::Line;
 use ratatui::text::Span;
 
 pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec<Line<'static>>) {
-    let screen_height = terminal
-        .backend()
-        .size()
-        .map(|s| s.height)
-        .unwrap_or(0xffffu16);
-    let mut area = terminal.get_frame().area();
-    // We scroll up one line at a time because we can't position the cursor
-    // above the top of the screen. i.e. if
-    //   lines.len() > screen_height - area.top()
-    // we would need to print the first line above the top of the screen, which
-    // can't be done.
-    for line in lines.into_iter() {
-        // 1. Scroll everything above the viewport up by one line
-        if area.bottom() >= screen_height {
-            let top = area.top();
-            terminal.backend_mut().scroll_region_up(0..top, 1).ok();
-            // 2. Move the cursor to the blank line
-            terminal.set_cursor_position(Position::new(0, top - 1)).ok();
-        } else {
-            // If the viewport isn't at the bottom of the screen, scroll down instead
-            terminal
-                .backend_mut()
-                .scroll_region_down(area.top()..area.bottom() + 1, 1)
-                .ok();
-            terminal
-                .set_cursor_position(Position::new(0, area.top()))
-                .ok();
-            area.y += 1;
-        }
-        // 3. Write the line
+    let area = terminal.get_frame().area();
+
+    queue!(std::io::stdout(), Print(format!("\x1b[1;{}r", area.top()))).ok();
+
+    terminal
+        .set_cursor_position(Position::new(0, area.top() - 1))
+        .ok();
+
+    for line in lines {
+        queue!(std::io::stdout(), Print("\r\n")).ok();
         write_spans(&mut std::io::stdout(), line.iter()).ok();
     }
-    terminal.set_viewport_area(area);
+
+    queue!(std::io::stdout(), Print("\x1b[r")).ok();

Same here?