fix: long lines incorrectly wrapped (#1710)

fix to #1685.
This commit is contained in:
Jeremy Rose
2025-07-28 12:19:03 -07:00
committed by GitHub
parent 80c19ea77c
commit 2d2df891bb

View File

@@ -1,7 +1,9 @@
use std::fmt;
use std::io; use std::io;
use std::io::Write; use std::io::Write;
use crate::tui; use crate::tui;
use crossterm::Command;
use crossterm::queue; use crossterm::queue;
use crossterm::style::Color as CColor; use crossterm::style::Color as CColor;
use crossterm::style::Colors; use crossterm::style::Colors;
@@ -11,46 +13,127 @@ use crossterm::style::SetBackgroundColor;
use crossterm::style::SetColors; use crossterm::style::SetColors;
use crossterm::style::SetForegroundColor; use crossterm::style::SetForegroundColor;
use ratatui::layout::Position; use ratatui::layout::Position;
use ratatui::layout::Size;
use ratatui::prelude::Backend; use ratatui::prelude::Backend;
use ratatui::style::Color; use ratatui::style::Color;
use ratatui::style::Modifier; use ratatui::style::Modifier;
use ratatui::text::Line; use ratatui::text::Line;
use ratatui::text::Span; use ratatui::text::Span;
/// Insert `lines` above the viewport.
pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec<Line<'static>>) { pub(crate) fn insert_history_lines(terminal: &mut tui::Tui, lines: Vec<Line<'static>>) {
let screen_height = terminal let screen_size = terminal.backend().size().unwrap_or(Size::new(0, 0));
.backend()
.size()
.map(|s| s.height)
.unwrap_or(0xffffu16);
let mut area = terminal.get_frame().area(); 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 let wrapped_lines = wrapped_line_count(&lines, area.width);
// lines.len() > screen_height - area.top() let cursor_top = if area.bottom() < screen_size.height {
// we would need to print the first line above the top of the screen, which // If the viewport is not at the bottom of the screen, scroll it down to make room.
// can't be done. // Don't scroll it past the bottom of the screen.
for line in lines.into_iter() { let scroll_amount = wrapped_lines.min(screen_size.height - area.bottom());
// 1. Scroll everything above the viewport up by one line terminal
if area.bottom() >= screen_height { .backend_mut()
let top = area.top(); .scroll_region_down(area.top()..screen_size.height, scroll_amount)
terminal.backend_mut().scroll_region_up(0..top, 1).ok(); .ok();
// 2. Move the cursor to the blank line let cursor_top = area.top() - 1;
terminal.set_cursor_position(Position::new(0, top - 1)).ok(); area.y += scroll_amount;
} else { terminal.set_viewport_area(area);
// If the viewport isn't at the bottom of the screen, scroll down instead cursor_top
terminal } else {
.backend_mut() area.top() - 1
.scroll_region_down(area.top()..area.bottom() + 1, 1) };
.ok();
terminal // Limit the scroll region to the lines from the top of the screen to the
.set_cursor_position(Position::new(0, area.top())) // top of the viewport. With this in place, when we add lines inside this
.ok(); // area, only the lines in this area will be scrolled. We place the cursor
area.y += 1; // at the end of the scroll region, and add lines starting there.
} //
// 3. Write the line // ┌─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(); 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 { struct ModifierDiff {