Skip to content

The backend records which rows soft-wrapped and termlens never asks, so a needle spanning a wrap is not found #265

Description

@vyncint

Today — a line long enough to wrap becomes two rows, and no accessor can put it back together, so a needle that spans the wrap is not found. Measured against 0.9.0 on a 20-column terminal:

// the child printed exactly: "the quick brown fox jumps"
screen:
  the quick brown fox
  jumps

s.contains("brown fox jumps")   // false  — but a reader sees it on the screen
s.find("brown fox jumps")       // None
s.contains("fox\njumps")        // true   — only if you know where the wrap fell

The information needed to fix this is already in the backend and never asked for. vt100::Screen::row_wrapped(row) -> bool is public and reports whether a row ended in a soft wrap rather than a newline:

$ grep -rn 'row_wrapped' crates/termlens/src/
$                                    # never called

Why it is worth fixing — the failure looks like the crate is lying. The timeout embeds the screen, the screen plainly shows brown fox jumps, and the predicate that was waiting for it says it never appeared. This crate has met that shape before and treated it as a defect worth real work: when text scrolls off the top, contains cannot see it either, and #203/#204 added a note to every timeout saying how many rows have scrolled and pointing at full_text. A wrap is the same trap one row lower, with no note and no full_text to point at.

It is also load-bearing for a second decision. Terminal::resize's rustdoc (terminal.rs:3573) and docs/DESIGN.md:451 both justify not reflowing scrollback like this:

History is text with no record of which rows were soft-wrapped, so there is nothing to reflow from

The record exists. It is row_wrapped, it is public, and it is available at the moment history is captured — it simply is not captured. The decision not to reflow may well still be right on cost grounds, but its stated reason is not the true one, and a contributor reading it is being told a capability does not exist when it does.

Fix — capture the flag and expose the joined text; leave the search semantics alone.

  • Read row_wrapped for each row when the snapshot is built (emu/vt100.rs, beside the cell conversion) and carry one bit per row on Screen.
  • Screen::row_wrapped(row) -> bool — the raw fact, matching the backend's name.
  • Screen::logical_text() -> String — the grid as text with soft-wrapped rows joined by nothing and hard line ends joined by \n, so logical_text().contains("brown fox jumps") is true. This is the accessor the failing assertion above wants.

contains and find do not change. Their rustdocs promise that a needle is found precisely when contains is true and that columns are real grid columns, and a match spanning a wrap has no single row or column to report. Instead their docs gain the wrap trap beside the scrollback trap they already carry, pointing at logical_text the way they point at full_text — the two are the same shape of answer to the same shape of question.

Then correct the two sentences above: whatever resize decides about reflow, the reason is the cost of doing it, not the absence of the record.

Out of scope, deliberately: reflowing on resize, and carrying the flag into scrollback history. Both become possible after this and neither should ride along with it — say so in the PR.

Done whenScreen::row_wrapped reports the backend's flag; logical_text() joins soft-wrapped rows so a needle spanning a wrap is found in it; contains and find are unchanged but document the trap and name logical_text; the resize rustdoc and docs/DESIGN.md no longer say the record does not exist; and a test drives a wrapping line and asserts all three.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions