Skip to content

Stop unwrapping sparse rows that wrapping could not have produced - #10

Merged
krotname merged 2 commits into
mainfrom
feature/preserve-sparse-rows
Aug 4, 2026
Merged

Stop unwrapping sparse rows that wrapping could not have produced#10
krotname merged 2 commits into
mainfrom
feature/preserve-sparse-rows

Conversation

@krotname

@krotname krotname commented Aug 4, 2026

Copy link
Copy Markdown
Owner

The defect

Fit Width and the manual narrow/widen actions unwrap continuation rows before re-laying out a
table. isLikelyContinuationRow decided what counts as a continuation row from the shape of
the row alone: any row that left at least one column empty under a filled one was merged into the
row above.

Ordinary sparse data has exactly that shape, so distinct records were silently collapsed:

| Name  | Note                     |
| ----- | ------------------------ |
| Alice | short                    |
|       | second                   |
| Bob   | a much longer value here |

became

| Alice | short second             |

This also fired automatically, when power auto fit runs after an unrelated edit.

The fix

Wrapping leaves a checkable trace, so test for it instead of guessing:

  • It fills a cell's segments from the top, so a segment can never sit under an empty cell.
  • It is greedy, so it never leaves room for the next token. A row whose first token would still
    have fitted after the previous segment was never produced by wrapping.

The reference width for that second test is measured only over rows that fill every column
(wrappingReferenceWidths). A continuation row must leave a column empty, so those rows carry the
real column width. Measuring the candidates too would let a hand-split row widen the very column it
is tested against — which is what keeps hand-split words such as scrip / t already joinable in
tables the user wrote by hand.

What is deliberately unchanged

Rows that wrapping really did produce are still rejoined, so widening a fitted table rebuilds the
original sentence and repeated fits stay idempotent.

One case stays merged and cannot be fixed by any algorithm: a table whose sparse pair is a valid
wrap output at its own column width. For
| Alice | first record | / | | orphan record | at width 13, fit(25) on the single-row form
produces exactly those two lines, so the two readings are the same text. The fix removes the cases
that are decidable and leaves this one, rather than guessing.

Verification

The change is identical in all three cores (JetBrains, Notepad++, VS Code) and was checked against
the reference implementation with a differential harness:

corpus cases result
edge cases + fuzz 11 719 TypeScript ≡ Java
widths, separators, CSV, Unicode 19 089 TypeScript ≡ Java
chained operation sequences 6 000 TypeScript ≡ Java
fit/narrow/widen only 5 000 TypeScript ≡ Java, C++ ≡ Java on content

Behaviour changes are confined to the three operations that unwrap (FIT, NARROW_COLUMN,
WIDEN_COLUMN); align, sorting and structural edits are untouched. A separate invariant run over
5 000 tables confirms no operation loses text.

Three scenarios were added to the shared golden fixture, kept byte-identical across the three
repositories.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KsNFrStLUQaEZNtCybK4XD

Fit Width and the manual narrow/widen actions unwrap continuation rows before
re-laying out a table. The check that decided what counts as a continuation row
only looked at the shape of the row: any row that left at least one column empty
under a filled one was merged into the row above. Ordinary sparse data has that
shape too, so distinct records were silently collapsed into a single row -
including automatically, when power auto fit runs after an unrelated edit.

Wrapping leaves a checkable trace, so test for it instead of guessing:

- It fills a cell's segments from the top, so a segment can never sit under an
  empty cell.
- It is greedy, so it never leaves room for the next token. A row whose first
  token would still have fitted after the previous segment was never wrapped.

The reference width for that test is measured only over rows that fill every
column. A continuation row must leave a column empty, so those rows carry the
real column width; measuring the candidates too would let a hand-split row widen
the very column it is tested against, which is what kept hand-split words such
as "scrip" / "t already" joinable.

Rows that wrapping did produce are still rejoined, so widening a fitted table
still rebuilds the original sentence and repeated fits stay idempotent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KsNFrStLUQaEZNtCybK4XD

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1c90137e79

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/core.ts Outdated
Two ways the continuation check could reject a genuine continuation:

- Header rows are never wrapped, so when a fit pushes body cells below the
  header width, formatTable widens the rendered column back to the header. The
  reference width then described a width the body was never split at, and a
  later fit refused to rejoin the record. Measure only body rows now.
- Wrapping hard-splits an over-wide link or code span mid-token, so a fragment
  such as "[x y](ur" no longer parses as a link. Re-tokenising it under-measured
  the segment. Compare the whole cell instead, which needs no tokenising and is
  the conservative direction: it only refuses a merge when the entire cell would
  still have fitted after the previous segment.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KsNFrStLUQaEZNtCybK4XD
@krotname
krotname merged commit 1fbbf24 into main Aug 4, 2026
6 checks passed
@krotname
krotname deleted the feature/preserve-sparse-rows branch August 4, 2026 09:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant