fix(table): byte-preserving row/column deletion - #208
Merged
Conversation
Deletion spliced the parsed grid and re-serialized the whole table, normalizing CRLF to LF, stripping the UTF-8 BOM, stripping the trailing newline, dropping ragged-row extra fields, and erasing malformed-CSV warnings. Replace it with raw-text splicing on the existing csvTable.ts primitives (BOM-aware rowSpans, parseCsvRecord, serializeCsvRow) — the same byte-fidelity contract the cell-edit commit already honors. Row deletion removes each span plus its own line terminator; column deletion rewrites only records that contain a dropped field. Untouched rows keep their exact bytes, so surviving ragged rows keep their malformed-CSV warnings.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
PR #207 shipped row/column deletion, but both transforms in
app/src/lib/tableDeletion.tsfilter the parsed grid and re-serialize the entire table throughserializeCsvTable. On any row or column deletion that silently:x,y,z,EXTRA1,EXTRA2→x,y,z), androwWidthsto "never trip the malformed-CSV warning detector" — that normalization is what made the warning disappear).That is data loss, and it contradicts the byte-preservation contract the cell-edit path already honors in
csvTable.ts("untouched rows keep their exact bytes"). Verified against master (9a1579a3) before this branch.What
Deletion now splices the raw source text using the same primitives the cell-edit commit path already proved out — BOM-aware
rowSpans,parseCsvRecord,serializeCsvRow. No new parsing, no grid re-serialization.Row deletion —
deleteRows(raw, table, sources): each removed region is the row's span plus its own trailing terminator. One generic rule covers every terminator edge:Mixed endings keep each row's own terminator, and the BOM is untouched because splices never touch index 0.
Column deletion —
deleteColumns(raw, table, dataCols): per-record splice.parseCsvRecordover each raw row span, drop the selected fields,serializeCsvRowthe record back. Records that lack every dropped field (short ragged rows) stay byte-identical — no splice at all; only records actually containing the field get their own bytes rewritten. The header gets the same treatment as one record (its span is derived from the parse's BOM-aware span base plus the single terminator before row 0). Wide ragged rows keep their extra fields, since extras sit past the header-defined columns and only those are deletable.API change: the old functions took the parsed
CsvTableDataand returned a string; byte splicing needs the raw text plus the parse, so the signatures are nowdeleteRows(raw, parse, sources)/deleteColumns(raw, parse, dataCols). The threeCsvTable.tsxcall sites (toolbar rows, toolbar columns, keyboardonDelete) now passlatestRef.current.textalongside the table. The guarded-reconversion path, selection-state dropping, last-column guard, and no-undo-stack posture are unchanged; the output table stays read-only.Deliberately NOT done: no new changelog entry — entry 10 (live <1 day) already describes the correct behavior users now actually get; this PR closes the gap between the description and the implementation rather than announcing it twice.
How to Review
app/src/lib/tableDeletion.ts— the whole fix.deleteRowsmaps source indices → spans → splice regions (deduped, sorted);deleteColumnswalks header span + row spans and rewrites only records containing a dropped field.headerSpanOfderives the header record's span arithmetically (BOM base; one terminator back from row 0's span start, or trailing-terminator trim when there are no data rows).app/src/lib/__tests__/tableDeletion.test.ts— the old normalized-output tests are replaced with the byte-fidelity contract: CRLF/BOM/no-trailing-newline/last-row/delete-all-rows, mixed endings, ragged rows on both deletion types, quoted fields with embedded delimiters/newlines sharing a file with plain rows, out-of-range tolerance, andcsvWarningspersistence (surviving ragged rows keep their warning; deleting the malformed row clears it naturally).app/src/components/CsvTable.tsx— callers only, plus updated comments describing the splice path.Non-obvious choice: after deleting the last row of a file without a trailing newline, the file gains a trailing newline (the surviving row's own terminator). That is the byte-faithful outcome — every surviving byte stays exactly where it was; the alternative (also deleting the header's terminator to preserve file-level "absence") would alter bytes the deletion never touched.
Test Evidence
Local gates, all green on the tested head
4508028de1d21e7ba542825cc78045c7bde7099e: full Vitest suite 453/453 (was 439 + 23 new − 9 replaced),npm run lint,npx tsc -b --noEmit,npm run buildwithapp/distrebuilt and committed,bash .github/scripts/verify-seo.sh.Dogfood (recorded against the tested head, fixtures ingested through the live app's file-upload control — the byte-honest ingest path, since textarea/
fillAPIs normalize CRLF before the app ever sees them):name,album\r\nJack White,Blunderbuss\r\n…\r\n—cr: trueon every line.r,s,t,EXTRA1,EXTRA2), row1,2,3selected via the row marker and deleted. Byte-verified: raw state isx,y,z\nr,s,t,EXTRA1,EXTRA2\n4,5,6— extras intact, so the malformed-CSV warning for that row persists.🔗 Obvious Project · 🧵 Obvious Thread