From 755dc48a383a10b6859e5efa7524068673e6f436 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sat, 28 Mar 2026 21:48:06 +0100 Subject: [PATCH] Preserve one-item inline spacing on insert --- docs/reference/support-matrix.md | 2 +- src/Encoder/Encoder.php | 23 +++++++++++++++++++ tests/Integration/EditingFixtureTest.php | 16 +++++++++++++ .../expected.toml | 1 + .../input.toml | 1 + .../expected.toml | 1 + .../input.toml | 1 + 7 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 tests/fixtures/Editing/nested-single-line-inline-insert-from-one/expected.toml create mode 100644 tests/fixtures/Editing/nested-single-line-inline-insert-from-one/input.toml create mode 100644 tests/fixtures/Editing/single-line-inline-insert-from-one/expected.toml create mode 100644 tests/fixtures/Editing/single-line-inline-insert-from-one/input.toml diff --git a/docs/reference/support-matrix.md b/docs/reference/support-matrix.md index f72f378..5e0c817 100644 --- a/docs/reference/support-matrix.md +++ b/docs/reference/support-matrix.md @@ -93,7 +93,7 @@ The default API behavior is TOML 1.1-compatible. Strict TOML 1.0 parsing/decodin | Trivia preservation on document items and table entries | Partial | Available through `Toml::parse($input, true)` for leading/trailing trivia on parsed items | | Trivia preservation inside parsed arrays and inline tables | Partial | Collection-local item spacing and comments are preserved where represented in the AST | | Comment preservation on re-encode | Partial | Preserved for parsed document items, table entries, and collection items when trivia is available | -| Formatting preservation on re-encode | Partial | Available in `DocumentFormattingMode::SourceAware` for trivia-preserving ASTs | +| Formatting preservation on re-encode | Partial | Available in `DocumentFormattingMode::SourceAware` for trivia-preserving ASTs, including local spacing preservation for common single-line collection edits | | `encodeDocument()` round-trip fidelity | Partial | Normalized by default; source-aware mode is lossless for unchanged parsed regions, while edited regions preserve local style when inferable and otherwise canonicalize locally | ## Tooling and Errors diff --git a/src/Encoder/Encoder.php b/src/Encoder/Encoder.php index 3cfd95c..6c76e2c 100644 --- a/src/Encoder/Encoder.php +++ b/src/Encoder/Encoder.php @@ -1121,8 +1121,18 @@ private function inferSingleLineInlineTableStyle(InlineTable $value): ?array $afterComma = null; $observedPair = false; $itemCount = count($value->items); + $reusableItemCount = 0; + $lastReusableItem = null; for ($index = 1; $index < $itemCount; $index++) { + if (!$this->isSyntheticNode($value->items[$index - 1])) { + $reusableItemCount++; + $lastReusableItem = $value->items[$index - 1]; + } + if ($index === $itemCount - 1 && !$this->isSyntheticNode($value->items[$index])) { + $reusableItemCount++; + $lastReusableItem = $value->items[$index]; + } if ($this->isSyntheticNode($value->items[$index - 1]) || $this->isSyntheticNode($value->items[$index])) { continue; } @@ -1140,6 +1150,19 @@ private function inferSingleLineInlineTableStyle(InlineTable $value): ?array } if (!$observedPair) { + if ($reusableItemCount === 1) { + $closingSpacing = $closing; + if ($closingSpacing === '' && $lastReusableItem !== null) { + $closingSpacing = $this->singleLineWhitespaceTrivia($lastReusableItem->getTrailingTrivia()) ?? ''; + } + + return [ + 'opening' => $opening !== '' ? $opening : ' ', + 'afterComma' => $closingSpacing !== '' ? $closingSpacing : ($opening !== '' ? $opening : ' '), + 'closing' => $closingSpacing !== '' ? $closingSpacing : ' ', + ]; + } + return null; } diff --git a/tests/Integration/EditingFixtureTest.php b/tests/Integration/EditingFixtureTest.php index e882cda..16e9da2 100644 --- a/tests/Integration/EditingFixtureTest.php +++ b/tests/Integration/EditingFixtureTest.php @@ -38,6 +38,8 @@ public function testEditedFixturesReencodeAsExpected(string $caseDir): void 'single-line-array-remove-to-one' => $this->applySingleLineArrayRemovalToOne($document), 'single-line-array-remove-consistent' => $this->applySingleLineArrayRemoval($document), 'single-line-inline-insert' => $this->applySingleLineInlineInsert($document), + 'single-line-inline-insert-from-one' => $this->applySingleLineInlineInsert($document), + 'nested-single-line-inline-insert-from-one' => $this->applyNestedSingleLineInlineInsert($document), 'single-line-inline-remove-to-one' => $this->applySingleLineInlineRemovalToOne($document), 'single-line-inline-remove-consistent' => $this->applySingleLineInlineRemoval($document), 'single-line-value-replace' => $this->applySingleLineValueReplace($document), @@ -274,6 +276,20 @@ private function applyNestedInlineReplacement(Document $document): void ); } + private function applyNestedSingleLineInlineInsert(Document $document): void + { + $item = $document->items[0]; + self::assertInstanceOf(KeyValue::class, $item); + self::assertInstanceOf(InlineTable::class, $item->value); + self::assertInstanceOf(InlineTable::class, $item->value->items[0]->value); + + $item->value->items[0]->value->items[] = new KeyValue( + new Key(['y'], [KeyStyle::Bare], $this->span()), + new IntegerValue(2, IntegerBase::Decimal, $this->span()), + $this->span(), + ); + } + private function readFixture(string $path): string { $contents = file_get_contents($path); diff --git a/tests/fixtures/Editing/nested-single-line-inline-insert-from-one/expected.toml b/tests/fixtures/Editing/nested-single-line-inline-insert-from-one/expected.toml new file mode 100644 index 0000000..cde668c --- /dev/null +++ b/tests/fixtures/Editing/nested-single-line-inline-insert-from-one/expected.toml @@ -0,0 +1 @@ +outer = { inner = { x = 1, y = 2 } } diff --git a/tests/fixtures/Editing/nested-single-line-inline-insert-from-one/input.toml b/tests/fixtures/Editing/nested-single-line-inline-insert-from-one/input.toml new file mode 100644 index 0000000..9b3b1a9 --- /dev/null +++ b/tests/fixtures/Editing/nested-single-line-inline-insert-from-one/input.toml @@ -0,0 +1 @@ +outer = { inner = { x = 1 } } diff --git a/tests/fixtures/Editing/single-line-inline-insert-from-one/expected.toml b/tests/fixtures/Editing/single-line-inline-insert-from-one/expected.toml new file mode 100644 index 0000000..0691074 --- /dev/null +++ b/tests/fixtures/Editing/single-line-inline-insert-from-one/expected.toml @@ -0,0 +1 @@ +point = { x = 1, z = 3 } diff --git a/tests/fixtures/Editing/single-line-inline-insert-from-one/input.toml b/tests/fixtures/Editing/single-line-inline-insert-from-one/input.toml new file mode 100644 index 0000000..1e28977 --- /dev/null +++ b/tests/fixtures/Editing/single-line-inline-insert-from-one/input.toml @@ -0,0 +1 @@ +point = { x = 1 }