feat(terminal): port render state, highlights and the formatter
- render.rs ports render.zig: RenderState, the per-frame snapshot the renderer consumes (rows, cells, styles, graphemes, selection, highlights, cursor, colors), updated incrementally with Zig's full redraw triggers and per-page/per-row dirty tracking. Cells are stored struct-of-arrays; use the accessors. - highlight.rs ports highlight.zig (untracked, tracked, flattened). - formatter.rs ports formatter.zig: plain, VT and HTML output for terminal, screen, page list and page, with selection, rectangle, unwrap, trim, palette, codepoint maps, hyperlinks and pin/point maps. Screen's test dump helpers now delegate to it. Cargo.toml also picks up dependencies for the kitty graphics and search modules that are being ported concurrently. Test Plan: - cargo test -p ghostty-terminal --lib (1243 passed; formatter 100/100, render 15/15 Zig tests)
This commit is contained in:
1 parent
f43c0cd00f
commit
6a374b6953
13 files changed
+5510
-356
No files matched your search
@@ -0,0 +1,53 @@
|
||||
//! Ported formatter.zig tests, split by Zig source line range:
|
||||
//! p1 = "Page plain ..." (Zig lines 1580-3287), p2 = "Page VT ...", "PageList ..."
|
||||
//! and "TerminalFormatter ..." (3288-4458), p3 = "Screen ...", "Terminal vt ...",
|
||||
//! "Page html ..." up to "Page VT style reset properly closes styles"
|
||||
//! (4459-5617), p4 = "Page codepoint_map ..." onwards (5618-end).
|
||||
#![allow(dead_code, clippy::needless_range_loop)]
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
page::Page,
|
||||
page_list::Pin,
|
||||
point::{Coordinate, Point},
|
||||
terminal::{Options as TermOptions, Terminal},
|
||||
};
|
||||
|
||||
mod p1;
|
||||
mod p2;
|
||||
mod p3;
|
||||
mod p4;
|
||||
|
||||
/// Zig `Terminal.init(alloc, .{ .cols, .rows })`.
|
||||
pub(super) fn term(cols: u16, rows: u16) -> Terminal {
|
||||
Terminal::new(TermOptions::new(cols, rows)).unwrap()
|
||||
}
|
||||
|
||||
/// Zig `var s = t.vtStream(); try s.nextSlice(bytes)` (fresh stream per call;
|
||||
/// use `t.vt_stream()` directly if parser state must persist).
|
||||
pub(super) fn feed(t: &mut Terminal, bytes: &[u8]) {
|
||||
let mut s = t.vt_stream();
|
||||
s.next_slice(bytes).unwrap();
|
||||
}
|
||||
|
||||
/// Zig `&pages.pages.last.?.data`.
|
||||
pub(super) fn last_page(t: &Terminal) -> &Page {
|
||||
let pages = &t.screen().pages;
|
||||
&pages.node(pages.last.unwrap()).unwrap().data
|
||||
}
|
||||
|
||||
/// Zig `&pages.pages.first.?.data`.
|
||||
pub(super) fn first_page(t: &Terminal) -> &Page {
|
||||
let pages = &t.screen().pages;
|
||||
&pages.node(pages.first.unwrap()).unwrap().data
|
||||
}
|
||||
|
||||
/// Zig `pages.pages.first.?.data.capacity.rows`.
|
||||
pub(super) fn first_page_rows(t: &Terminal) -> u16 {
|
||||
first_page(t).capacity.rows
|
||||
}
|
||||
|
||||
/// Format a whole `PageFormatter` into a string, returning the output.
|
||||
pub(super) fn fmt_str(f: PageFormatter<'_>) -> String {
|
||||
f.format_string().unwrap()
|
||||
}
|
||||
@@ -0,0 +1,589 @@
|
||||
//! "Page plain ..." tests (formatter.zig lines 1580-3287).
|
||||
use super::*;
|
||||
|
||||
struct Out {
|
||||
s: String,
|
||||
st: TrailingState,
|
||||
map: Vec<Coordinate>,
|
||||
}
|
||||
|
||||
/// Runs a PageFormatter over the last page with a point map attached.
|
||||
fn run(t: &Terminal, opts: &Options, cfg: impl FnOnce(&mut PageFormatter<'_>)) -> Out {
|
||||
let page = last_page(t);
|
||||
let mut map = Vec::new();
|
||||
let mut buf = Vec::new();
|
||||
let st = {
|
||||
let mut f = PageFormatter::new(page, opts);
|
||||
f.point_map = Some(&mut map);
|
||||
cfg(&mut f);
|
||||
f.format_with_state(&mut buf).unwrap()
|
||||
};
|
||||
Out {
|
||||
s: String::from_utf8(buf).unwrap(),
|
||||
st,
|
||||
map,
|
||||
}
|
||||
}
|
||||
|
||||
fn c(x: u16, y: u32) -> Coordinate {
|
||||
Coordinate { x, y }
|
||||
}
|
||||
|
||||
/// `n` consecutive cells on row `y` starting at column `x0`.
|
||||
fn span(x0: u16, n: u16, y: u32) -> Vec<Coordinate> {
|
||||
(0..n).map(|i| c(x0 + i, y)).collect()
|
||||
}
|
||||
|
||||
fn setup(cols: u16, rows: u16, input: &str) -> Terminal {
|
||||
let mut t = term(cols, rows);
|
||||
feed(&mut t, input.as_bytes());
|
||||
// Verify we have only a single page
|
||||
let pages = &t.screen().pages;
|
||||
assert!(pages.first.is_some());
|
||||
assert_eq!(pages.first, pages.last);
|
||||
t
|
||||
}
|
||||
|
||||
fn unwrap_opts() -> Options {
|
||||
Options {
|
||||
unwrap: true,
|
||||
..Options::plain()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_single_line() {
|
||||
let t = setup(80, 24, "hello, world");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |_| {});
|
||||
assert_eq!(o.s, "hello, world");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 12));
|
||||
assert_eq!(o.map, span(0, 12, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_single_line_soft_wrapped_unwrapped() {
|
||||
let t = setup(3, 5, "hello!");
|
||||
let o = run(&t, &unwrap_opts(), |_| {});
|
||||
assert_eq!(o.s, "hello!");
|
||||
let mut e = span(0, 3, 0);
|
||||
e.extend(span(0, 3, 1));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_single_wide_char() {
|
||||
let t = setup(80, 24, "1A⚡");
|
||||
let page = last_page(&t);
|
||||
let opts = Options::plain();
|
||||
|
||||
// Full string
|
||||
let o = run(&t, &opts, |_| {});
|
||||
assert_eq!(o.s, "1A⚡");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 4));
|
||||
assert_eq!(o.map.len(), o.s.len());
|
||||
for i in 2..o.s.len() {
|
||||
assert_eq!(o.map[i], c(2, 0));
|
||||
}
|
||||
|
||||
// Wide only (from start)
|
||||
let o = run(&t, &opts, |f| f.start_x = 2);
|
||||
assert_eq!(o.s, "⚡");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 4));
|
||||
assert_eq!(o.map, vec![c(2, 0); o.s.len()]);
|
||||
|
||||
// Wide only (from tail)
|
||||
let o = run(&t, &opts, |f| f.start_x = 3);
|
||||
assert_eq!(o.s, "⚡");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 4));
|
||||
assert_eq!(o.map, vec![c(2, 0); o.s.len()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_single_wide_char_soft_wrapped_unwrapped() {
|
||||
let t = setup(3, 24, "1A⚡");
|
||||
let page = last_page(&t);
|
||||
let opts = unwrap_opts();
|
||||
|
||||
// Full string
|
||||
let o = run(&t, &opts, |_| {});
|
||||
assert_eq!(o.s, "1A⚡");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows - 1));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 2));
|
||||
assert_eq!(o.map.len(), o.s.len());
|
||||
for i in 2..o.s.len() {
|
||||
assert_eq!(o.map[i], c(0, 1));
|
||||
}
|
||||
|
||||
// Full string (ending on spacer head)
|
||||
let o = run(&t, &opts, |f| {
|
||||
f.end_x = Some(2);
|
||||
f.end_y = Some(0);
|
||||
});
|
||||
assert_eq!(o.s, "1A⚡");
|
||||
assert_eq!(o.map.len(), o.s.len());
|
||||
for i in 2..o.s.len() {
|
||||
assert_eq!(o.map[i], c(0, 1));
|
||||
}
|
||||
|
||||
// Wide only (from start)
|
||||
let o = run(&t, &opts, |f| f.start_x = 2);
|
||||
assert_eq!(o.s, "⚡");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows - 1));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 2));
|
||||
assert_eq!(o.map, vec![c(0, 1); o.s.len()]);
|
||||
|
||||
// Wide only (from tail). In Zig the formatter carried start_x = 2 over
|
||||
// from the previous block; start_y = 1, start_x = 1.
|
||||
let o = run(&t, &opts, |f| {
|
||||
f.start_y = 1;
|
||||
f.start_x = 1;
|
||||
});
|
||||
assert_eq!(o.s, "⚡");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows - 1));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 2));
|
||||
assert_eq!(o.map, vec![c(0, 1); o.s.len()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_multiline() {
|
||||
let t = setup(80, 24, "hello\r\nworld");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |_| {});
|
||||
assert_eq!(o.s, "hello\nworld");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows - 1));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 5));
|
||||
let mut e = span(0, 5, 0);
|
||||
e.push(c(4, 0)); // \n
|
||||
e.extend(span(0, 5, 1));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_multiline_rectangle() {
|
||||
let t = setup(80, 24, "hello\r\nworld");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |f| {
|
||||
f.start_x = 1;
|
||||
f.end_x = Some(3);
|
||||
f.rectangle = true;
|
||||
});
|
||||
assert_eq!(o.s, "ell\norl");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows - 1));
|
||||
assert_eq!(o.st.cells, 0);
|
||||
let mut e = span(1, 3, 0);
|
||||
e.push(c(3, 0)); // \n
|
||||
e.extend(span(1, 3, 1));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_multi_blank_lines() {
|
||||
let t = setup(80, 24, "hello\r\n\r\n\r\nworld");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |_| {});
|
||||
assert_eq!(o.s, "hello\n\n\nworld");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows - 3));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 5));
|
||||
let mut e = span(0, 5, 0);
|
||||
e.push(c(4, 0)); // \n after row 0
|
||||
e.push(c(0, 1)); // \n after blank row 1
|
||||
e.push(c(0, 2)); // \n after blank row 2
|
||||
e.extend(span(0, 5, 3));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_trailing_blank_lines() {
|
||||
let t = setup(80, 24, "hello\r\nworld\r\n\r\n");
|
||||
let page = last_page(&t);
|
||||
// We expect there to be no trailing newlines because we can't
|
||||
// differentiate trailing blank lines as being meaningful because the
|
||||
// page formatter can't see the cursor position.
|
||||
let o = run(&t, &Options::plain(), |_| {});
|
||||
assert_eq!(o.s, "hello\nworld");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows - 1));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 5));
|
||||
let mut e = span(0, 5, 0);
|
||||
e.push(c(4, 0));
|
||||
e.extend(span(0, 5, 1));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_trailing_whitespace() {
|
||||
let t = setup(80, 24, "hello \r\nworld ");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |_| {});
|
||||
assert_eq!(o.s, "hello\nworld");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows - 1));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 5));
|
||||
let mut e = span(0, 5, 0);
|
||||
e.push(c(4, 0));
|
||||
e.extend(span(0, 5, 1));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_trailing_whitespace_no_trim() {
|
||||
let t = setup(80, 24, "hello \r\nworld ");
|
||||
let page = last_page(&t);
|
||||
let opts = Options {
|
||||
trim: false,
|
||||
..Options::plain()
|
||||
};
|
||||
let o = run(&t, &opts, |_| {});
|
||||
assert_eq!(o.s, "hello \nworld ");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows - 1));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 7));
|
||||
let mut e = span(0, 8, 0);
|
||||
e.push(c(7, 0));
|
||||
e.extend(span(0, 7, 1));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_with_prior_trailing_state_rows() {
|
||||
let t = setup(80, 24, "hello");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |f| {
|
||||
f.trailing_state = Some(TrailingState { rows: 2, cells: 0 });
|
||||
});
|
||||
assert_eq!(o.s, "\n\nhello");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 5));
|
||||
let mut e = vec![c(0, 0), c(0, 1)];
|
||||
e.extend(span(0, 5, 0));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_with_prior_trailing_state_cells_no_wrapped_line() {
|
||||
let t = setup(80, 24, "hello");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |f| {
|
||||
f.trailing_state = Some(TrailingState { rows: 0, cells: 3 });
|
||||
});
|
||||
// Blank cells are reset when row is not a wrap continuation
|
||||
assert_eq!(o.s, "hello");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 5));
|
||||
assert_eq!(o.map, span(0, 5, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_with_prior_trailing_state_cells_with_wrap_continuation() {
|
||||
let mut t = setup(80, 24, "world");
|
||||
// Surgically modify the first row to be a wrap continuation
|
||||
{
|
||||
let pages = &mut t.screen_mut().pages;
|
||||
let id = pages.last.unwrap();
|
||||
pages
|
||||
.node_mut(id)
|
||||
.unwrap()
|
||||
.data
|
||||
.get_row_mut(0)
|
||||
.unwrap()
|
||||
.set_wrap_continuation(true);
|
||||
}
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &unwrap_opts(), |f| {
|
||||
f.trailing_state = Some(TrailingState { rows: 0, cells: 3 });
|
||||
});
|
||||
// Blank cells are preserved when row is a wrap continuation with unwrap enabled
|
||||
assert_eq!(o.s, " world");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 5));
|
||||
// 3 spaces from prior trailing state (can't go back beyond (0,0)) + "world"
|
||||
let mut e = vec![c(0, 0); 3];
|
||||
e.extend(span(0, 5, 0));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_soft_wrapped_without_unwrap() {
|
||||
let t = setup(10, 24, "hello world test");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |_| {});
|
||||
// Without unwrap, wrapped lines show as separate lines
|
||||
assert_eq!(o.s, "hello worl\nd test");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows - 1));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 6));
|
||||
let mut e = span(0, 10, 0);
|
||||
e.push(c(9, 0)); // \n
|
||||
e.extend(span(0, 6, 1));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_soft_wrapped_with_unwrap() {
|
||||
let t = setup(10, 24, "hello world test");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &unwrap_opts(), |_| {});
|
||||
// With unwrap, wrapped lines are joined together
|
||||
assert_eq!(o.s, "hello world test");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows - 1));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 6));
|
||||
let mut e = span(0, 10, 0);
|
||||
e.extend(span(0, 6, 1));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_soft_wrapped_3_lines_without_unwrap() {
|
||||
let t = setup(10, 24, "hello world this is a test");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |_| {});
|
||||
assert_eq!(o.s, "hello worl\nd this is\na test");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows - 2));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 6));
|
||||
let mut e = span(0, 10, 0);
|
||||
e.push(c(9, 0)); // \n
|
||||
e.extend(span(0, 9, 1));
|
||||
e.push(c(8, 1)); // \n
|
||||
e.extend(span(0, 6, 2));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_soft_wrapped_3_lines_with_unwrap() {
|
||||
let t = setup(10, 24, "hello world this is a test");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &unwrap_opts(), |_| {});
|
||||
assert_eq!(o.s, "hello world this is a test");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows - 2));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 6));
|
||||
let mut e = span(0, 10, 0);
|
||||
e.extend(span(0, 10, 1));
|
||||
e.extend(span(0, 6, 2));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_start_y_subset() {
|
||||
let t = setup(80, 24, "hello\r\nworld\r\ntest");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |f| f.start_y = 1);
|
||||
assert_eq!(o.s, "world\ntest");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows - 2));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 4));
|
||||
let mut e = span(0, 5, 1);
|
||||
e.push(c(4, 1)); // \n
|
||||
e.extend(span(0, 4, 2));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_end_y_subset() {
|
||||
let t = setup(80, 24, "hello\r\nworld\r\ntest");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |f| f.end_y = Some(1));
|
||||
assert_eq!(o.s, "hello\nworld");
|
||||
assert_eq!(o.st.rows, 1);
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 5));
|
||||
let mut e = span(0, 5, 0);
|
||||
e.push(c(4, 0)); // \n
|
||||
e.extend(span(0, 5, 1));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_start_y_and_end_y_range() {
|
||||
let t = setup(80, 24, "hello\r\nworld\r\ntest\r\nfoo");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |f| {
|
||||
f.start_y = 1;
|
||||
f.end_y = Some(2);
|
||||
});
|
||||
assert_eq!(o.s, "world\ntest");
|
||||
assert_eq!(o.st.rows, 1);
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 4));
|
||||
let mut e = span(0, 5, 1);
|
||||
e.push(c(4, 1)); // \n
|
||||
e.extend(span(0, 4, 2));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_start_y_out_of_bounds() {
|
||||
let t = setup(80, 24, "hello");
|
||||
let o = run(&t, &Options::plain(), |f| f.start_y = 30);
|
||||
assert_eq!(o.s, "");
|
||||
assert_eq!(o.st.rows, 0);
|
||||
assert_eq!(o.st.cells, 0);
|
||||
assert_eq!(o.map.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_end_y_greater_than_rows() {
|
||||
let t = setup(80, 24, "hello");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |f| f.end_y = Some(30));
|
||||
// Should clamp to page.size.rows and work normally
|
||||
assert_eq!(o.s, "hello");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 5));
|
||||
assert_eq!(o.map, span(0, 5, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_end_y_less_than_start_y() {
|
||||
let t = setup(80, 24, "hello");
|
||||
let o = run(&t, &Options::plain(), |f| {
|
||||
f.start_y = 5;
|
||||
f.end_y = Some(2);
|
||||
});
|
||||
assert_eq!(o.s, "");
|
||||
assert_eq!(o.st.rows, 0);
|
||||
assert_eq!(o.st.cells, 0);
|
||||
assert_eq!(o.map.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_start_x_on_first_row_only() {
|
||||
let t = setup(80, 24, "hello world");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |f| f.start_x = 6);
|
||||
assert_eq!(o.s, "world");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 11));
|
||||
assert_eq!(o.map, span(6, 5, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_end_x_on_last_row_only() {
|
||||
let t = setup(80, 24, "first line\r\nsecond line\r\nthird line");
|
||||
let o = run(&t, &Options::plain(), |f| {
|
||||
f.end_y = Some(2);
|
||||
f.end_x = Some(4);
|
||||
});
|
||||
assert_eq!(o.s, "first line\nsecond line\nthird");
|
||||
assert_eq!(o.st.rows, 1);
|
||||
assert_eq!(o.st.cells, 0);
|
||||
let mut e = span(0, 10, 0);
|
||||
e.push(c(9, 0)); // \n
|
||||
e.extend(span(0, 11, 1));
|
||||
e.push(c(10, 1)); // \n
|
||||
e.extend(span(0, 5, 2));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_start_x_and_end_x_multiline() {
|
||||
let t = setup(80, 24, "hello world\r\ntest case\r\nfoo bar");
|
||||
let o = run(&t, &Options::plain(), |f| {
|
||||
f.start_x = 6;
|
||||
f.end_y = Some(2);
|
||||
f.end_x = Some(2);
|
||||
});
|
||||
// First row: "world" (start_x=6 to end of row)
|
||||
// Second row: "test case" (full row)
|
||||
// Third row: "foo" (start to end_x=2, inclusive)
|
||||
assert_eq!(o.s, "world\ntest case\nfoo");
|
||||
assert_eq!(o.st.rows, 1);
|
||||
assert_eq!(o.st.cells, 0);
|
||||
let mut e = span(6, 5, 0);
|
||||
e.push(c(10, 0)); // \n
|
||||
e.extend(span(0, 9, 1));
|
||||
e.push(c(8, 1)); // \n
|
||||
e.extend(span(0, 3, 2));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_start_x_out_of_bounds() {
|
||||
let t = setup(80, 24, "hello");
|
||||
let o = run(&t, &Options::plain(), |f| f.start_x = 100);
|
||||
assert_eq!(o.s, "");
|
||||
assert_eq!(o.st.rows, 0);
|
||||
assert_eq!(o.st.cells, 0);
|
||||
assert_eq!(o.map.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_end_x_greater_than_cols() {
|
||||
let t = setup(80, 24, "hello");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |f| f.end_x = Some(100));
|
||||
assert_eq!(o.s, "hello");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 5));
|
||||
assert_eq!(o.map, span(0, 5, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_end_x_less_than_start_x_single_row() {
|
||||
let t = setup(80, 24, "hello");
|
||||
let o = run(&t, &Options::plain(), |f| {
|
||||
f.start_x = 10;
|
||||
f.end_y = Some(0);
|
||||
f.end_x = Some(5);
|
||||
});
|
||||
assert_eq!(o.s, "");
|
||||
assert_eq!(o.st.rows, 0);
|
||||
assert_eq!(o.st.cells, 0);
|
||||
assert_eq!(o.map.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_start_y_non_zero_ignores_trailing_state() {
|
||||
let t = setup(80, 24, "hello\r\nworld");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |f| {
|
||||
f.start_y = 1;
|
||||
f.trailing_state = Some(TrailingState { rows: 5, cells: 10 });
|
||||
});
|
||||
// Should NOT output the 5 newlines from trailing_state because start_y is non-zero
|
||||
assert_eq!(o.s, "world");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows - 1));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 5));
|
||||
assert_eq!(o.map, span(0, 5, 1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_start_x_non_zero_ignores_trailing_state() {
|
||||
let t = setup(80, 24, "hello world");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |f| {
|
||||
f.start_x = 6;
|
||||
f.trailing_state = Some(TrailingState { rows: 2, cells: 8 });
|
||||
});
|
||||
// Should NOT output the 2 newlines or 8 spaces from trailing_state
|
||||
assert_eq!(o.s, "world");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 11));
|
||||
assert_eq!(o.map, span(6, 5, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_start_y_and_start_x_zero_uses_trailing_state() {
|
||||
let t = setup(80, 24, "hello");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |f| {
|
||||
f.start_y = 0;
|
||||
f.start_x = 0;
|
||||
f.trailing_state = Some(TrailingState { rows: 2, cells: 0 });
|
||||
});
|
||||
// SHOULD output the 2 newlines from trailing_state
|
||||
assert_eq!(o.s, "\n\nhello");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 5));
|
||||
let mut e = vec![c(0, 0), c(0, 1)];
|
||||
e.extend(span(0, 5, 0));
|
||||
assert_eq!(o.map, e);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_plain_single_line_with_styling() {
|
||||
let t = setup(80, 24, "hello, \x1b[1mworld\x1b[0m");
|
||||
let page = last_page(&t);
|
||||
let o = run(&t, &Options::plain(), |_| {});
|
||||
assert_eq!(o.s, "hello, world");
|
||||
assert_eq!(o.st.rows, usize::from(page.size.rows));
|
||||
assert_eq!(o.st.cells, usize::from(page.size.cols - 12));
|
||||
assert_eq!(o.map, span(0, 12, 0));
|
||||
}
|
||||
@@ -0,0 +1,686 @@
|
||||
use super::*;
|
||||
|
||||
/// Format the last page of `t` with a page formatter and a point map.
|
||||
fn page_fmt(t: &Terminal, opts: &Options) -> (String, Vec<Coordinate>) {
|
||||
let mut point_map = Vec::new();
|
||||
let mut f = PageFormatter::new(last_page(t), opts);
|
||||
f.point_map = Some(&mut point_map);
|
||||
let out = f.format_string().unwrap();
|
||||
(out, point_map)
|
||||
}
|
||||
|
||||
fn trim_start<'a>(s: &'a str, chars: &[char]) -> &'a str {
|
||||
s.trim_start_matches(chars)
|
||||
}
|
||||
|
||||
fn coord(x: u16, y: u32) -> Coordinate {
|
||||
Coordinate { x, y }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_vt_single_line_plain_text() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello");
|
||||
let (output, point_map) = page_fmt(&t, &Options::vt());
|
||||
assert_eq!(output, "hello");
|
||||
assert_eq!(point_map.len(), output.len());
|
||||
for i in 0..5 {
|
||||
assert_eq!(point_map[i], coord(i as u16, 0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_vt_single_line_with_bold() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b[1mhello\x1b[0m");
|
||||
let (output, point_map) = page_fmt(&t, &Options::vt());
|
||||
assert_eq!(output, "\x1b[0m\x1b[1mhello\x1b[0m");
|
||||
assert_eq!(point_map.len(), output.len());
|
||||
for i in 0..8 {
|
||||
assert_eq!(point_map[i], coord(0, 0));
|
||||
}
|
||||
for i in 0..5 {
|
||||
assert_eq!(point_map[8 + i], coord(i as u16, 0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_vt_multiple_styles() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b[1mhello \x1b[3mworld\x1b[0m");
|
||||
let (output, point_map) = page_fmt(&t, &Options::vt());
|
||||
assert_eq!(
|
||||
output,
|
||||
"\x1b[0m\x1b[1mhello \x1b[0m\x1b[1m\x1b[3mworld\x1b[0m"
|
||||
);
|
||||
assert_eq!(point_map.len(), output.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_vt_with_foreground_color() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b[31mred\x1b[0m");
|
||||
let (output, point_map) = page_fmt(&t, &Options::vt());
|
||||
assert_eq!(output, "\x1b[0m\x1b[38;5;1mred\x1b[0m");
|
||||
assert_eq!(point_map.len(), output.len());
|
||||
for i in 0..13 {
|
||||
assert_eq!(point_map[i], coord(0, 0));
|
||||
}
|
||||
for i in 0..3 {
|
||||
assert_eq!(point_map[13 + i], coord(i as u16, 0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_vt_with_background_and_foreground_colors() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello");
|
||||
let mut opts = Options::vt();
|
||||
opts.background = Some(Rgb {
|
||||
r: 0x12,
|
||||
g: 0x34,
|
||||
b: 0x56,
|
||||
});
|
||||
opts.foreground = Some(Rgb {
|
||||
r: 0xab,
|
||||
g: 0xcd,
|
||||
b: 0xef,
|
||||
});
|
||||
let output = fmt_str(PageFormatter::new(last_page(&t), &opts));
|
||||
assert_eq!(
|
||||
output,
|
||||
"\x1b]10;rgb:ab/cd/ef\x1b\\\x1b]11;rgb:12/34/56\x1b\\hello"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_vt_multi_line_with_styles() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b[1mfirst\x1b[0m\r\n\x1b[3msecond\x1b[0m");
|
||||
let (output, point_map) = page_fmt(&t, &Options::vt());
|
||||
// Note: style is reset before newline to prevent background colors from
|
||||
// bleeding to the next line's leading cells.
|
||||
assert_eq!(
|
||||
output,
|
||||
"\x1b[0m\x1b[1mfirst\x1b[0m\r\n\x1b[0m\x1b[3msecond\x1b[0m"
|
||||
);
|
||||
assert_eq!(point_map.len(), output.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_vt_duplicate_style_not_emitted_twice() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b[1mhel\x1b[1mlo\x1b[0m");
|
||||
let (output, point_map) = page_fmt(&t, &Options::vt());
|
||||
assert_eq!(output, "\x1b[0m\x1b[1mhello\x1b[0m");
|
||||
assert_eq!(point_map.len(), output.len());
|
||||
}
|
||||
|
||||
fn node_of_first(t: &Terminal) -> crate::page_list::NodeId {
|
||||
t.screen().pages.first.unwrap()
|
||||
}
|
||||
|
||||
fn node_of_last(t: &Terminal) -> crate::page_list::NodeId {
|
||||
t.screen().pages.last.unwrap()
|
||||
}
|
||||
|
||||
fn list_fmt(t: &Terminal, opts: Options) -> (String, Vec<Pin>) {
|
||||
let mut pin_map = Vec::new();
|
||||
let mut f = PageListFormatter::new(&t.screen().pages, opts);
|
||||
f.pin_map = Some(&mut pin_map);
|
||||
let out = f.format_string().unwrap();
|
||||
(out, pin_map)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagelist_plain_single_line() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello, world");
|
||||
let (output, pin_map) = list_fmt(&t, Options::plain());
|
||||
assert_eq!(output, "hello, world");
|
||||
assert_eq!(pin_map.len(), output.len());
|
||||
let node = node_of_first(&t);
|
||||
for i in 0..output.len() {
|
||||
assert_eq!(
|
||||
pin_map[i],
|
||||
Pin {
|
||||
node,
|
||||
x: i as u16,
|
||||
y: 0,
|
||||
garbage: false
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagelist_plain_spanning_two_pages() {
|
||||
let mut t = term(80, 24);
|
||||
let first_page_rows = first_page_rows(&t);
|
||||
|
||||
// Fill the first page almost completely
|
||||
for _ in 0..first_page_rows - 1 {
|
||||
feed(&mut t, b"\r\n");
|
||||
}
|
||||
feed(&mut t, b"page one");
|
||||
|
||||
// Verify we're still on one page
|
||||
assert_eq!(t.screen().pages.first, t.screen().pages.last);
|
||||
|
||||
// Add one more newline to push content to a second page
|
||||
feed(&mut t, b"\r\n");
|
||||
assert_ne!(t.screen().pages.first, t.screen().pages.last);
|
||||
|
||||
// Write content on the second page
|
||||
feed(&mut t, b"page two");
|
||||
|
||||
let (full_output, pin_map) = list_fmt(&t, Options::plain());
|
||||
let output = trim_start(&full_output, &['\n']);
|
||||
assert_eq!(output, "page one\npage two");
|
||||
|
||||
assert_eq!(pin_map.len(), full_output.len());
|
||||
let first_node = node_of_first(&t);
|
||||
let last_node = node_of_last(&t);
|
||||
let trimmed_count = full_output.len() - output.len();
|
||||
|
||||
// First part (trimmed blank lines) maps to first node
|
||||
for p in &pin_map[..trimmed_count] {
|
||||
assert_eq!(p.node, first_node);
|
||||
}
|
||||
|
||||
// "page one" (8 chars) maps to first node
|
||||
for i in 0..8 {
|
||||
let idx = trimmed_count + i;
|
||||
assert_eq!(pin_map[idx].node, first_node);
|
||||
assert_eq!(pin_map[idx].x, i as u16);
|
||||
}
|
||||
|
||||
// \n - maps to last node as it represents the transition to new page
|
||||
assert_eq!(pin_map[trimmed_count + 8].node, last_node);
|
||||
|
||||
// "page two" (8 chars) maps to last node
|
||||
for i in 0..8 {
|
||||
let idx = trimmed_count + 9 + i;
|
||||
assert_eq!(pin_map[idx].node, last_node);
|
||||
assert_eq!(pin_map[idx].x, i as u16);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagelist_soft_wrapped_line_spanning_two_pages_without_unwrap() {
|
||||
let mut t = term(10, 3);
|
||||
let first_page_rows = first_page_rows(&t);
|
||||
|
||||
// Fill the first page with soft-wrapped content
|
||||
for _ in 0..first_page_rows - 1 {
|
||||
feed(&mut t, b"\r\n");
|
||||
}
|
||||
feed(&mut t, b"hello world test");
|
||||
|
||||
// Verify we're on two pages due to wrapping
|
||||
assert_ne!(t.screen().pages.first, t.screen().pages.last);
|
||||
|
||||
let (full_output, pin_map) = list_fmt(&t, Options::plain());
|
||||
let output = trim_start(&full_output, &['\n']);
|
||||
assert_eq!(output, "hello worl\nd test");
|
||||
|
||||
assert_eq!(pin_map.len(), full_output.len());
|
||||
let first_node = node_of_first(&t);
|
||||
let last_node = node_of_last(&t);
|
||||
let trimmed_count = full_output.len() - output.len();
|
||||
|
||||
for p in &pin_map[..trimmed_count] {
|
||||
assert_eq!(p.node, first_node);
|
||||
}
|
||||
for i in 0..10 {
|
||||
assert_eq!(pin_map[trimmed_count + i].node, first_node);
|
||||
}
|
||||
|
||||
// \n - maps to last node as it represents the transition to new page
|
||||
assert_eq!(pin_map[trimmed_count + 10].node, last_node);
|
||||
|
||||
// "d test" (6 chars) maps to last node
|
||||
for i in 0..6 {
|
||||
assert_eq!(pin_map[trimmed_count + 11 + i].node, last_node);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagelist_soft_wrapped_line_spanning_two_pages_with_unwrap() {
|
||||
let mut t = term(10, 3);
|
||||
let first_page_rows = first_page_rows(&t);
|
||||
|
||||
for _ in 0..first_page_rows - 1 {
|
||||
feed(&mut t, b"\r\n");
|
||||
}
|
||||
feed(&mut t, b"hello world test");
|
||||
|
||||
assert_ne!(t.screen().pages.first, t.screen().pages.last);
|
||||
|
||||
let mut opts = Options::plain();
|
||||
opts.unwrap = true;
|
||||
let (full_output, pin_map) = list_fmt(&t, opts);
|
||||
let output = trim_start(&full_output, &['\r', '\n']);
|
||||
assert_eq!(output, "hello world test");
|
||||
|
||||
assert_eq!(pin_map.len(), full_output.len());
|
||||
let first_node = node_of_first(&t);
|
||||
let last_node = node_of_last(&t);
|
||||
let trimmed_count = full_output.len() - output.len();
|
||||
|
||||
for p in &pin_map[..trimmed_count] {
|
||||
assert_eq!(p.node, first_node);
|
||||
}
|
||||
for i in 0..10 {
|
||||
assert_eq!(pin_map[trimmed_count + i].node, first_node);
|
||||
}
|
||||
for i in 0..6 {
|
||||
assert_eq!(pin_map[trimmed_count + 10 + i].node, last_node);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagelist_vt_spanning_two_pages() {
|
||||
let mut t = term(80, 24);
|
||||
let first_page_rows = first_page_rows(&t);
|
||||
|
||||
// Fill the first page almost completely
|
||||
for _ in 0..first_page_rows - 1 {
|
||||
feed(&mut t, b"\r\n");
|
||||
}
|
||||
feed(&mut t, b"\x1b[1mpage one");
|
||||
|
||||
assert_eq!(t.screen().pages.first, t.screen().pages.last);
|
||||
|
||||
feed(&mut t, b"\r\n");
|
||||
assert_ne!(t.screen().pages.first, t.screen().pages.last);
|
||||
|
||||
// New content is still styled
|
||||
feed(&mut t, b"page two");
|
||||
|
||||
let (full_output, pin_map) = list_fmt(&t, Options::vt());
|
||||
let output = trim_start(&full_output, &['\r', '\n']);
|
||||
assert_eq!(
|
||||
output,
|
||||
"\x1b[0m\x1b[1mpage one\x1b[0m\r\n\x1b[0m\x1b[1mpage two\x1b[0m"
|
||||
);
|
||||
|
||||
assert_eq!(pin_map.len(), full_output.len());
|
||||
let first_node = node_of_first(&t);
|
||||
let last_node = node_of_last(&t);
|
||||
|
||||
let first_count = pin_map.iter().filter(|p| p.node == first_node).count();
|
||||
let last_count = pin_map.iter().filter(|p| p.node == last_node).count();
|
||||
assert!(first_count > 0);
|
||||
assert!(last_count > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagelist_plain_with_x_offset_on_single_page() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello world\r\ntest case\r\nfoo bar");
|
||||
|
||||
let node = node_of_first(&t);
|
||||
let mut pin_map = Vec::new();
|
||||
let mut f = PageListFormatter::new(&t.screen().pages, Options::plain());
|
||||
f.top_left = Some(Pin {
|
||||
node,
|
||||
y: 0,
|
||||
x: 6,
|
||||
garbage: false,
|
||||
});
|
||||
f.bottom_right = Some(Pin {
|
||||
node,
|
||||
y: 2,
|
||||
x: 2,
|
||||
garbage: false,
|
||||
});
|
||||
f.pin_map = Some(&mut pin_map);
|
||||
let output = f.format_string().unwrap();
|
||||
assert_eq!(output, "world\ntest case\nfoo");
|
||||
|
||||
assert_eq!(pin_map.len(), output.len());
|
||||
for p in &pin_map {
|
||||
assert_eq!(p.node, node);
|
||||
}
|
||||
|
||||
// "world" starts at x=6, y=0
|
||||
for i in 0..5 {
|
||||
assert_eq!(pin_map[i].x, 6 + i as u16);
|
||||
assert_eq!(pin_map[i].y, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagelist_plain_with_x_offset_spanning_two_pages() {
|
||||
let mut t = term(80, 24);
|
||||
let first_page_rows = first_page_rows(&t);
|
||||
|
||||
for _ in 0..first_page_rows - 1 {
|
||||
feed(&mut t, b"\r\n");
|
||||
}
|
||||
feed(&mut t, b"hello world");
|
||||
|
||||
assert_eq!(t.screen().pages.first, t.screen().pages.last);
|
||||
|
||||
feed(&mut t, b"\r\n");
|
||||
assert_ne!(t.screen().pages.first, t.screen().pages.last);
|
||||
|
||||
feed(&mut t, b"foo bar test");
|
||||
|
||||
let first_node = node_of_first(&t);
|
||||
let last_node = node_of_last(&t);
|
||||
let first_rows = t.screen().pages.node(first_node).unwrap().data.size.rows;
|
||||
|
||||
let mut pin_map = Vec::new();
|
||||
let mut f = PageListFormatter::new(&t.screen().pages, Options::plain());
|
||||
f.top_left = Some(Pin {
|
||||
node: first_node,
|
||||
y: first_rows - 1,
|
||||
x: 6,
|
||||
garbage: false,
|
||||
});
|
||||
f.bottom_right = Some(Pin {
|
||||
node: last_node,
|
||||
y: 1,
|
||||
x: 2,
|
||||
garbage: false,
|
||||
});
|
||||
f.pin_map = Some(&mut pin_map);
|
||||
let full_output = f.format_string().unwrap();
|
||||
let output = trim_start(&full_output, &['\n']);
|
||||
assert_eq!(output, "world\nfoo");
|
||||
|
||||
assert_eq!(pin_map.len(), full_output.len());
|
||||
let trimmed_count = full_output.len() - output.len();
|
||||
|
||||
// "world" (5 chars) from first page
|
||||
for i in 0..5 {
|
||||
let idx = trimmed_count + i;
|
||||
assert_eq!(pin_map[idx].node, first_node);
|
||||
assert_eq!(pin_map[idx].x, 6 + i as u16);
|
||||
}
|
||||
|
||||
// \n - maps to last node as it represents the transition to new page
|
||||
assert_eq!(pin_map[trimmed_count + 5].node, last_node);
|
||||
|
||||
// "foo" (3 chars) from last page
|
||||
for i in 0..3 {
|
||||
let idx = trimmed_count + 6 + i;
|
||||
assert_eq!(pin_map[idx].node, last_node);
|
||||
assert_eq!(pin_map[idx].x, i as u16);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagelist_plain_with_start_x_only() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello world");
|
||||
|
||||
let node = node_of_first(&t);
|
||||
let mut pin_map = Vec::new();
|
||||
let mut f = PageListFormatter::new(&t.screen().pages, Options::plain());
|
||||
f.top_left = Some(Pin {
|
||||
node,
|
||||
y: 0,
|
||||
x: 6,
|
||||
garbage: false,
|
||||
});
|
||||
f.pin_map = Some(&mut pin_map);
|
||||
let output = f.format_string().unwrap();
|
||||
assert_eq!(output, "world");
|
||||
|
||||
assert_eq!(pin_map.len(), output.len());
|
||||
for i in 0..5 {
|
||||
assert_eq!(pin_map[i].node, node);
|
||||
assert_eq!(pin_map[i].x, 6 + i as u16);
|
||||
assert_eq!(pin_map[i].y, 0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagelist_plain_with_end_x_only() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello world\r\ntest");
|
||||
|
||||
let node = node_of_first(&t);
|
||||
let mut pin_map = Vec::new();
|
||||
let mut f = PageListFormatter::new(&t.screen().pages, Options::plain());
|
||||
f.bottom_right = Some(Pin {
|
||||
node,
|
||||
y: 1,
|
||||
x: 2,
|
||||
garbage: false,
|
||||
});
|
||||
f.pin_map = Some(&mut pin_map);
|
||||
let output = f.format_string().unwrap();
|
||||
assert_eq!(output, "hello world\ntes");
|
||||
|
||||
assert_eq!(pin_map.len(), output.len());
|
||||
|
||||
for i in 0..11 {
|
||||
assert_eq!(pin_map[i].node, node);
|
||||
assert_eq!(pin_map[i].x, i as u16);
|
||||
assert_eq!(pin_map[i].y, 0);
|
||||
}
|
||||
|
||||
// \n
|
||||
assert_eq!(pin_map[11].node, node);
|
||||
|
||||
for i in 0..3 {
|
||||
assert_eq!(pin_map[12 + i].node, node);
|
||||
assert_eq!(pin_map[12 + i].x, i as u16);
|
||||
assert_eq!(pin_map[12 + i].y, 1);
|
||||
}
|
||||
}
|
||||
|
||||
fn rect_format(t: &Terminal, tl: (u16, u32), br: (u16, u32)) -> String {
|
||||
let pages = &t.screen().pages;
|
||||
let mut f = PageListFormatter::new(pages, Options::plain());
|
||||
f.top_left = Some(
|
||||
pages
|
||||
.pin(Point::Screen(Coordinate { x: tl.0, y: tl.1 }))
|
||||
.unwrap(),
|
||||
);
|
||||
f.bottom_right = Some(
|
||||
pages
|
||||
.pin(Point::Screen(Coordinate { x: br.0, y: br.1 }))
|
||||
.unwrap(),
|
||||
);
|
||||
f.rectangle = true;
|
||||
f.format_string().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagelist_plain_rectangle_basic() {
|
||||
let mut t = term(30, 5);
|
||||
feed(&mut t, b"Lorem ipsum dolor\r\n");
|
||||
feed(&mut t, b"sit amet, consectetur\r\n");
|
||||
feed(&mut t, b"adipiscing elit, sed do\r\n");
|
||||
feed(&mut t, b"eiusmod tempor incididunt\r\n");
|
||||
feed(&mut t, b"ut labore et dolore");
|
||||
|
||||
let output = rect_format(&t, (2, 1), (6, 3));
|
||||
assert_eq!(output, "t ame\nipisc\nusmod");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagelist_plain_rectangle_with_eol() {
|
||||
let mut t = term(30, 5);
|
||||
feed(&mut t, b"Lorem ipsum dolor\r\n");
|
||||
feed(&mut t, b"sit amet, consectetur\r\n");
|
||||
feed(&mut t, b"adipiscing elit, sed do\r\n");
|
||||
feed(&mut t, b"eiusmod tempor incididunt\r\n");
|
||||
feed(&mut t, b"ut labore et dolore");
|
||||
|
||||
let output = rect_format(&t, (12, 0), (26, 4));
|
||||
assert_eq!(
|
||||
output,
|
||||
"dolor\nnsectetur\nlit, sed do\nor incididunt\n dolore"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pagelist_plain_rectangle_more_complex_with_breaks() {
|
||||
let mut t = term(30, 8);
|
||||
feed(&mut t, b"Lorem ipsum dolor\r\n");
|
||||
feed(&mut t, b"sit amet, consectetur\r\n");
|
||||
feed(&mut t, b"adipiscing elit, sed do\r\n");
|
||||
feed(&mut t, b"eiusmod tempor incididunt\r\n");
|
||||
feed(&mut t, b"ut labore et dolore\r\n");
|
||||
feed(&mut t, b"\r\n");
|
||||
feed(&mut t, b"magna aliqua. Ut enim\r\n");
|
||||
feed(&mut t, b"ad minim veniam, quis");
|
||||
|
||||
let output = rect_format(&t, (11, 2), (26, 7));
|
||||
assert_eq!(
|
||||
output,
|
||||
"elit, sed do\npor incididunt\nt dolore\n\na. Ut enim\nniam, quis"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_formatter_plain_no_selection() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello\r\nworld");
|
||||
let f = TerminalFormatter::new(&t, Options::plain());
|
||||
assert_eq!(f.format_string().unwrap(), "hello\nworld");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_formatter_vt_with_palette() {
|
||||
let mut t = term(80, 24);
|
||||
|
||||
// Modify some palette colors using VT sequences
|
||||
feed(&mut t, b"\x1b]4;0;rgb:12/34/56\x1b\\");
|
||||
feed(&mut t, b"\x1b]4;1;rgb:ab/cd/ef\x1b\\");
|
||||
feed(&mut t, b"\x1b]4;255;rgb:ff/00/ff\x1b\\");
|
||||
feed(&mut t, b"test");
|
||||
|
||||
let output = TerminalFormatter::new(&t, Options::vt())
|
||||
.format_string()
|
||||
.unwrap();
|
||||
|
||||
// Create a second terminal and apply the output
|
||||
let mut t2 = term(80, 24);
|
||||
feed(&mut t2, output.as_bytes());
|
||||
|
||||
// Verify the palettes match
|
||||
for i in [0, 1, 255] {
|
||||
assert_eq!(t.colors.palette.current[i], t2.colors.palette.current[i]);
|
||||
}
|
||||
}
|
||||
|
||||
fn line2_selection(t: &Terminal) -> Selection {
|
||||
let pages = &t.screen().pages;
|
||||
Selection::new(
|
||||
pages.pin(Point::Active(Coordinate { x: 0, y: 1 })).unwrap(),
|
||||
pages.pin(Point::Active(Coordinate { x: 4, y: 1 })).unwrap(),
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_formatter_with_selection() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"line1\r\nline2\r\nline3");
|
||||
let sel = line2_selection(&t);
|
||||
let mut f = TerminalFormatter::new(&t, Options::plain());
|
||||
f.content = Content::Selection(Some(sel));
|
||||
assert_eq!(f.format_string().unwrap(), "line2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_formatter_plain_with_pin_map() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello, world");
|
||||
|
||||
let mut pin_map = Vec::new();
|
||||
let mut f = TerminalFormatter::new(&t, Options::plain());
|
||||
f.pin_map = Some(&mut pin_map);
|
||||
let output = f.format_string().unwrap();
|
||||
assert_eq!(output, "hello, world");
|
||||
|
||||
assert_eq!(pin_map.len(), output.len());
|
||||
let node = node_of_first(&t);
|
||||
for i in 0..output.len() {
|
||||
assert_eq!(
|
||||
pin_map[i],
|
||||
Pin {
|
||||
node,
|
||||
x: i as u16,
|
||||
y: 0,
|
||||
garbage: false
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_formatter_plain_multiline_with_pin_map() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello\r\nworld");
|
||||
|
||||
let mut pin_map = Vec::new();
|
||||
let mut f = TerminalFormatter::new(&t, Options::plain());
|
||||
f.pin_map = Some(&mut pin_map);
|
||||
let output = f.format_string().unwrap();
|
||||
assert_eq!(output, "hello\nworld");
|
||||
|
||||
assert_eq!(pin_map.len(), output.len());
|
||||
let node = node_of_first(&t);
|
||||
for i in 0..5 {
|
||||
assert_eq!(pin_map[i].node, node);
|
||||
assert_eq!(pin_map[i].x, i as u16);
|
||||
assert_eq!(pin_map[i].y, 0);
|
||||
}
|
||||
// "\n" maps to end of first line
|
||||
assert_eq!(pin_map[5].node, node);
|
||||
for i in 0..5 {
|
||||
let idx = 6 + i;
|
||||
assert_eq!(pin_map[idx].node, node);
|
||||
assert_eq!(pin_map[idx].x, i as u16);
|
||||
assert_eq!(pin_map[idx].y, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_formatter_vt_with_palette_and_pin_map() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b]4;0;rgb:12/34/56\x1b\\");
|
||||
feed(&mut t, b"test");
|
||||
|
||||
let mut pin_map = Vec::new();
|
||||
let mut f = TerminalFormatter::new(&t, Options::vt());
|
||||
f.pin_map = Some(&mut pin_map);
|
||||
let output = f.format_string().unwrap();
|
||||
|
||||
// Palette bytes should be mapped to top left
|
||||
assert_eq!(pin_map.len(), output.len());
|
||||
let node = node_of_first(&t);
|
||||
for p in &pin_map {
|
||||
assert_eq!(p.node, node);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_formatter_with_selection_and_pin_map() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"line1\r\nline2\r\nline3");
|
||||
let sel = line2_selection(&t);
|
||||
|
||||
let mut pin_map = Vec::new();
|
||||
let mut f = TerminalFormatter::new(&t, Options::plain());
|
||||
f.content = Content::Selection(Some(sel));
|
||||
f.pin_map = Some(&mut pin_map);
|
||||
let output = f.format_string().unwrap();
|
||||
assert_eq!(output, "line2");
|
||||
|
||||
assert_eq!(pin_map.len(), output.len());
|
||||
let node = node_of_first(&t);
|
||||
for i in 0..5 {
|
||||
assert_eq!(pin_map[i].node, node);
|
||||
assert_eq!(pin_map[i].x, i as u16);
|
||||
assert_eq!(pin_map[i].y, 1);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,530 @@
|
||||
use super::*;
|
||||
use ghostty_vt::modes::Mode;
|
||||
|
||||
/// Format the screen with the given extras, returning output and pin map.
|
||||
fn screen_vt(t: &Terminal, mutate: impl FnOnce(&mut ScreenFormatter<'_>)) -> (String, Vec<Pin>) {
|
||||
let mut pin_map: Vec<Pin> = Vec::new();
|
||||
let out = {
|
||||
let mut f = ScreenFormatter::new(t.screen(), Options::vt());
|
||||
mutate(&mut f);
|
||||
f.pin_map = Some(&mut pin_map);
|
||||
f.format_string().unwrap()
|
||||
};
|
||||
(out, pin_map)
|
||||
}
|
||||
|
||||
fn replay(output: &str) -> Terminal {
|
||||
let mut t2 = term(80, 24);
|
||||
feed(&mut t2, output.as_bytes());
|
||||
t2
|
||||
}
|
||||
|
||||
fn first_node(t: &Terminal) -> crate::page_list::NodeId {
|
||||
t.screen().pages.first.unwrap()
|
||||
}
|
||||
|
||||
fn page_fmt(t: &Terminal, opts: &Options) -> String {
|
||||
PageFormatter::new(last_page(t), opts)
|
||||
.format_string()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
const HTML_OPEN: &str = "<div style=\"font-family: monospace; white-space: pre;\">";
|
||||
|
||||
#[test]
|
||||
fn screen_plain_single_line() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello, world");
|
||||
|
||||
let mut pin_map = Vec::new();
|
||||
let output = {
|
||||
let mut f = ScreenFormatter::new(t.screen(), Options::plain());
|
||||
f.pin_map = Some(&mut pin_map);
|
||||
f.format_string().unwrap()
|
||||
};
|
||||
assert_eq!(output, "hello, world");
|
||||
|
||||
assert_eq!(output.len(), pin_map.len());
|
||||
let node = first_node(&t);
|
||||
for (i, p) in pin_map.iter().enumerate() {
|
||||
assert_eq!(
|
||||
*p,
|
||||
Pin {
|
||||
node,
|
||||
x: i as u16,
|
||||
y: 0,
|
||||
garbage: false
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_plain_multiline() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello\r\nworld");
|
||||
|
||||
let mut pin_map = Vec::new();
|
||||
let output = {
|
||||
let mut f = ScreenFormatter::new(t.screen(), Options::plain());
|
||||
f.pin_map = Some(&mut pin_map);
|
||||
f.format_string().unwrap()
|
||||
};
|
||||
assert_eq!(output, "hello\nworld");
|
||||
|
||||
assert_eq!(output.len(), pin_map.len());
|
||||
let node = first_node(&t);
|
||||
for i in 0..5 {
|
||||
assert_eq!(pin_map[i].node, node);
|
||||
assert_eq!(pin_map[i].x, i as u16);
|
||||
assert_eq!(pin_map[i].y, 0);
|
||||
}
|
||||
assert_eq!(pin_map[5].node, node);
|
||||
for i in 0..5 {
|
||||
let idx = 6 + i;
|
||||
assert_eq!(pin_map[idx].node, node);
|
||||
assert_eq!(pin_map[idx].x, i as u16);
|
||||
assert_eq!(pin_map[idx].y, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_plain_with_selection() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"line1\r\nline2\r\nline3");
|
||||
|
||||
let pages = &t.screen().pages;
|
||||
let a = pages.pin(Point::Active(Coordinate { x: 0, y: 1 })).unwrap();
|
||||
let b = pages.pin(Point::Active(Coordinate { x: 4, y: 1 })).unwrap();
|
||||
|
||||
let mut pin_map = Vec::new();
|
||||
let output = {
|
||||
let mut f = ScreenFormatter::new(t.screen(), Options::plain());
|
||||
f.content = Content::Selection(Some(Selection::new(a, b, false)));
|
||||
f.pin_map = Some(&mut pin_map);
|
||||
f.format_string().unwrap()
|
||||
};
|
||||
assert_eq!(output, "line2");
|
||||
|
||||
assert_eq!(output.len(), pin_map.len());
|
||||
let node = first_node(&t);
|
||||
for i in 0..5 {
|
||||
assert_eq!(pin_map[i].node, node);
|
||||
assert_eq!(pin_map[i].x, i as u16);
|
||||
assert_eq!(pin_map[i].y, 1);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_vt_with_cursor_position() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello\r\nworld");
|
||||
|
||||
let (output, pin_map) = screen_vt(&t, |f| f.extra.cursor = true);
|
||||
let t2 = replay(&output);
|
||||
|
||||
assert_eq!(t.screen().cursor.x, t2.screen().cursor.x);
|
||||
assert_eq!(t.screen().cursor.y, t2.screen().cursor.y);
|
||||
|
||||
assert_eq!(output.len(), pin_map.len());
|
||||
let node = first_node(&t);
|
||||
for p in &pin_map {
|
||||
assert_eq!(p.node, node);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_vt_with_style() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b[1;31mhello");
|
||||
|
||||
let (output, pin_map) = screen_vt(&t, |f| f.extra.style = true);
|
||||
let t2 = replay(&output);
|
||||
|
||||
assert!(t.screen().cursor.style.eql(t2.screen().cursor.style));
|
||||
|
||||
assert_eq!(output.len(), pin_map.len());
|
||||
let node = first_node(&t);
|
||||
for p in &pin_map {
|
||||
assert_eq!(p.node, node);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_vt_with_hyperlink() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b]8;;http://example.com\x1b\\hello");
|
||||
|
||||
let (output, pin_map) = screen_vt(&t, |f| f.extra.hyperlink = true);
|
||||
let t2 = replay(&output);
|
||||
|
||||
let l1 = &t.screen().cursor.hyperlink;
|
||||
let l2 = &t2.screen().cursor.hyperlink;
|
||||
assert_eq!(l1.is_some(), l2.is_some());
|
||||
if let (Some(a), Some(b)) = (l1, l2) {
|
||||
assert_eq!(a.uri, b.uri);
|
||||
}
|
||||
|
||||
assert_eq!(output.len(), pin_map.len());
|
||||
let node = first_node(&t);
|
||||
for p in &pin_map {
|
||||
assert_eq!(p.node, node);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_vt_with_protection() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b[1\"qhello");
|
||||
|
||||
let (output, pin_map) = screen_vt(&t, |f| f.extra.protection = true);
|
||||
let t2 = replay(&output);
|
||||
|
||||
assert_eq!(t.screen().cursor.protected, t2.screen().cursor.protected);
|
||||
|
||||
assert_eq!(output.len(), pin_map.len());
|
||||
let node = first_node(&t);
|
||||
for p in &pin_map {
|
||||
assert_eq!(p.node, node);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_vt_with_kitty_keyboard() {
|
||||
let mut t = term(80, 24);
|
||||
// Set kitty keyboard flags (disambiguate + report_events = 3)
|
||||
feed(&mut t, b"\x1b[=3;1uhello");
|
||||
|
||||
let (output, pin_map) = screen_vt(&t, |f| f.extra.kitty_keyboard = true);
|
||||
let t2 = replay(&output);
|
||||
|
||||
let flags1 = t.screen().kitty_keyboard.current().int();
|
||||
let flags2 = t2.screen().kitty_keyboard.current().int();
|
||||
assert_eq!(flags1, flags2);
|
||||
|
||||
assert_eq!(output.len(), pin_map.len());
|
||||
let node = first_node(&t);
|
||||
for p in &pin_map {
|
||||
assert_eq!(p.node, node);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn screen_vt_with_charsets() {
|
||||
use ghostty_vt::charsets::Slots;
|
||||
let mut t = term(80, 24);
|
||||
// Set G0 to DEC special and shift to G1
|
||||
feed(&mut t, b"\x1b(0\x0ehello");
|
||||
|
||||
let (output, pin_map) = screen_vt(&t, |f| f.extra.charsets = true);
|
||||
let t2 = replay(&output);
|
||||
|
||||
let (c1, c2) = (&t.screen().charset, &t2.screen().charset);
|
||||
assert_eq!(c1.gl, c2.gl);
|
||||
assert_eq!(c1.gr, c2.gr);
|
||||
assert_eq!(c1.charsets.get(Slots::G0), c2.charsets.get(Slots::G0));
|
||||
|
||||
assert_eq!(output.len(), pin_map.len());
|
||||
let node = first_node(&t);
|
||||
for p in &pin_map {
|
||||
assert_eq!(p.node, node);
|
||||
}
|
||||
}
|
||||
|
||||
fn term_vt(t: &Terminal, mutate: impl FnOnce(&mut TerminalFormatter<'_>)) -> String {
|
||||
let mut f = TerminalFormatter::new(t, Options::vt());
|
||||
mutate(&mut f);
|
||||
f.format_string().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_vt_with_scrolling_region() {
|
||||
let mut t = term(80, 24);
|
||||
// Set scrolling region: top=5, bottom=20
|
||||
feed(&mut t, b"\x1b[6;21rhello");
|
||||
|
||||
let output = term_vt(&t, |f| f.extra.scrolling_region = true);
|
||||
let t2 = replay(&output);
|
||||
|
||||
assert_eq!(t.scrolling_region.top, t2.scrolling_region.top);
|
||||
assert_eq!(t.scrolling_region.bottom, t2.scrolling_region.bottom);
|
||||
assert_eq!(t.scrolling_region.left, t2.scrolling_region.left);
|
||||
assert_eq!(t.scrolling_region.right, t2.scrolling_region.right);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_vt_with_modes() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b[?2004h"); // Bracketed paste
|
||||
feed(&mut t, b"\x1b[?1000h"); // Mouse event normal
|
||||
feed(&mut t, b"\x1b[?7l"); // Disable wraparound (default is true)
|
||||
feed(&mut t, b"hello");
|
||||
|
||||
let output = term_vt(&t, |f| f.extra.modes = true);
|
||||
let t2 = replay(&output);
|
||||
|
||||
for m in [
|
||||
Mode::BracketedPaste,
|
||||
Mode::MouseEventNormal,
|
||||
Mode::Wraparound,
|
||||
] {
|
||||
assert_eq!(t.modes.get(m), t2.modes.get(m));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_vt_with_tabstops() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b[3g"); // Clear all tabs
|
||||
feed(&mut t, b"\x1b[5G\x1bH"); // Set tab at column 5
|
||||
feed(&mut t, b"\x1b[15G\x1bH"); // Set tab at column 15
|
||||
feed(&mut t, b"\x1b[30G\x1bH"); // Set tab at column 30
|
||||
feed(&mut t, b"hello");
|
||||
|
||||
let output = term_vt(&t, |f| f.extra.tabstops = true);
|
||||
let t2 = replay(&output);
|
||||
|
||||
for c in [4, 14, 29] {
|
||||
assert_eq!(t.tabstops.get(c), t2.tabstops.get(c));
|
||||
assert!(t2.tabstops.get(c));
|
||||
}
|
||||
assert!(!t2.tabstops.get(8)); // Not a tab
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_vt_with_keyboard_modes() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b[>4;2m");
|
||||
feed(&mut t, b"hello");
|
||||
|
||||
let output = term_vt(&t, |f| f.extra.keyboard = true);
|
||||
let t2 = replay(&output);
|
||||
|
||||
assert_eq!(t.flags.modify_other_keys_2, t2.flags.modify_other_keys_2);
|
||||
assert!(t2.flags.modify_other_keys_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_vt_with_pwd() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b]7;file://host/home/user\x1b\\hello");
|
||||
|
||||
let output = term_vt(&t, |f| f.extra.pwd = true);
|
||||
let t2 = replay(&output);
|
||||
|
||||
assert_eq!(t.pwd, t2.pwd);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_html_with_multiple_styles() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b[1mbold\x1b[3mitalic\x1b[0mnormal");
|
||||
|
||||
let output = page_fmt(&t, &Options::html());
|
||||
assert_eq!(
|
||||
output,
|
||||
"<div style=\"font-family: monospace; white-space: pre;\">\
|
||||
<div style=\"display: inline;font-weight: bold;\">bold</div>\
|
||||
<div style=\"display: inline;font-weight: bold;font-style: italic;\">italic</div>\
|
||||
normal\
|
||||
</div>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_html_plain_text() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello, world");
|
||||
|
||||
let output = page_fmt(&t, &Options::html());
|
||||
assert_eq!(output, format!("{HTML_OPEN}hello, world</div>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_html_with_colors() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b[31;44mcolored");
|
||||
|
||||
let output = page_fmt(&t, &Options::html());
|
||||
assert_eq!(
|
||||
output,
|
||||
"<div style=\"font-family: monospace; white-space: pre;\">\
|
||||
<div style=\"display: inline;color: var(--vt-palette-1);background-color: var(--vt-palette-4);\">colored</div>\
|
||||
</div>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn terminal_formatter_html_with_palette() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b]4;0;rgb:12/34/56\x1b\\");
|
||||
feed(&mut t, b"\x1b]4;1;rgb:ab/cd/ef\x1b\\");
|
||||
feed(&mut t, b"\x1b]4;255;rgb:ff/00/ff\x1b\\");
|
||||
feed(&mut t, b"test");
|
||||
|
||||
let mut f = TerminalFormatter::new(&t, Options::html());
|
||||
f.extra.palette = true;
|
||||
let output = f.format_string().unwrap();
|
||||
|
||||
assert!(output.contains("<style>:root{"));
|
||||
assert!(output.contains("--vt-palette-0: #123456;"));
|
||||
assert!(output.contains("--vt-palette-1: #abcdef;"));
|
||||
assert!(output.contains("--vt-palette-255: #ff00ff;"));
|
||||
assert!(output.contains("}</style>"));
|
||||
assert!(output.contains("test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_html_with_background_and_foreground_colors() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello");
|
||||
|
||||
let mut opts = Options::html();
|
||||
opts.background = Some(Rgb {
|
||||
r: 0x12,
|
||||
g: 0x34,
|
||||
b: 0x56,
|
||||
});
|
||||
opts.foreground = Some(Rgb {
|
||||
r: 0xab,
|
||||
g: 0xcd,
|
||||
b: 0xef,
|
||||
});
|
||||
let output = page_fmt(&t, &opts);
|
||||
assert_eq!(
|
||||
output,
|
||||
"<div style=\"font-family: monospace; white-space: pre;background-color: #123456;color: #abcdef;\">hello</div>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_html_with_escaping() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"<tag>&\"'text");
|
||||
|
||||
let opts = Options::html();
|
||||
let mut point_map: Vec<Coordinate> = Vec::new();
|
||||
let output = {
|
||||
let mut f = PageFormatter::new(last_page(&t), &opts);
|
||||
f.point_map = Some(&mut point_map);
|
||||
f.format_string().unwrap()
|
||||
};
|
||||
assert_eq!(
|
||||
output,
|
||||
format!("{HTML_OPEN}<tag>&"'text</div>")
|
||||
);
|
||||
|
||||
assert_eq!(output.len(), point_map.len());
|
||||
|
||||
let offset = HTML_OPEN.len();
|
||||
for c in &point_map[..offset] {
|
||||
assert_eq!(*c, Coordinate { x: 0, y: 0 });
|
||||
}
|
||||
|
||||
let at = |i: usize, x: u16| {
|
||||
assert_eq!(point_map[offset + i], Coordinate { x, y: 0 }, "index {i}");
|
||||
};
|
||||
for i in 0..4 {
|
||||
at(i, 0);
|
||||
}
|
||||
at(4, 1);
|
||||
at(5, 2);
|
||||
at(6, 3);
|
||||
for i in 0..4 {
|
||||
at(7 + i, 4);
|
||||
}
|
||||
for i in 0..5 {
|
||||
at(11 + i, 5);
|
||||
}
|
||||
for i in 0..6 {
|
||||
at(16 + i, 6);
|
||||
}
|
||||
for i in 0..5 {
|
||||
at(22 + i, 7);
|
||||
}
|
||||
at(27, 8);
|
||||
at(28, 9);
|
||||
at(29, 10);
|
||||
at(30, 11);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_html_with_unicode_as_numeric_entities() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, "╰─ ❯".as_bytes());
|
||||
|
||||
let output = page_fmt(&t, &Options::html());
|
||||
assert_eq!(output, format!("{HTML_OPEN}╰─ ❯</div>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_html_ascii_characters_unchanged() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello world");
|
||||
|
||||
let output = page_fmt(&t, &Options::html());
|
||||
assert_eq!(output, format!("{HTML_OPEN}hello world</div>"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_html_mixed_ascii_and_unicode() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, "test ╰─❯ ok".as_bytes());
|
||||
|
||||
let output = page_fmt(&t, &Options::html());
|
||||
assert_eq!(
|
||||
output,
|
||||
format!("{HTML_OPEN}test ╰─❯ ok</div>")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_vt_with_palette_option_emits_rgb() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b]4;1;rgb:ab/cd/ef\x1b\\");
|
||||
feed(&mut t, b"\x1b[31mred");
|
||||
|
||||
// Without palette option - should emit palette index
|
||||
let output = page_fmt(&t, &Options::vt());
|
||||
assert_eq!(output, "\x1b[0m\x1b[38;5;1mred\x1b[0m");
|
||||
|
||||
// With palette option - should emit RGB directly
|
||||
let mut opts = Options::vt();
|
||||
opts.palette = Some(t.colors.palette.current);
|
||||
let output = page_fmt(&t, &opts);
|
||||
assert_eq!(output, "\x1b[0m\x1b[38;2;171;205;239mred\x1b[0m");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_html_with_palette_option_emits_rgb() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b]4;1;rgb:ab/cd/ef\x1b\\");
|
||||
feed(&mut t, b"\x1b[31mred");
|
||||
|
||||
let output = page_fmt(&t, &Options::html());
|
||||
assert_eq!(
|
||||
output,
|
||||
"<div style=\"font-family: monospace; white-space: pre;\">\
|
||||
<div style=\"display: inline;color: var(--vt-palette-1);\">red</div>\
|
||||
</div>"
|
||||
);
|
||||
|
||||
let mut opts = Options::html();
|
||||
opts.palette = Some(t.colors.palette.current);
|
||||
let output = page_fmt(&t, &opts);
|
||||
assert_eq!(
|
||||
output,
|
||||
"<div style=\"font-family: monospace; white-space: pre;\">\
|
||||
<div style=\"display: inline;color: rgb(171, 205, 239);\">red</div>\
|
||||
</div>"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_vt_style_reset_properly_closes_styles() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"\x1b[1mbold\x1b[0mnormal");
|
||||
|
||||
let output = page_fmt(&t, &Options::vt());
|
||||
assert_eq!(output, "\x1b[0m\x1b[1mbold\x1b[0mnormal");
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
use super::*;
|
||||
|
||||
fn map_entry(range: (char, char), replacement: Replacement) -> CodepointMap {
|
||||
CodepointMap {
|
||||
range: [range.0 as u32, range.1 as u32],
|
||||
replacement,
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the last page of `t` with `opts`, optionally collecting a point map.
|
||||
fn run(t: &Terminal, opts: &Options, point_map: Option<&mut Vec<Coordinate>>) -> String {
|
||||
let mut f = PageFormatter::new(last_page(t), opts);
|
||||
f.point_map = point_map;
|
||||
f.format_string().unwrap()
|
||||
}
|
||||
|
||||
fn plain_with_map(entries: Vec<CodepointMap>) -> Options {
|
||||
let mut opts = Options::plain();
|
||||
opts.codepoint_map = Some(entries);
|
||||
opts
|
||||
}
|
||||
|
||||
fn c(x: u16, y: u32) -> Coordinate {
|
||||
Coordinate { x, y }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_codepoint_map_single_replacement() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello world");
|
||||
|
||||
// Replace 'o' with 'x'
|
||||
let opts = plain_with_map(vec![map_entry(
|
||||
('o', 'o'),
|
||||
Replacement::Codepoint('x' as u32),
|
||||
)]);
|
||||
let mut point_map = Vec::new();
|
||||
let output = run(&t, &opts, Some(&mut point_map));
|
||||
assert_eq!(output, "hellx wxrld");
|
||||
|
||||
// Verify point map - each output byte should map to original cell position
|
||||
assert_eq!(output.len(), point_map.len());
|
||||
for (i, coord) in point_map.iter().enumerate() {
|
||||
assert_eq!(*coord, c(i as u16, 0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_codepoint_map_conflicting_replacement_prefers_last() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello");
|
||||
|
||||
// Replace 'o' with 'x', then with 'y' - should prefer last
|
||||
let opts = plain_with_map(vec![
|
||||
map_entry(('o', 'o'), Replacement::Codepoint('x' as u32)),
|
||||
map_entry(('o', 'o'), Replacement::Codepoint('y' as u32)),
|
||||
]);
|
||||
assert_eq!(run(&t, &opts, None), "helly");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_codepoint_map_replace_with_string() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello");
|
||||
|
||||
// Replace 'o' with a multi-byte string
|
||||
let opts = plain_with_map(vec![map_entry(
|
||||
('o', 'o'),
|
||||
Replacement::String("XYZ".into()),
|
||||
)]);
|
||||
let mut point_map = Vec::new();
|
||||
let output = run(&t, &opts, Some(&mut point_map));
|
||||
assert_eq!(output, "hellXYZ");
|
||||
|
||||
// Verify point map - string replacements should all map to the original cell
|
||||
assert_eq!(output.len(), point_map.len());
|
||||
for i in 0..4 {
|
||||
assert_eq!(point_map[i], c(i as u16, 0));
|
||||
}
|
||||
// All bytes of the replacement string "XYZ" should point to position 4 (where 'o' was)
|
||||
assert_eq!(point_map[4], c(4, 0)); // X
|
||||
assert_eq!(point_map[5], c(4, 0)); // Y
|
||||
assert_eq!(point_map[6], c(4, 0)); // Z
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_codepoint_map_range_replacement() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"abcdefg");
|
||||
|
||||
// Replace 'b' through 'e' with 'X'
|
||||
let opts = plain_with_map(vec![map_entry(
|
||||
('b', 'e'),
|
||||
Replacement::Codepoint('X' as u32),
|
||||
)]);
|
||||
assert_eq!(run(&t, &opts, None), "aXXXXfg");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_codepoint_map_multiple_ranges() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello world");
|
||||
|
||||
// Replace 'a'-'m' with 'A' and 'n'-'z' with 'Z'
|
||||
let opts = plain_with_map(vec![
|
||||
map_entry(('a', 'm'), Replacement::Codepoint('A' as u32)),
|
||||
map_entry(('n', 'z'), Replacement::Codepoint('Z' as u32)),
|
||||
]);
|
||||
// h e l l o w o r l d
|
||||
// A A A A Z Z Z Z A A
|
||||
assert_eq!(run(&t, &opts, None), "AAAAZ ZZZAA");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_codepoint_map_unicode_replacement() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, "hello \u{26A1} world".as_bytes());
|
||||
|
||||
// Replace lightning bolt with fire emoji
|
||||
let opts = plain_with_map(vec![map_entry(
|
||||
('\u{26A1}', '\u{26A1}'),
|
||||
Replacement::String("\u{1F525}".into()),
|
||||
)]);
|
||||
let mut point_map = Vec::new();
|
||||
let output = run(&t, &opts, Some(&mut point_map));
|
||||
assert_eq!(output, "hello \u{1F525} world");
|
||||
|
||||
// Verify point map
|
||||
assert_eq!(output.len(), point_map.len());
|
||||
// Note: the bolt is a wide character occupying cells 6-7
|
||||
for i in 0..6 {
|
||||
assert_eq!(point_map[i], c(i as u16, 0));
|
||||
}
|
||||
// The fire emoji is 4 UTF-8 bytes, all should map to cell 6 (where the bolt was)
|
||||
let fire_start = 6; // "hello " is 6 bytes
|
||||
for i in 0..4 {
|
||||
assert_eq!(point_map[fire_start + i], c(6, 0));
|
||||
}
|
||||
// " world" follows
|
||||
let world_start = fire_start + 4;
|
||||
for i in 0..6 {
|
||||
assert_eq!(point_map[world_start + i], c(8 + i as u16, 0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_codepoint_map_with_styled_formats() {
|
||||
let mut t = term(10, 24);
|
||||
feed(&mut t, b"\x1b[31mred text\x1b[0m");
|
||||
|
||||
// Replace 'e' with 'X' in styled text
|
||||
let mut opts = Options::vt();
|
||||
opts.codepoint_map = Some(vec![map_entry(
|
||||
('e', 'e'),
|
||||
Replacement::Codepoint('X' as u32),
|
||||
)]);
|
||||
// Should preserve styles while replacing text
|
||||
// "red text" becomes "rXd tXxt"
|
||||
// VT format uses \x1b[38;5;1m for palette color 1
|
||||
assert_eq!(run(&t, &opts, None), "\x1b[0m\x1b[38;5;1mrXd tXxt\x1b[0m");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_codepoint_map_empty_map() {
|
||||
let mut t = term(80, 24);
|
||||
feed(&mut t, b"hello world");
|
||||
|
||||
// Empty map should not change anything
|
||||
let opts = plain_with_map(Vec::new());
|
||||
assert_eq!(run(&t, &opts, None), "hello world");
|
||||
|
||||
// A None map is equivalent.
|
||||
let mut opts = Options::plain();
|
||||
opts.codepoint_map = None;
|
||||
assert_eq!(run(&t, &opts, None), "hello world");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_vt_background_color_on_trailing_blank_cells() {
|
||||
// This test reproduces a bug where trailing cells with background color
|
||||
// but no text are emitted as plain spaces without SGR sequences.
|
||||
// This causes TUIs like htop to lose background colors on rehydration.
|
||||
let mut t = term(20, 5);
|
||||
|
||||
// Simulate a TUI row: "CPU:" with text, then trailing cells with red background
|
||||
// to end of line (no text after the colored region).
|
||||
// \x1b[41m sets red background, then EL fills rest of row with that bg.
|
||||
feed(&mut t, b"CPU:\x1b[41m\x1b[K");
|
||||
// Reset colors and move to next line with different content
|
||||
feed(&mut t, b"\x1b[0m\r\nline2");
|
||||
|
||||
let mut opts = Options::vt();
|
||||
opts.trim = false; // Don't trim so we can see the trailing behavior
|
||||
let output = run(&t, &opts, None);
|
||||
|
||||
// The output should preserve the red background SGR for trailing cells on line 1.
|
||||
// The red background should appear BEFORE the newline, not after.
|
||||
|
||||
// Find position of CRLF
|
||||
let crlf_pos = output.find("\r\n").expect("crlf");
|
||||
|
||||
// Check that red background (48;5;1) appears BEFORE the newline (on line 1)
|
||||
let line1 = &output[..crlf_pos];
|
||||
let has_red_bg_line1 = line1.contains("\x1b[41m") || line1.contains("\x1b[48;5;1m");
|
||||
assert!(has_red_bg_line1);
|
||||
}
|
||||
|
||||
const HTML_OPEN: &str = "<div style=\"font-family: monospace; white-space: pre;\">";
|
||||
|
||||
fn html(t: &Terminal, point_map: Option<&mut Vec<Coordinate>>) -> String {
|
||||
run(t, &Options::html(), point_map)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_html_with_hyperlinks() {
|
||||
let mut t = term(80, 24);
|
||||
|
||||
// Start a hyperlink, write some text, end it
|
||||
feed(
|
||||
&mut t,
|
||||
b"\x1b]8;;https://example.com\x1b\\link text\x1b]8;;\x1b\\ normal",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
html(&t, None),
|
||||
format!("{HTML_OPEN}<a href=\"https://example.com\">link text</a> normal</div>")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_html_with_multiple_hyperlinks() {
|
||||
let mut t = term(80, 24);
|
||||
|
||||
// Two different hyperlinks
|
||||
feed(
|
||||
&mut t,
|
||||
b"\x1b]8;;https://first.com\x1b\\first\x1b]8;;\x1b\\ ",
|
||||
);
|
||||
feed(
|
||||
&mut t,
|
||||
b"\x1b]8;;https://second.com\x1b\\second\x1b]8;;\x1b\\",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
html(&t, None),
|
||||
format!(
|
||||
"{HTML_OPEN}<a href=\"https://first.com\">first</a> <a href=\"https://second.com\">second</a></div>"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_html_with_hyperlink_escaping() {
|
||||
let mut t = term(80, 24);
|
||||
|
||||
// URL with special characters that need escaping
|
||||
feed(
|
||||
&mut t,
|
||||
b"\x1b]8;;https://example.com?a=1&b=2\x1b\\link\x1b]8;;\x1b\\",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
html(&t, None),
|
||||
format!("{HTML_OPEN}<a href=\"https://example.com?a=1&b=2\">link</a></div>")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_html_with_styled_hyperlink() {
|
||||
let mut t = term(80, 24);
|
||||
|
||||
// Bold hyperlink
|
||||
feed(
|
||||
&mut t,
|
||||
b"\x1b]8;;https://example.com\x1b\\\x1b[1mbold link\x1b[0m\x1b]8;;\x1b\\",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
html(&t, None),
|
||||
format!(
|
||||
"{HTML_OPEN}<div style=\"display: inline;font-weight: bold;\"><a href=\"https://example.com\">bold link</div></a></div>"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_html_hyperlink_closes_style_before_anchor() {
|
||||
let mut t = term(80, 24);
|
||||
|
||||
// Styled hyperlink followed by plain text
|
||||
feed(
|
||||
&mut t,
|
||||
b"\x1b]8;;https://example.com\x1b\\\x1b[1mbold\x1b[0m plain",
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
html(&t, None),
|
||||
format!(
|
||||
"{HTML_OPEN}<div style=\"display: inline;font-weight: bold;\"><a href=\"https://example.com\">bold</div> plain</a></div>"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_html_hyperlink_point_map_maps_closing_to_previous_cell() {
|
||||
let mut t = term(80, 24);
|
||||
|
||||
feed(
|
||||
&mut t,
|
||||
b"\x1b]8;;https://example.com\x1b\\link\x1b]8;;\x1b\\ normal",
|
||||
);
|
||||
|
||||
let mut point_map = Vec::new();
|
||||
let output = html(&t, Some(&mut point_map));
|
||||
|
||||
let expected_output =
|
||||
format!("{HTML_OPEN}<a href=\"https://example.com\">link</a> normal</div>");
|
||||
assert_eq!(output, expected_output);
|
||||
assert_eq!(expected_output.len(), point_map.len());
|
||||
|
||||
// The </a> closing tag bytes should all map to the last cell of the link
|
||||
let closing_idx = expected_output.find("</a>").unwrap();
|
||||
let expected_coord = point_map[closing_idx - 1];
|
||||
for i in closing_idx..closing_idx + "</a>".len() {
|
||||
assert_eq!(point_map[i], expected_coord);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user