fix(editor): the renderer never got the mode everything else was reading
The frame resolved ui.line-wrap, recorded it on the window, and fed it to the coordinate mapping and the scroll indicator --- while the viewport handed TextView::render a hard-coded Truncate. With the default of wrap, that meant the cursor was placed for wrapped text and the indicator reckoned against wrapped rows while the text was still clipped at the edge. The worst possible split: every part that reports where things are agreed, and the part that draws them did not. The viewport now reads window.last_wrap, so it consumes the same single resolution as everything else rather than resolving again. How it survived: the edit was in a script that raised on a LATER assertion, so nothing was written; a follow-up script's replace then matched nothing and silently did nothing. Every test I had asked "is the mode right?" and none asked "is the text wrapped?", so all of them passed. Hence the new witness reads the GRID. the_default_actually_wraps_the_ painted_text goes through RenderState and reconstructs rows from the emitted CellDelta spans, for two reasons: the defect lived in the DRIVER, between the resolved mode and the viewport it built, so a test building its own viewport would have passed against it --- and the spans are what a TUI actually consumes. truncate_clips_the_painted_text is its control, so the pair is discriminating rather than merely true. It bites: restoring the hard-coded Truncate fails the wrap witness while all five other tests keep passing, which is exactly the shape that let it through. Gates: fmt, workspace clippy -D warnings, diff --check, --lib 1917/0, crdt 2102/0, line_wrap 6/6, tab_width 2/0, folding 21/0, full_grid_resync 1/1. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016bqGA6s9tTUFzYpbeW3tai
This commit is contained in:
parent
eaf3df8765
commit
aa3cd4da34
|
|
@ -4363,15 +4363,18 @@ fn paint_window_content(
|
||||||
cell_size: crate::cell::CellSize::new(inner_rows, rect.size.cols - gutter_w),
|
cell_size: crate::cell::CellSize::new(inner_rows, rect.size.cols - gutter_w),
|
||||||
gutter_w,
|
gutter_w,
|
||||||
folds,
|
folds,
|
||||||
// QoL Stage 3: the resolved mode belongs here, beside `folds`,
|
// The resolved mode belongs here beside `folds`, for the same
|
||||||
// for the same reason — the driver holds the registry and the
|
// reason: the driver holds the registry and the buffer, the view
|
||||||
// buffer, the view holds neither. Pinned to the identity case
|
// holds neither.
|
||||||
// until `ui.line-wrap` is registered; the wrap path is built
|
//
|
||||||
// and tested beneath this, but nothing can reach it yet, which
|
// It reads `last_wrap` rather than resolving again. The render
|
||||||
// is deliberate. Exposing a mode before the cursor mapping
|
// loop already resolved it for this window this frame, and the
|
||||||
// honors it would ship a setting that renders one thing and
|
// coordinate callers read that same field through
|
||||||
// navigates another.
|
// `Window::layout_ctx` — so a second resolution here could
|
||||||
wrap: crate::view::WrapMode::Truncate,
|
// disagree with the one the cursor is placed against, which is
|
||||||
|
// the failure this whole one-resolution arrangement exists to
|
||||||
|
// prevent.
|
||||||
|
wrap: window.last_wrap,
|
||||||
};
|
};
|
||||||
// Composition (T M2.9): base text_view paints first, then the
|
// Composition (T M2.9): base text_view paints first, then the
|
||||||
// gutter numbers — before the overlays, so a diagnostic overlay
|
// gutter numbers — before the overlays, so a diagnostic overlay
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,53 @@ fn mode_global(s: &EditorState) -> String {
|
||||||
eval(s, "return pmacs.config.get('ui.line-wrap')")
|
eval(s, "return pmacs.config.get('ui.line-wrap')")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Put `text` in the window's current buffer by typing it, which is the
|
||||||
|
/// only path Lua exposes and also the one a user takes.
|
||||||
|
fn fill_active(s: &EditorState, text: &str) {
|
||||||
|
for ch in text.chars() {
|
||||||
|
exec(
|
||||||
|
s,
|
||||||
|
&format!("pmacs.editor.insert_char_over_region({})", ch as u32),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Render one frame and return the text of the first `rows` grid rows,
|
||||||
|
/// reconstructed from the emitted `CellDelta` spans.
|
||||||
|
///
|
||||||
|
/// Two reasons for going through `RenderState` and the wire rather than
|
||||||
|
/// calling `TextView::render` with a hand-built viewport. The defect
|
||||||
|
/// this guards against lived in the *driver*, between the resolved mode
|
||||||
|
/// and the viewport it built — a test that constructed its own viewport
|
||||||
|
/// would have passed against it. And the spans are what the TUI
|
||||||
|
/// actually consumes, so this asserts on the bytes that reach a screen.
|
||||||
|
fn render_rows(s: &EditorState, rows: u32, cols: u32) -> Vec<String> {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
let size = pmacs::cell::CellSize::new(rows, cols);
|
||||||
|
let mut rs = pmacs::instance_render::RenderState::new(size);
|
||||||
|
let msgs = rs.render_frame(s, pmacs::protocol::FrontendId::LOCAL, &HashMap::new(), &[]);
|
||||||
|
|
||||||
|
let mut grid = vec![vec![' '; cols as usize]; rows as usize];
|
||||||
|
for msg in &msgs {
|
||||||
|
if let pmacs_protocol::InstanceMessage::CellDelta { spans, .. } = msg {
|
||||||
|
for span in spans {
|
||||||
|
for (i, cell) in span.cells.iter().enumerate() {
|
||||||
|
let r = span.start.row as usize;
|
||||||
|
let c = span.start.col as usize + i;
|
||||||
|
if r < rows as usize
|
||||||
|
&& c < cols as usize
|
||||||
|
&& let pmacs::cell::Glyph::Char(ch) = cell.glyph
|
||||||
|
{
|
||||||
|
grid[r][c] = ch;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
grid.into_iter().map(|r| r.into_iter().collect()).collect()
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn the_default_is_wrap() {
|
fn the_default_is_wrap() {
|
||||||
let s = session("default");
|
let s = session("default");
|
||||||
|
|
@ -137,3 +184,40 @@ fn a_pinned_buffer_toggles_from_its_own_value() {
|
||||||
"the toggle read this buffer's value, not the global one"
|
"the toggle read this buffer's value, not the global one"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The default must reach the **rendered cells**, not merely the
|
||||||
|
/// resolved value.
|
||||||
|
///
|
||||||
|
/// This is the gap review found: the frame resolved `ui.line-wrap`,
|
||||||
|
/// recorded it on the window, and fed it to the coordinate mapping and
|
||||||
|
/// the scroll indicator — while the viewport handed the renderer a
|
||||||
|
/// hard-coded `Truncate`. Every "is the mode right?" assertion passed
|
||||||
|
/// and the text was still clipped. So this test reads the grid.
|
||||||
|
#[test]
|
||||||
|
fn the_default_actually_wraps_the_painted_text() {
|
||||||
|
let s = session("rendered_default");
|
||||||
|
// Wider than the viewport below, and distinctive.
|
||||||
|
fill_active(&s, "ABCDEFGHIJKLMNOP");
|
||||||
|
let rows = render_rows(&s, 4, 4);
|
||||||
|
assert_eq!(rows[0], "ABCD");
|
||||||
|
assert_eq!(
|
||||||
|
rows[1], "EFGH",
|
||||||
|
"the default is `wrap`, so the remainder continues on the next row \
|
||||||
|
— a hard-coded Truncate in the viewport leaves this blank"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// And `truncate` still clips, so the witness above is discriminating
|
||||||
|
/// rather than merely true.
|
||||||
|
#[test]
|
||||||
|
fn truncate_clips_the_painted_text() {
|
||||||
|
let s = session("rendered_truncate");
|
||||||
|
fill_active(&s, "ABCDEFGHIJKLMNOP");
|
||||||
|
exec(
|
||||||
|
&s,
|
||||||
|
"pmacs.config.set_local(pmacs.window.buffer(), 'ui.line-wrap', 'truncate')",
|
||||||
|
);
|
||||||
|
let rows = render_rows(&s, 4, 4);
|
||||||
|
assert_eq!(rows[0], "ABCD");
|
||||||
|
assert_eq!(rows[1], " ", "truncate keeps one row per source line");
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue