Skip to content

Commit

Permalink
fix: line clipping and overlapping (#3291)
Browse files Browse the repository at this point in the history
  • Loading branch information
jkelleyrtp authored Dec 5, 2024
1 parent d738eff commit 0974550
Show file tree
Hide file tree
Showing 3 changed files with 70 additions and 87 deletions.
11 changes: 5 additions & 6 deletions packages/cli/src/serve/ansi_buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@ use std::fmt::{self, Display, Formatter};
///
/// This is taken from a PR on the ratatui repo (https://github.com/ratatui/ratatui/pull/1065) and
/// modified to be more appropriate for our use case.
pub struct AnsiStringBuffer {
pub struct AnsiStringLine {
buf: Buffer,
}

// The sentinel character used to mark the end of the ansi string so when we dump it, we know where to stop
// Not sure if we actually still need this....
const SENTINEL: &str = "✆";

impl AnsiStringBuffer {
impl AnsiStringLine {
/// Creates a new `AnsiStringBuffer` with the given width and height.
pub(crate) fn new(width: u16, height: u16) -> Self {
pub(crate) fn new(width: u16) -> Self {
Self {
buf: Buffer::empty(Rect::new(0, 0, width, height)),
buf: Buffer::empty(Rect::new(0, 0, width, 1)),
}
}

Expand Down Expand Up @@ -50,7 +50,6 @@ impl AnsiStringBuffer {
for x in 0..self.buf.area.width {
let cell = self.buf.cell((x, y)).unwrap();
if cell.symbol() == SENTINEL {
f.write_str("\n")?;
break;
}

Expand All @@ -66,7 +65,7 @@ impl AnsiStringBuffer {
}
}

impl Display for AnsiStringBuffer {
impl Display for AnsiStringLine {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
self.write_fmt(f)
}
Expand Down
138 changes: 58 additions & 80 deletions packages/cli/src/serve/output.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
serve::{ansi_buffer::AnsiStringBuffer, Builder, ServeUpdate, Watcher, WebServer},
serve::{ansi_buffer::AnsiStringLine, Builder, ServeUpdate, Watcher, WebServer},
BuildStage, BuildUpdate, DioxusCrate, Platform, ServeArgs, TraceContent, TraceMsg, TraceSrc,
};
use crossterm::{
Expand Down Expand Up @@ -767,10 +767,9 @@ impl Output {

// Render the log into an ansi string
// We're going to add some metadata to it like the timestamp and source and then dump it to the raw ansi sequences we need to send to crossterm
let output_sequence = Self::tracemsg_to_ansi_string(log, term_size.width);
let lines = Self::tracemsg_to_ansi_string(log);

// Get the lines of the output sequence and their overflow
let lines = output_sequence.lines().collect::<Vec<_>>();
let lines_printed = lines
.iter()
.map(|line| {
Expand All @@ -793,7 +792,7 @@ impl Output {
.saturating_sub(frame_rect.y + frame_rect.height);

// Calculate how many lines we need to push back
let to_push = max_scrollback.saturating_sub(remaining_space + 1);
let to_push = max_scrollback.saturating_sub(remaining_space);

// Wipe the viewport clean so it doesn't tear
crossterm::queue!(
Expand All @@ -807,7 +806,7 @@ impl Output {
// Ratatui will handle this rest.
// FIXME(jon): eventually insert_before will get scroll regions, breaking this, but making the logic here simpler
if to_push == 0 {
terminal.insert_before(lines_printed.saturating_sub(1), |_| {})?;
terminal.insert_before(lines_printed, |_| {})?;
}

// Start printing the log by writing on top of the topmost line
Expand Down Expand Up @@ -847,93 +846,72 @@ impl Output {
}
}

fn tracemsg_to_ansi_string(log: TraceMsg, term_width: u16) -> String {
fn tracemsg_to_ansi_string(log: TraceMsg) -> Vec<String> {
use ansi_to_tui::IntoText;
use chrono::Timelike;

let rendered = match log.content {
TraceContent::Cargo(msg) => msg.message.rendered.unwrap_or_default(),
TraceContent::Text(text) => text,
};

// Create a paragraph widget using the log line itself
// From here on out, we want to work with the escaped ansi string and the "real lines" to be printed
//
// We make a special case for lines that look like frames (ie ==== or ---- or ------) and make them
// dark gray, just for readability.
//
// todo(jon): refactor this out to accept any widget, not just paragraphs
let paragraph = Paragraph::new({
use ansi_to_tui::IntoText;
use chrono::Timelike;

let mut text = Text::default();

for (idx, raw_line) in rendered.lines().enumerate() {
let line_as_text = raw_line.into_text().unwrap();
let is_pretending_to_be_frame = raw_line
let mut lines = vec![];

for (idx, raw_line) in rendered.lines().enumerate() {
let line_as_text = raw_line.into_text().unwrap();
let is_pretending_to_be_frame = !raw_line.is_empty()
&& raw_line
.chars()
.all(|c| c == '=' || c == '-' || c == ' ' || c == '─');

for (subline_idx, line) in line_as_text.lines.into_iter().enumerate() {
let mut out_line = if idx == 0 && subline_idx == 0 {
let mut formatted_line = Line::default();

formatted_line.push_span(
Span::raw(format!(
"{:02}:{:02}:{:02} ",
log.timestamp.hour(),
log.timestamp.minute(),
log.timestamp.second()
))
.dark_gray(),
);
formatted_line.push_span(
Span::raw(format!(
"[{src}] {padding}",
src = log.source,
padding =
" ".repeat(3usize.saturating_sub(log.source.to_string().len()))
))
.style(match log.source {
TraceSrc::App(_platform) => Style::new().blue(),
TraceSrc::Dev => Style::new().magenta(),
TraceSrc::Build => Style::new().yellow(),
TraceSrc::Bundle => Style::new().magenta(),
TraceSrc::Cargo => Style::new().yellow(),
TraceSrc::Unknown => Style::new().gray(),
}),
);

for span in line.spans {
formatted_line.push_span(span);
}

formatted_line
} else {
line
};

if is_pretending_to_be_frame {
out_line = out_line.dark_gray();
for (subline_idx, mut line) in line_as_text.lines.into_iter().enumerate() {
if idx == 0 && subline_idx == 0 {
let mut formatted_line = Line::default();

formatted_line.push_span(
Span::raw(format!(
"{:02}:{:02}:{:02} ",
log.timestamp.hour(),
log.timestamp.minute(),
log.timestamp.second()
))
.dark_gray(),
);

formatted_line.push_span(
Span::raw(format!(
"[{src}] {padding}",
src = log.source,
padding =
" ".repeat(3usize.saturating_sub(log.source.to_string().len()))
))
.style(match log.source {
TraceSrc::App(_platform) => Style::new().blue(),
TraceSrc::Dev => Style::new().magenta(),
TraceSrc::Build => Style::new().yellow(),
TraceSrc::Bundle => Style::new().magenta(),
TraceSrc::Cargo => Style::new().yellow(),
TraceSrc::Unknown => Style::new().gray(),
}),
);

for span in line.spans {
formatted_line.push_span(span);
}

text.lines.push(out_line);
line = formatted_line;
}
}

text
});
if is_pretending_to_be_frame {
line = line.dark_gray();
}

let line_length: usize = line.spans.iter().map(|f| f.content.len()).sum();

lines.push(AnsiStringLine::new(line_length.max(100) as _).render(&line));
}
}

// We want to get the escaped ansii string and then by dumping the paragraph as ascii codes (again)
//
// This is important because the line_count method on paragraph takes into account the width of these codes
// the 3000 clip width is to bound log lines to a reasonable memory usage
// We could consider reusing this buffer since it's a lot to allocate, but log printing is not the
// slowest thing in the world and allocating is pretty fast...
//
// After we've dumped the ascii out, we want to call "trim_end" which ensures we don't attempt
// to print extra characters as lines, since AnsiStringBuffer will in fact attempt to print empty
// cells as characters. That might not actually be important, but we want to shrink the buffer
// before printing it
let line_count = paragraph.line_count(term_width);
AnsiStringBuffer::new(3000, line_count as u16).render(&paragraph)
lines
}
}
8 changes: 7 additions & 1 deletion packages/const-serialize/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ impl_serialize_const_tuple!(T1: 0, T2: 1, T3: 2, T4: 3, T5: 4, T6: 5, T7: 6, T8:
const MAX_STR_SIZE: usize = 256;

/// A string that is stored in a constant sized buffer that can be serialized and deserialized at compile time
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy, Hash)]
#[derive(PartialEq, PartialOrd, Clone, Copy, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct ConstStr {
#[cfg_attr(feature = "serde", serde(with = "serde_bytes"))]
Expand Down Expand Up @@ -455,6 +455,12 @@ impl ConstStr {
}
}

impl std::fmt::Debug for ConstStr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.as_str())
}
}

#[test]
fn test_rsplit_once() {
let str = ConstStr::new("hello world");
Expand Down

0 comments on commit 0974550

Please sign in to comment.