Skip to content

feat(inspector): set CSV delimiter, quote, encoding, and line ending with reload#1943

Merged
datlechin merged 1 commit into
mainfrom
feat/1469-csv-properties
Jul 22, 2026
Merged

feat(inspector): set CSV delimiter, quote, encoding, and line ending with reload#1943
datlechin merged 1 commit into
mainfrom
feat/1469-csv-properties

Conversation

@datlechin

Copy link
Copy Markdown
Member

Part 5 toward #1469 (CSV structural editing). The Set CSV Properties dialog. Stacked on #1942 (base branch feat/1469-csv-header-toggle).

What

  • Edit > Set CSV Properties… opens a sheet to set the delimiter, quote character, encoding, and line ending by hand, prefilled with the values auto-detected at open. Reload re-reads the file with the chosen settings; Cancel leaves it as is.
  • Reload discards unsaved edits, and asks first when there are any.

(The escape-character field is the last remaining property from the issue; it needs a change to the shared parser and lands in a follow-up so this dialog stays low-risk.)

Why

Auto-detection can guess the delimiter or encoding wrong. #1469 asks for a manual override with a Reload so the user can fix it without leaving the app.

How (native / HIG)

  • The dialog is a document-scoped sheet (presentAsSheet), which the HIG recommends over a floating panel for a text/selection-heavy, document-bound task. It has a Reload primary and a Cancel, per the sheet guidance.
  • A new CSVConfigurableDocument protocol (refining InspectorDocument, in PluginKit) exposes csvDialect and reload(with:), so the app-target inspector reaches CSV-specific settings through a protocol instead of importing the plugin. Added as a new protocol, so it is additive (no version bump).
  • reload(with:) re-parses the originally loaded bytes with the new dialect, re-infers types, clears the undo stack (like a revert), and marks the document edited so Save writes with the new settings.
  • Option-to-dialect mapping lives in a pure CSVPropertyOptions helper so the picker choices and the resulting CSVDialect are unit-testable; the BOM is preserved from the original.

Tests

  • CSVPropertyOptions: index lookup from a dialect, unknown-value fallback, and building a dialect from picker indices (with BOM preserved).

Docs / changelog

  • docs/features/csv-inspector.mdx auto-detection section notes the override.
  • CHANGELOG entry under [Unreleased].

Needs a local Xcode build to confirm compilation. PluginKit change is additive (a new protocol); no version bump.

@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: a48d85a45b

ℹ️ 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 on lines +358 to +360
let data = store.data
dialect = newDialect
store = CSVRowStore(data: data, dialect: newDialect)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Re-encode all rows using the selected dialect

After any property change, this recreates the store with every row still backed by the original byte ranges. On Save, CSVRowStore.rowSource(at:) returns those ranges as .rawBytes, and CSVWriter.append copies them verbatim instead of serializing them with the newly selected dialect. Thus changing encoding or line ending and saving leaves existing rows in the old format (and editing only one row produces a mixed-format file); with an encoding change, the writer can even prepend a new BOM to payload bytes still in the prior encoding. Materialize/rewrite the original rows when the dialect changes so the Save action promised by this flow actually writes the selected settings.

Useful? React with 👍 / 👎.

Comment on lines +26 to +27
("UTF-16 LE", .utf16LittleEndian),
("UTF-16 BE", .utf16BigEndian),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Support UTF-16 parsing before offering it as an override

For a BOM-less UTF-16 CSV, selecting either option here cannot correctly repair detection: CSVStreamingParser scans and splits one byte at a time, so after consuming an ASCII delimiter or newline byte it leaves the adjacent UTF-16 NUL byte at the start of the next field/row. That yields malformed fields and, for CRLF input, spurious rows. BOM-less UTF-16 is precisely a case where the manual encoding picker is needed, so decode into code-unit-aligned text (or make the parser encoding-aware) before exposing these choices.

Useful? React with 👍 / 👎.

@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: 31a66c02d2

ℹ️ 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".

static let encodings: [(label: String, encoding: String.Encoding)] = [
("UTF-8", .utf8),
("UTF-16 LE", .utf16LittleEndian),
("UTF-16 BE", .utf16BigEndian),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject unrepresentable rows for legacy encoding selections

When a user selects Latin-1 or Windows-1252 and then edits a row containing an unrepresentable character (for example, an emoji from an originally UTF-8 file), CSVWriter.append gets nil from data(using:) but still appends the line ending. The save therefore silently replaces the entire edited record with a blank row instead of reporting its existing encodingFailed error; reject the save or otherwise handle the failed conversion.

Useful? React with 👍 / 👎.

@datlechin

Copy link
Copy Markdown
Member Author

Reviewed this PR. The review surfaced only a false positive and one below-threshold edge, so no code changes were needed (the branch was rebased onto the updated stack):

  • False positive: reload(with:) was flagged for not clearing typeOverrides, but it calls recomputeInferredTypes(), which already resets typeOverrides = [:].
  • Known limitation (niche): switching a BOM-detected file to a non-BOM encoding (e.g. Latin-1) and reloading would show the BOM bytes as leading garbage. A correct fix needs BOM-length tracking independent of encoding; deferring it since it only triggers on a deliberate mis-selection of a file whose encoding was already correctly detected from its BOM.

@datlechin
datlechin force-pushed the feat/1469-csv-header-toggle branch from ba0d32f to ef9a509 Compare July 22, 2026 03:53
@datlechin
datlechin force-pushed the feat/1469-csv-properties branch from 31a66c0 to 1e9c6e6 Compare July 22, 2026 03:53
@datlechin
datlechin force-pushed the feat/1469-csv-header-toggle branch from ef9a509 to 216a906 Compare July 22, 2026 03:56
@datlechin
datlechin force-pushed the feat/1469-csv-properties branch from 1e9c6e6 to 6a09a40 Compare July 22, 2026 03:56
@datlechin
datlechin force-pushed the feat/1469-csv-header-toggle branch from 216a906 to e3eb043 Compare July 22, 2026 06:52
@datlechin
datlechin force-pushed the feat/1469-csv-properties branch from 6a09a40 to 31cdb39 Compare July 22, 2026 06:52
@datlechin
datlechin force-pushed the feat/1469-csv-header-toggle branch from e3eb043 to 140e8ea Compare July 22, 2026 07:00
@datlechin
datlechin force-pushed the feat/1469-csv-properties branch from 31cdb39 to 1b2c45a Compare July 22, 2026 07:00
Base automatically changed from feat/1469-csv-header-toggle to main July 22, 2026 07:01

@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: 1b2c45a17a

ℹ️ 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".

quoteChar: quotes.indices.contains(quoteIndex) ? quotes[quoteIndex].byte : base.quoteChar,
encoding: encodings.indices.contains(encodingIndex) ? encodings[encodingIndex].encoding : base.encoding,
lineEnding: lineEndings.indices.contains(lineEndingIndex) ? lineEndings[lineEndingIndex].value : base.lineEnding,
hasBom: base.hasBom

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Skip an existing BOM when changing the encoding

When a UTF-8-BOM CSV is reloaded after selecting Latin-1 or Windows-1252, this retains hasBom == true but changes encoding; CSVStreamingParser.bomSkip returns zero for those encodings, so the three BOM bytes are parsed as part of the first header/field (for Latin-1, name). Preserve the source BOM length independently of the chosen encoding, or clear/normalize it when building the override dialect.

Useful? React with 👍 / 👎.

@datlechin
datlechin force-pushed the feat/1469-csv-properties branch from 1b2c45a to a04770b Compare July 22, 2026 07:02
@datlechin
datlechin merged commit 98f9160 into main Jul 22, 2026
1 of 2 checks passed
@datlechin
datlechin deleted the feat/1469-csv-properties branch July 22, 2026 07:03
@mintlify

mintlify Bot commented Jul 22, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
TablePro 🟢 Ready View Preview Jul 22, 2026, 7:03 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@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: a04770ba8f

ℹ️ 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 on lines +61 to +63
CSVDialect(
delimiter: delimiters.indices.contains(delimiterIndex) ? delimiters[delimiterIndex].byte : base.delimiter,
quoteChar: quotes.indices.contains(quoteIndex) ? quotes[quoteIndex].byte : base.quoteChar,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Prevent non-tab delimiters for TSV documents

When a user selects (for example) Comma for a .tsv document and saves, the file is written using that delimiter, but reopening it calls read(from:ofType:), which forcibly resets every public.tab-separated-values-text dialect to tab. The reopened document is therefore parsed incorrectly, so this newly exposed selection cannot persist for TSV files. Either constrain TSV documents to tab in this dialog or stop overriding their selected delimiter on read.

Useful? React with 👍 / 👎.

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