Skip to content

Commit 2d2df89

Browse files
fix: long lines incorrectly wrapped (#1710)
fix to #1685.
1 parent 80c19ea commit 2d2df89

File tree

1 file changed

+113
-30
lines changed

1 file changed

+113
-30
lines changed

codex-rs/tui/src/insert_history.rs

Lines changed: 113 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
use std::fmt;
12
use std::io;
23
use std::io::Write;
34

45
use crate::tui;
6+
use crossterm::Command;
57
use crossterm::queue;
68
use crossterm::style::Color as CColor;
79
use crossterm::style::Colors;
@@ -11,46 +13,127 @@ use crossterm::style::SetBackgroundColor;
1113
use crossterm::style::SetColors;
1214
use crossterm::style::SetForegroundColor;
1315
use ratatui::layout::Position;
16+
use ratatui::layout::Size;
1417
use ratatui::prelude::Backend;
1518
use ratatui::style::Color;
1619
use ratatui::style::Modifier;
1720
use ratatui::text::Line;
1821
use ratatui::text::Span;
1922

23+
/// Insert `lines` above the viewport.
2024
pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec<Line<'static>>) {
21-
let screen_height = terminal
22-
.backend()
23-
.size()
24-
.map(|s| s.height)
25-
.unwrap_or(0xffffu16);
25+
let screen_size = terminal.backend().size().unwrap_or(Size::new(0, 0));
26+
2627
let mut area = terminal.get_frame().area();
27-
// We scroll up one line at a time because we can't position the cursor
28-
// above the top of the screen. i.e. if
29-
// lines.len() > screen_height - area.top()
30-
// we would need to print the first line above the top of the screen, which
31-
// can't be done.
32-
for line in lines.into_iter() {
33-
// 1. Scroll everything above the viewport up by one line
34-
if area.bottom() >= screen_height {
35-
let top = area.top();
36-
terminal.backend_mut().scroll_region_up(0..top, 1).ok();
37-
// 2. Move the cursor to the blank line
38-
terminal.set_cursor_position(Position::new(0, top - 1)).ok();
39-
} else {
40-
// If the viewport isn't at the bottom of the screen, scroll down instead
41-
terminal
42-
.backend_mut()
43-
.scroll_region_down(area.top()..area.bottom() + 1, 1)
44-
.ok();
45-
terminal
46-
.set_cursor_position(Position::new(0, area.top()))
47-
.ok();
48-
area.y += 1;
49-
}
50-
// 3. Write the line
28+
29+
let wrapped_lines = wrapped_line_count(&lines, area.width);
30+
let cursor_top = if area.bottom() < screen_size.height {
31+
// If the viewport is not at the bottom of the screen, scroll it down to make room.
32+
// Don't scroll it past the bottom of the screen.
33+
let scroll_amount = wrapped_lines.min(screen_size.height - area.bottom());
34+
terminal
35+
.backend_mut()
36+
.scroll_region_down(area.top()..screen_size.height, scroll_amount)
37+
.ok();
38+
let cursor_top = area.top() - 1;
39+
area.y += scroll_amount;
40+
terminal.set_viewport_area(area);
41+
cursor_top
42+
} else {
43+
area.top() - 1
44+
};
45+
46+
// Limit the scroll region to the lines from the top of the screen to the
47+
// top of the viewport. With this in place, when we add lines inside this
48+
// area, only the lines in this area will be scrolled. We place the cursor
49+
// at the end of the scroll region, and add lines starting there.
50+
//
51+
// ┌─Screen───────────────────────┐
52+
// │┌╌Scroll region╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐│
53+
// │┆ ┆│
54+
// │┆ ┆│
55+
// │┆ ┆│
56+
// │█╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘│
57+
// │╭─Viewport───────────────────╮│
58+
// ││ ││
59+
// │╰────────────────────────────╯│
60+
// └──────────────────────────────┘
61+
queue!(std::io::stdout(), SetScrollRegion(1..area.top())).ok();
62+
63+
terminal
64+
.set_cursor_position(Position::new(0, cursor_top))
65+
.ok();
66+
67+
for line in lines {
68+
queue!(std::io::stdout(), Print("\r\n")).ok();
5169
write_spans(&mut std::io::stdout(), line.iter()).ok();
5270
}
53-
terminal.set_viewport_area(area);
71+
72+
queue!(std::io::stdout(), ResetScrollRegion).ok();
73+
}
74+
75+
fn wrapped_line_count(lines: &[Line], width: u16) -> u16 {
76+
let mut count = 0;
77+
for line in lines {
78+
count += line_height(line, width);
79+
}
80+
count
81+
}
82+
83+
fn line_height(line: &Line, width: u16) -> u16 {
84+
use unicode_width::UnicodeWidthStr;
85+
// get the total display width of the line, accounting for double-width chars
86+
let total_width = line
87+
.spans
88+
.iter()
89+
.map(|span| span.content.width())
90+
.sum::<usize>();
91+
// divide by width to get the number of lines, rounding up
92+
if width == 0 {
93+
1
94+
} else {
95+
(total_width as u16).div_ceil(width).max(1)
96+
}
97+
}
98+
99+
#[derive(Debug, Clone, PartialEq, Eq)]
100+
pub struct SetScrollRegion(pub std::ops::Range<u16>);
101+
102+
impl Command for SetScrollRegion {
103+
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
104+
write!(f, "\x1b[{};{}r", self.0.start, self.0.end)
105+
}
106+
107+
#[cfg(windows)]
108+
fn execute_winapi(&self) -> std::io::Result<()> {
109+
panic!("tried to execute SetScrollRegion command using WinAPI, use ANSI instead");
110+
}
111+
112+
#[cfg(windows)]
113+
fn is_ansi_code_supported(&self) -> bool {
114+
// TODO(nornagon): is this supported on Windows?
115+
true
116+
}
117+
}
118+
119+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
120+
pub struct ResetScrollRegion;
121+
122+
impl Command for ResetScrollRegion {
123+
fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result {
124+
write!(f, "\x1b[r")
125+
}
126+
127+
#[cfg(windows)]
128+
fn execute_winapi(&self) -> std::io::Result<()> {
129+
panic!("tried to execute ResetScrollRegion command using WinAPI, use ANSI instead");
130+
}
131+
132+
#[cfg(windows)]
133+
fn is_ansi_code_supported(&self) -> bool {
134+
// TODO(nornagon): is this supported on Windows?
135+
true
136+
}
54137
}
55138

56139
struct ModifierDiff {

0 commit comments

Comments
 (0)