Skip to content

feat: Add TextSegment for composable nested string interpolation#249

Merged
skarllot merged 8 commits intomainfrom
feat/text-sequence
Mar 17, 2026
Merged

feat: Add TextSegment for composable nested string interpolation#249
skarllot merged 8 commits intomainfrom
feat/text-sequence

Conversation

@skarllot
Copy link
Copy Markdown
Owner

@skarllot skarllot commented Mar 15, 2026

Summary

  • Introduces TextSegment, a composable interpolated string handler that captures interpolated string segments for deferred or nested rendering within SourceTextWriter
  • Extends SourceTextWriter.Write with overloads accepting ReadOnlySpan<string?>, string?[], and IReadOnlyList<string?> for writing multiple text parts
  • Adds AppendFormatted overloads for ReadOnlySpan<char>, ReadOnlySpan<string?>, string?[], IReadOnlyList<string?>, and TextSegment to the interpolation handler, enabling TextSegment values to be embedded directly in interpolated strings

Test plan

  • Run dotnet test to verify all unit tests pass
  • Verify TextSegmentTest covers creation, appending, and rendering scenarios
  • Check that no regressions exist in SourceTextWriterTest and InterpolationCodeWriter.CSharp.Tests

Summary by CodeRabbit

  • New Features

    • Introduced TextSegment - a new interpolated string handler that captures text fragments for reuse in writer operations.
    • Added support for TextSegment.Create() with optional appendLine flag and IFormatProvider parameter.
    • Added Write() method overloads accepting various string collection types.
  • Documentation

    • Added comprehensive TextSegment documentation with usage examples and scenarios.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 15, 2026

📝 Walkthrough

Walkthrough

This PR introduces TextSegment, a new interpolated string handler type that captures and composes text fragments for embedding in SourceTextWriter. It adds Write overloads for multiple collection types, introduces internal validation utilities, updates documentation, and includes comprehensive test coverage with version bump to 2.3.

Changes

Cohort / File(s) Summary
TextSegment Implementation
src/InterpolationCodeWriter/TextSegment.cs, src/InterpolationCodeWriter/TextSegment.Interpolation.cs
New public TextSegment struct with interpolated string handler support. Provides Create factory methods, AppendLine flag, and conversions to SourceTextWriter and list representations. Comprehensive AppendFormatted overloads for all primitive types and nested segments (512 total lines).
SourceTextWriter Extensions
src/InterpolationCodeWriter/SourceTextWriter.Write.cs, src/InterpolationCodeWriter/SourceTextWriter.Interpolation.cs, src/InterpolationCodeWriter/SourceTextWriter.cs, src/InterpolationCodeWriter/SourceTextWriter.Indentation.cs
Adds Write overloads for ReadOnlySpan\<string?\>, string arrays, and IReadOnlyList\<string?\>. Extends WriteInterpolatedStringHandler with AppendFormatted variants for TextSegment and collection types. Introduces region groupings and internal namespace imports (101 total lines).
Internal Utilities
src/InterpolationCodeWriter/Internals/Throws.cs, src/InterpolationCodeWriter/Internals/MathEx.cs
Adds MinMaxExceptionIf validation method to Throws and new Clamp utility for uint with platform-specific implementations (43 total lines).
Tests & Test Configuration
tests/InterpolationCodeWriter.Tests/SourceTextWriterTest.Interpolation.cs, tests/InterpolationCodeWriter.Tests/TextSegmentTest.cs, tests/Directory.Build.props
Converts interpolation tests to parameterized theory data-driven approach with expanded coverage. Adds comprehensive TextSegmentTest suite covering interpolation, AppendLine, ToList, DebuggerDisplay, and growth scenarios. Adds MSBuild props for code coverage exclusion (411 total lines).
Documentation & Configuration
README.md, src/InterpolationCodeWriter/README.md, version.json, modules/.csharpierignore, src/InternalPolyfill/InternalPolyfill.csproj, tests/Directory.Packages.props
Adds TextSegment usage documentation with examples across two README files. Updates version to 2.3. Adds C#ier ignore pattern for polyfill directory. Updates project and package props files (39 total lines).

Sequence Diagram

sequenceDiagram
    participant Code as Client Code
    participant Handler as TextSegment Handler
    participant Writer as SourceTextWriter
    
    Code->>Handler: Create(ref handler, appendLine=false)
    activate Handler
    Handler->>Handler: Initialize _provider, _parts, _length
    deactivate Handler
    
    Code->>Handler: AppendLiteral("text")
    activate Handler
    Handler->>Handler: Add to _parts array
    deactivate Handler
    
    Code->>Handler: AppendFormatted<T>(value)
    activate Handler
    Handler->>Handler: Format value using _provider
    Handler->>Handler: Add formatted string to _parts
    deactivate Handler
    
    Code->>Handler: Create returns TextSegment
    activate Handler
    Handler->>Handler: Finalize _parts array
    deactivate Handler
    
    Code->>Writer: WriteTo(writer)
    activate Writer
    Writer->>Writer: Iterate through _parts
    Writer->>Writer: Write each part (recursive for nested segments)
    alt appendLine is true
        Writer->>Writer: Write newline
    end
    deactivate Writer
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

enhancement

Suggested reviewers

  • fgodoy-lepaho

Poem

🐰 TextSegments dance in strings so bright,
Nested, formatted, shining light,
Handlers hop through write cascades,
Building prose through interpolade!
Version bumps to 2.3 we go,
Watch those fragments splendidly flow!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 74.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and accurately describes the main feature being introduced: TextSegment for composable nested string interpolation, which aligns with the substantial changes across multiple files.
Description check ✅ Passed The PR description covers the main feature (TextSegment), related changes (Write overloads and AppendFormatted additions), and includes a test plan, but is missing some required template sections.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/text-sequence
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Mar 15, 2026

Codecov Report

❌ Patch coverage is 92.83668% with 25 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.08%. Comparing base (ccfb1d5) to head (9bf6732).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...terpolationCodeWriter/TextSegment.Interpolation.cs 93.33% 0 Missing and 14 partials ⚠️
.../InterpolationCodeWriter/SourceTextWriter.Write.cs 75.00% 4 Missing and 2 partials ⚠️
src/InterpolationCodeWriter/Internals/Throws.cs 0.00% 4 Missing ⚠️
src/InterpolationCodeWriter/TextSegment.cs 98.88% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #249      +/-   ##
==========================================
+ Coverage   80.71%   89.08%   +8.37%     
==========================================
  Files          15       18       +3     
  Lines         980     1329     +349     
  Branches       69      101      +32     
==========================================
+ Hits          791     1184     +393     
+ Misses        162      101      -61     
- Partials       27       44      +17     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

fgodoy-lepaho
fgodoy-lepaho previously approved these changes Mar 15, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Line 116: The C# sample in the TextSegment uses unescaped nested double quotes
in the interpolated writer.WriteLine call (the call to FormatTypeRef with
"System" and "Serializable"), which breaks compilation; fix it by escaping the
inner quotes (e.g., replace inner "System" and "Serializable" with escaped
double quotes) or by converting the outer string to a verbatim/interpolated form
so the call to FormatTypeRef("System", "Serializable") is represented with
properly escaped quotes; update the writer.WriteLine(...) line accordingly.

In `@src/InterpolationCodeWriter/SourceTextWriter.Write.cs`:
- Around line 536-544: The Write(string?[] textParts) and
Write(IReadOnlyList<string?> textParts) overloads should treat a null sequence
as a no-op; update each method (Write(string?[] textParts) and
Write(IReadOnlyList<string?> textParts)) to check if the incoming container is
null and return immediately instead of iterating, preserving the existing
null-tolerant behavior of Write(string?) / Write(object?) for elements when the
container is non-null.

In `@src/InterpolationCodeWriter/TextSegment.cs`:
- Around line 116-120: The struct can be copied so multiple instances may call
Clear()/ToListAndClear()/WriteToAndClear() and return the same rented _parts
array multiple times; fix this by introducing an ownership/consumed sentinel
object instead of the simple _isRented bool so only one instance can return the
rented buffer. Replace the primitive _isRented with a small reference type owner
(e.g., PartsOwner { string?[] Parts; int consumed; }) stored in the struct (keep
_parts for fast access if desired), update WithAppendLine to copy the same owner
reference, and in Clear(), ToListAndClear(), and WriteToAndClear() use an atomic
operation (Interlocked.Exchange or Interlocked.CompareExchange on owner.consumed
or swap the owner reference to null) to ensure only the first caller returns the
array to ArrayPool<Item>.Shared and subsequent callers are no-ops; reference the
symbols _parts, _isRented (replace), WithAppendLine, ToListAndClear,
WriteToAndClear, and Clear when making the change.
- Around line 161-166: The Clear method in TextSegment currently returns the
rented Item[] (_parts) while Item contains reference fields, so clear the array
entries before returning to avoid retaining references; inside TextSegment.Clear
(which checks _isRented) set the elements of _parts to default(Item) (e.g.,
Array.Clear(_parts, 0, _parts.Length) or a loop assigning default) and then call
ArrayPool<Item>.Shared.Return(_parts), and ensure you still handle the
non-rented case and any state resets (_isRented/_parts) as appropriate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9b0b1eb0-fc09-4afc-a34f-e750f66bdfcb

📥 Commits

Reviewing files that changed from the base of the PR and between ccfb1d5 and fcf838a.

📒 Files selected for processing (16)
  • README.md
  • modules/.csharpierignore
  • src/InternalPolyfill/InternalPolyfill.csproj
  • src/InterpolationCodeWriter/Internals/Throws.cs
  • src/InterpolationCodeWriter/README.md
  • src/InterpolationCodeWriter/SourceTextWriter.Indentation.cs
  • src/InterpolationCodeWriter/SourceTextWriter.Interpolation.cs
  • src/InterpolationCodeWriter/SourceTextWriter.Write.cs
  • src/InterpolationCodeWriter/SourceTextWriter.cs
  • src/InterpolationCodeWriter/TextSegment.Interpolation.cs
  • src/InterpolationCodeWriter/TextSegment.cs
  • tests/Directory.Build.props
  • tests/Directory.Packages.props
  • tests/InterpolationCodeWriter.Tests/SourceTextWriterTest.Interpolation.cs
  • tests/InterpolationCodeWriter.Tests/TextSegmentTest.cs
  • version.json

Comment thread README.md
Comment thread src/InterpolationCodeWriter/SourceTextWriter.Write.cs
Comment thread src/InterpolationCodeWriter/TextSegment.cs Outdated
Comment thread src/InterpolationCodeWriter/TextSegment.cs Outdated
fgodoy-lepaho
fgodoy-lepaho previously approved these changes Mar 16, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/InterpolationCodeWriter/SourceTextWriter.Interpolation.cs (1)

447-457: Consider adding explicit casts for null literal interpolation holes to prevent ambiguous overload resolution.

The new AppendFormatted(string?[]) and AppendFormatted(IReadOnlyList<string?>) overloads alongside the existing AppendFormatted(string?) can create ambiguous overload resolution for {null} interpolation holes, since null is implicitly convertible to all reference types and neither string?[] nor IReadOnlyList<string?> has a conversion relationship to string?. Use explicit casts in such cases (e.g., {(string?)null}) to avoid C# error CS0121. No current instances were found in the codebase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/InterpolationCodeWriter/SourceTextWriter.Interpolation.cs` around lines
447 - 457, The three overloads AppendFormatted(string?[]),
AppendFormatted(IReadOnlyList<string?>) and the existing
AppendFormatted(string?) create ambiguous overload resolution for interpolation
holes like `{null}`; update the API docs/XML comments on both new overloads to
explicitly warn callers to disambiguate null interpolation holes by casting
(e.g. `{(string?)null}`), add a brief inline code comment at each method
(AppendFormatted(string?[])/AppendFormatted(IReadOnlyList<string?>)) pointing to
the casting requirement, and add a small unit test or comment demonstrating the
correct cast so callers know to use `(string?)null` to avoid CS0121.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/InterpolationCodeWriter/SourceTextWriter.Write.cs`:
- Around line 538-564: The ambiguity occurs because both collection overloads
Write(string?[]?) and Write(IReadOnlyList<string?>?) accept null literals;
change their parameters to non-nullable collections (Write(string?[] textParts)
and Write(IReadOnlyList<string?> textParts)) so a bare null literal will no
longer be considered applicable to those overloads and will resolve to the
existing Write(string?) overload; update the two method signatures (the Write
overloads currently declared at/around the signatures for Write(string?[]?
textParts) and Write(IReadOnlyList<string?>? textParts)) accordingly and keep
their null checks inside the method bodies if you still want to guard against
callers passing null via variables.

In `@src/InterpolationCodeWriter/TextSegment.cs`:
- Around line 11-24: The XML summary for TextSegment incorrectly claims the type
is "immutable" even though it is mutated via AppendLiteral and AppendFormatted
during construction; update the doc comments for the TextSegment type and its
Create(ref TextSegment, bool) and Create(IFormatProvider?, ref TextSegment,
bool) overloads to state that instances are constructed via an interpolated
string handler and are mutated during the AppendLiteral/AppendFormatted calls
but become effectively immutable after construction (or describe their
mutability semantics clearly), and remove or reword the single-word "immutable"
claim so consumers are not misled.

---

Nitpick comments:
In `@src/InterpolationCodeWriter/SourceTextWriter.Interpolation.cs`:
- Around line 447-457: The three overloads AppendFormatted(string?[]),
AppendFormatted(IReadOnlyList<string?>) and the existing
AppendFormatted(string?) create ambiguous overload resolution for interpolation
holes like `{null}`; update the API docs/XML comments on both new overloads to
explicitly warn callers to disambiguate null interpolation holes by casting
(e.g. `{(string?)null}`), add a brief inline code comment at each method
(AppendFormatted(string?[])/AppendFormatted(IReadOnlyList<string?>)) pointing to
the casting requirement, and add a small unit test or comment demonstrating the
correct cast so callers know to use `(string?)null` to avoid CS0121.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d2e0ea79-2fc2-494c-9e1a-59d69f03a2fd

📥 Commits

Reviewing files that changed from the base of the PR and between fcf838a and 6a4847c.

📒 Files selected for processing (5)
  • src/InterpolationCodeWriter/SourceTextWriter.Interpolation.cs
  • src/InterpolationCodeWriter/SourceTextWriter.Write.cs
  • src/InterpolationCodeWriter/TextSegment.Interpolation.cs
  • src/InterpolationCodeWriter/TextSegment.cs
  • tests/InterpolationCodeWriter.Tests/TextSegmentTest.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/InterpolationCodeWriter.Tests/TextSegmentTest.cs

Comment thread src/InterpolationCodeWriter/SourceTextWriter.Write.cs
Comment thread src/InterpolationCodeWriter/TextSegment.cs
fgodoy-lepaho
fgodoy-lepaho previously approved these changes Mar 16, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/InterpolationCodeWriter/SourceTextWriter.Interpolation.cs (1)

422-422: Minor: Inconsistent region directive formatting.

There's a space between # and region here, unlike all other region directives in this file.

🔧 Suggested fix
-        # region String formatting
+        `#region` String formatting
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/InterpolationCodeWriter/SourceTextWriter.Interpolation.cs` at line 422,
The region directive " # region String formatting" is formatted inconsistently;
change the directive text to "#region String formatting" (remove the space
between '#' and 'region') so it matches the other region directives in this
file; look for the exact line containing " # region String formatting" in
SourceTextWriter.Interpolation.cs and update it to "#region String formatting".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/InterpolationCodeWriter/SourceTextWriter.Interpolation.cs`:
- Line 422: The region directive " # region String formatting" is formatted
inconsistently; change the directive text to "#region String formatting" (remove
the space between '#' and 'region') so it matches the other region directives in
this file; look for the exact line containing " # region String formatting" in
SourceTextWriter.Interpolation.cs and update it to "#region String formatting".

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cdaadf8e-af3e-43f6-9de6-ed48b4c635b2

📥 Commits

Reviewing files that changed from the base of the PR and between 6a4847c and df6db35.

📒 Files selected for processing (2)
  • src/InterpolationCodeWriter/SourceTextWriter.Interpolation.cs
  • src/InterpolationCodeWriter/TextSegment.cs

- Fix # region typo (space) in SourceTextWriter.Interpolation.cs
- Add missing AppendFormatted(in TextSegment?) overload to SourceTextWriter handler
- Add EnsureCapacityForAdditionalItems/Grow to guard against _parts overflow
- Fix WriteToAndClear reference in README to use WriteTo
- Add implicit string→TextSegment conversion
- Improve ToList() doc comment to clarify null semantics
- Move SuppressMessage to parameter level in TextSegment constructor

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/InterpolationCodeWriter/Internals/MathEx.cs`:
- Around line 1-2: This file is missing the file-level nullable opt-in; add the
directive `#nullable enable` at the very top of
src/InterpolationCodeWriter/Internals/MathEx.cs (above the using directives) so
the file opts into nullable reference types; update any related XML comments or
nullable annotations in the MathEx class/methods if the compiler surfaces
warnings after enabling nullable.

In `@src/InterpolationCodeWriter/TextSegment.Interpolation.cs`:
- Around line 274-277: The AppendFormatted(bool? value) method lacks a capacity
check and can overflow _parts when _length grows; add a call to
EnsureCapacityForAdditionalItems(1) at the start of AppendFormatted(bool? value)
before writing _parts[_length++] = value?.ToString(_provider) so the array is
grown as needed (match the pattern used by other primitive nullable overloads).

In `@tests/InterpolationCodeWriter.Tests/SourceTextWriterTest.Interpolation.cs`:
- Around line 17-112: The test expectation for the "{(short)1000:N0}" case in
WriteInterpolationCases is using a comma thousand separator but SourceTextWriter
uses CultureInfo.InvariantCulture (which yields "1.000"); update the expected
value in WriteInterpolationCases from "1,000" to "1.000" or alternatively change
the test to create SourceTextWriter with an explicit en-US culture provider so
the output remains "1,000"; reference the WriteInterpolationCases data set and
the SourceTextWriter constructor/creation site when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ffc3d302-a98e-4ffa-b246-6d3eeb29c6f6

📥 Commits

Reviewing files that changed from the base of the PR and between df6db35 and e4df59d.

📒 Files selected for processing (10)
  • README.md
  • src/InternalPolyfill/InternalPolyfill.csproj
  • src/InterpolationCodeWriter/Internals/MathEx.cs
  • src/InterpolationCodeWriter/Internals/Throws.cs
  • src/InterpolationCodeWriter/InterpolationCodeWriter.csproj
  • src/InterpolationCodeWriter/SourceTextWriter.Interpolation.cs
  • src/InterpolationCodeWriter/TextSegment.Interpolation.cs
  • src/InterpolationCodeWriter/TextSegment.cs
  • tests/InterpolationCodeWriter.Tests/SourceTextWriterTest.Interpolation.cs
  • tests/InterpolationCodeWriter.Tests/TextSegmentTest.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/InterpolationCodeWriter.Tests/TextSegmentTest.cs

Comment thread src/InterpolationCodeWriter/Internals/MathEx.cs Outdated
Comment thread src/InterpolationCodeWriter/TextSegment.Interpolation.cs
fgodoy-lepaho
fgodoy-lepaho previously approved these changes Mar 17, 2026
@skarllot skarllot merged commit beb42b7 into main Mar 17, 2026
7 checks passed
@skarllot skarllot deleted the feat/text-sequence branch March 17, 2026 23:43
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.

2 participants