Skip to content

Switch wrapping to textwrap with adapter and unified prefixes#271

Merged
leynos merged 9 commits intomainfrom
refactor-text-wrapping-to-use-textwrap-ek5oxh
Apr 23, 2026
Merged

Switch wrapping to textwrap with adapter and unified prefixes#271
leynos merged 9 commits intomainfrom
refactor-text-wrapping-to-use-textwrap-ek5oxh

Conversation

@leynos
Copy link
Copy Markdown
Owner

@leynos leynos commented Apr 22, 2026

Summary

Switch the wrapping engine from a bespoke LineBuffer-driven path to a textwrap-based implementation, with a small adapter to preserve existing public entry points. This delivery consolidates display-width-aware prefix handling and introduces Markdown-aware fragment modeling that delegates line fitting to textwrap while preserving code spans, links, and trailing spaces.

Changes

  • Dependency updates
    • Add textwrap to Cargo.toml (Rust 1.89+ compatibility / 2024 edition).
    • Ensure unicode-width remains present and used for display-width calculations.
  • Core refactor
    • Replace the bespoke LineBuffer-driven wrapping loop in the active wrap path with a textwrap-based adapter.
    • Centralize display-width-aware prefix handling into a single helper for prefixes and continuation lines.
    • Introduce a minimal adapter so existing wrap entry points (mdtablefix::wrap::wrap_text, Token, tokenize_markdown) continue to function for this first delivery.
  • Modules touched
    • src/wrap.rs, src/wrap/paragraph.rs, and src/wrap/inline.rs as primary touchpoints.
    • Add/adjust an adapter layer to route line breaking through textwrap while preserving inline fragment semantics.
    • Introduce src/wrap/inline/postprocess.rs and src/wrap/inline/test_support.rs for post-processing and test helpers.
  • Tests
    • Promote and align active wrap tests to ensure parity during the transition (updated in src/wrap/tests.rs, tests/wrap_unit.rs, tests/wrap_cli.rs, tests/wrap_renumber.rs, and related CLI/markdown tests).
    • Add regression coverage for critical wrap scenarios (code spans, links, prefixes, bullets, blockquotes, code blocks).
  • Documentation
    • Update user-facing docs to describe the new wrapping approach and clarify that textwrap + unicode-width handle width-sensitive breaking.
    • Revise README.md, docs/architecture.md, and docs/trailing-spaces.md to reflect architecture changes.
    • Add new execution plan docs describing migration and constraints (e.g., docs/execplans/replace-bespoke-wrapping-with-textwrap-and-unicode-width.md).
  • API safety
    • Public API (e.g., Token, tokenize_markdown, wrap_text) preserved for this first pass.
  • Artifacts touched/added
    • src/wrap.rs, src/wrap/paragraph.rs, src/wrap/inline.rs, src/wrap/inline/postprocess.rs, src/wrap/inline/test_support.rs, src/wrap/inline/tests.rs, src/wrap/tests.rs (updated), removal of dead src/wrap/line_buffer.rs.
    • Documentation updates and new execplan doc file: docs/execplans/replace-bespoke-wrapping-with-textwrap-and-unicode-width.md.

Rationale

  • Move to a battle-tested, width-aware wrapping engine while maintaining public API stability.
  • Ensure Unicode display width correctness via unicode-width.
  • Provide a safe, incremental transition by introducing a small adapter and preserving existing entry points.

Testing plan

  • Focused test targets and general test suites:
    • cargo test -- --list | rg wrap
    • cargo test wrap_text_preserves_code_spans --lib
    • cargo test wrap_text_multiple_code_spans --lib
    • cargo test --test wrap_unit
    • cargo test --test wrap_cli
    • cargo test --test wrap_renumber
    • cargo test --test cli
    • cargo test --test cli_frontmatter
    • cargo test --test code_emphasis
    • cargo test --test markdownlint
  • End-to-end validation: verify nested blockquotes, inline code, and links wrap correctly while preserving spans.

Acceptance criteria

  1. wrap_text preserves behavior covered by the active wrap tests (code spans, links, prefixes, fences, hard breaks, trailing spaces).
  2. src/wrap/paragraph.rs uses a single, display-width-aware helper for prefix wrapping instead of a two-step approach.
  3. The bespoke LineBuffer-driven wrapping loop is removed from the active wrap path; line breaking is delegated to textwrap.
  4. tokenize_markdown remains intact and documented as out of scope for this delivery (or its broader removal is clearly approved with updated tests/docs).
  5. Documentation (README.md, docs/architecture.md, and docs/trailing-spaces.md) accurately describe the new implementation.
  6. All quality gates (fmt, lint, tests, markdownlint, nixie) pass.

Rollback plan

If acceptance criteria are not met or regressions appear, revert the changes to the prior wrapping path and re-evaluate in a narrower scope, then reintroduce textwrap support in a controlled, incremental manner.

Artifacts and notes

Summary by Sourcery

Refactor the Markdown wrapping engine to delegate inline line-breaking to the textwrap crate with unicode-width–based fragment widths while unifying display-width-aware prefix handling, and update documentation and tests to reflect and validate the new behavior.

Enhancements:

  • Replace the bespoke LineBuffer-based inline wrapping loop with a fragment adapter over textwrap::wrap_algorithms::wrap_first_fit that preserves Markdown inline code, links, whitespace, and trailing spaces.
  • Centralize display-width-aware prefix and continuation handling in ParagraphWriter, ensuring consistent indentation for blockquotes, bullets, footnotes, and checkboxes.
  • Clarify architecture, trailing-space semantics, and wrapping flow in documentation, including new execution plan guidance for the textwrap-based wrapping engine and minor doc style/table formatting cleanups.

Documentation:

  • Document the new textwrap + unicode-width driven wrapping flow, including updated diagrams and trailing-space behavior, and add an execution plan for the wrapping refactor.

Tests:

  • Expand active wrap tests to cover nested blockquotes, footnote URL continuations, and checkbox indentation while removing obsolete LineBuffer-specific tests.

Chores:

  • Remove the now-unused LineBuffer module from the wrapping implementation and add the textwrap dependency to Cargo.toml.

Task/Notes

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 22, 2026

Note

Reviews paused

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Replace the bespoke incremental LineBuffer wrapper with a fragment-based wrapping pipeline: add textwrap and unicode-width, remove src/wrap/line_buffer.rs, centralise prefix handling via wrap_with_prefix, implement InlineFragment fragments driven by textwrap::wrap_first_fit, and add post-processing to merge whitespace lines and rebalance atomic tails. (≤50 words)

Changes

Cohort / File(s) Summary
Dependencies
Cargo.toml
Add textwrap = "0.16.2" to dependencies.
Top-level docs
README.md, docs/architecture.md, docs/trailing-spaces.md
Update wrap behaviour text: declare textwrap-based fitting, fragment grouping preserving inline code/links/hard breaks, revise trailing-space semantics and replace tokenizer flow diagram with fragment/fit/reconstruct diagrams.
Execution plans
docs/execplans/replace-bespoke-wrapping-with-textwrap-and-unicode-width.md, docs/execplans/yaml-frontmatter.md
Add living exec-plan for migration; make make nixie conditional on Mermaid changes.
Wrapping core
src/wrap.rs, src/wrap/inline.rs, src/wrap/paragraph.rs
Remove mod line_buffer; implement InlineFragment as textwrap::core::Fragment with precomputed unicode-width; implement wrap_preserving_code via wrap_first_fit; centralise prefix/continuation handling in wrap_with_prefix.
Post-processing
src/wrap/inline/postprocess.rs
Add merge_whitespace_only_lines and rebalance_atomic_tails to normalise whitespace-only wrapped lines and move eligible atomic/plain tails while respecting widths.
Deleted module
src/wrap/line_buffer.rs
Delete LineBuffer and SplitContext and all incremental buffering/splitting state and logic.
Tests
src/wrap/tests.rs, tests/wrap_unit.rs, src/wrap/tests/prefix.rs, src/wrap/inline/tests.rs
Remove low-level LineBuffer unit tests; add higher-level wrapping tests (prefix handling, Unicode-width/regression, inline-fragment classification) and test-scoped punctuation helper.
Test helpers
src/wrap/inline/test_support.rs
Add attach_punctuation_to_previous_line as a test-scoped utility.
Docs & ADRs
docs/developers-guide.md, docs/users-guide.md, docs/adrs/0002-textwrap-wrapping-engine.md
Document three-stage wrap pipeline (classification → fragmentisation with precomputed widths → first-fit wrapping + post-processing), map functions to modules, and record ADR justifying textwrap choice.

Sequence Diagram

sequenceDiagram
    participant Markdown as Markdown Text
    participant Classify as Block & Token Classifier
    participant Fragment as Fragment Coalescer
    participant Fit as textwrap::wrap_first_fit
    participant Post as Post-Processor
    participant Render as Line Renderer

    Markdown->>Classify: Provide token spans (code, links, plain, whitespace)
    Classify->>Fragment: Group tokens into InlineFragments and measure widths (unicode-width)
    Fragment->>Fit: Supply fragment slice and target width
    Fit->>Post: Return wrapped fragment lines
    Post->>Render: Merge whitespace-only lines and rebalance atomic tails where allowed
    Render->>Markdown: Emit final prefixed/continued lines
Loading

Possibly related PRs

  • Cargo binstall support #265 — Modify the same wrapping subsystem (files like src/wrap/paragraph.rs, src/wrap/inline.rs) so changes are directly related at the code level.

Poem

Toss the old buffer to the bin,
Fragments file in, let wrapping begin—
textwrap fits the greedy seam,
Atomic tails find room to dream,
Clean lines march out, neat and trim.

🚥 Pre-merge checks | ✅ 7 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Testing ⚠️ Warning The wrap_preserving_code function lacks direct unit tests, relying only on indirect testing through integration tests and helper function tests. Add direct unit tests for wrap_preserving_code covering basic fitting, multiline wrapping, empty input, code span preservation, Markdown links, hard breaks, and width compliance assertions.
Module-Level Documentation ⚠️ Warning pub fn tokenize_markdown() lacks any documentation, and pub use fence::... re-export on src/wrap.rs line 20 lacks a docstring. Add comprehensive doc comments before tokenize_markdown() describing its purpose, parameters, and return value, and before the pub use statement explaining FenceTracker and is_fence roles.
✅ Passed checks (7 passed)
Check name Status Explanation
Linked Issues check ✅ Passed PR addresses core objectives from #80: replaces bespoke wrapping with textwrap, consolidates prefix handling in wrap_with_prefix, removes LineBuffer from active path, preserves public API, and updates documentation. All coding requirements from the linked issue are implemented.
Out of Scope Changes check ✅ Passed All changes align with the refactoring scope defined in #80. Documentation updates, dependency additions, test adjustments, and module removals are all directly related to the textwrap integration and prefix consolidation objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
User-Facing Documentation ✅ Passed The PR adds comprehensive documentation of the new --wrap functionality to docs/users-guide.md, covering usage, Unicode character handling, algorithm details, and special cases like code blocks and hard breaks.
Developer Documentation ✅ Passed All developer documentation requirements are comprehensively satisfied: internal APIs and 4-stage pipeline documented in developers-guide.md, architecture changes reflected in architecture.md with ADR 0002, and execplan complete with all milestones marked and acceptance criteria confirmed.
Title check ✅ Passed The title clearly and directly describes the main change: switching from a bespoke wrapping approach to textwrap with an adapter and unified prefix handling.
Description check ✅ Passed The description comprehensively relates to the changeset, detailing the replacement of LineBuffer with textwrap, addition of InlineFragment model, consolidation of prefix handling, dependency updates, test adjustments, and documentation changes across all modified files.

✏️ 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 refactor-text-wrapping-to-use-textwrap-ek5oxh

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

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Apr 22, 2026

Reviewer's Guide

Refactors the Markdown wrapping engine to delegate inline line-breaking to the textwrap crate with unicode-width-based measurement, replaces the bespoke LineBuffer flow with a fragment adapter, and consolidates prefix handling and documentation to preserve existing wrapping behaviour and public APIs.

Sequence diagram for inline wrapping delegation to textwrap

sequenceDiagram
    participant WT as wrap_text
    participant PW as ParagraphWriter
    participant WP as wrap_preserving_code
    participant TW as textwrap_wrap_first_fit

    WT->>PW: handle_prefix_or_paragraph_line(line, width)
    alt Line has prefix
        PW->>PW: wrap_with_prefix(prefix, continuation_prefix, text)
        PW->>WP: wrap_preserving_code(text, available_width)
    else Plain paragraph line
        PW->>WP: wrap_preserving_code(text, width)
    end

    WP->>WP: tokenize::segment_inline(text)
    WP->>WP: build_fragments(tokens) -> Vec<InlineFragment>

    loop For each fragment
        WP->>WP: candidate = buffer + fragment
        WP->>TW: wrap_first_fit(candidate, [width])
        TW-->>WP: raw_lines: Vec<Vec<InlineFragment>>
        WP->>WP: merged = merge_whitespace_only_lines(raw_lines)
        WP->>WP: rebalance_atomic_tails(merged)
        alt merged.len() == 1
            WP->>WP: buffer = candidate
        else Multiple lines
            WP->>WP: render_line(all_but_last, is_final_output_line=false)
            WP-->>PW: push wrapped lines
            WP->>WP: buffer = last_line
        end
    end

    alt buffer not empty
        WP->>WP: render_line(buffer, is_final_output_line=true)
        WP-->>PW: push final wrapped line
    end

    PW-->>WT: emit prefixed wrapped lines
    WT-->>WT: append lines to global output
Loading

Class diagram for updated wrapping and prefix handling types

classDiagram
    class ParagraphWriter {
        -out : &mut Vec~String~
        -width : usize
        +new(out : &mut Vec~String~, width : usize) ParagraphWriter
        -wrap_with_prefix(prefix : &str, continuation_prefix : &str, text : &str) void
        -append_wrapped_with_prefix(line : &PrefixLine) void
        +flush_paragraph(state : &mut ParagraphState) void
        -push_wrapped_segment(indent : &str, segment : &str) void
        +push_verbatim(state : &mut ParagraphState, line : &str) void
    }

    class PrefixLine {
        +prefix : String
        +rest : &str
        +repeat_prefix : bool
    }

    class ParagraphState {
        +buf : String
        +indent : String
        +has_prefix : bool
    }

    class InlineFragment {
        +text : String
        +width : usize
        +new(text : String) InlineFragment
        +width() usize
    }

    class Fragment {
        <<trait>> Fragment
        +width() f64
        +whitespace_width() f64
        +penalty_width() f64
    }

    class InlineWrapperModule {
        <<module>> InlineWrapper
        +wrap_preserving_code(text : &str, width : usize) Vec~String~
        +attach_punctuation_to_previous_line(lines : &mut [String], current : &str, width : usize) bool
        +determine_token_span(tokens : &[String], start : usize) (usize, usize)
        +build_fragments(tokens : &[String]) Vec~InlineFragment~
        +merge_whitespace_only_lines(lines : &[Vec~InlineFragment~]) Vec~Vec~InlineFragment~~
        +rebalance_atomic_tails(lines : &mut [Vec~InlineFragment~]) void
        +render_line(line : &[InlineFragment], is_final_output_line : bool) String
        +width_as_f64(width : usize) f64
    }

    class TextwrapWrapFirstFit {
        <<external>> textwrap_wrap_first_fit
        +wrap_first_fit(fragments : &[InlineFragment], widths : &[f64]) Vec~&[InlineFragment]~
    }

    ParagraphWriter --> PrefixLine : uses
    ParagraphWriter --> ParagraphState : uses
    ParagraphWriter --> InlineWrapperModule : calls_wrap_preserving_code

    InlineFragment ..|> Fragment
    InlineWrapperModule --> InlineFragment : creates
    InlineWrapperModule --> TextwrapWrapFirstFit : delegates_line_breaking

    class TokenizeModule {
        <<module>> tokenize
        +segment_inline(text : &str) Vec~String~
    }

    InlineWrapperModule --> TokenizeModule : calls_segment_inline
Loading

File-Level Changes

Change Details Files
Replace bespoke inline LineBuffer-based wrapping with a textwrap-backed fragment adapter while preserving Markdown-aware behaviour.
  • Remove the LineBuffer-based wrapping loop and associated tests from the active wrap path.
  • Introduce an InlineFragment type implementing textwrap::core::Fragment with unicode-width-based width calculations.
  • Refactor wrap_preserving_code to tokenize into InlineFragments, call wrap_first_fit, post-process whitespace-only lines, rebalance atomic tails (code spans/links), and render lines while preserving final-line trailing spaces.
  • Keep attach_punctuation_to_previous_line (now test-gated) and reimplement punctuation-trimming behaviour within fragment construction.
src/wrap/inline.rs
src/wrap/tests.rs
src/wrap/line_buffer.rs
Unify prefix-aware paragraph wrapping around a single display-width helper that delegates inline fitting to the new adapter.
  • Introduce ParagraphWriter::wrap_with_prefix to compute available width from prefix display width and call wrap_preserving_code once per prefix.
  • Rework append_wrapped_with_prefix to derive a continuation_prefix (repeated or aligned under text) and delegate to wrap_with_prefix.
  • Refactor push_wrapped_segment to wrap arbitrary segments via wrap_with_prefix instead of manually looping wrap_preserving_code.
src/wrap/paragraph.rs
Tighten and extend wrapping-related tests to cover prefixes, indentation, and active scenarios under the new engine.
  • Remove obsolete LineBuffer-specific unit tests that no longer apply to the active wrapping engine.
  • Add focused tests for nested blockquote prefix repetition, footnote URL continuation alignment, and checkbox indentation preservation.
  • Ensure existing wrap_preserving_code tests still validate whitespace handling, code spans, and trailing space semantics.
src/wrap/tests.rs
tests/wrap_unit.rs
tests/wrap_cli.rs
tests/wrap_renumber.rs
tests/cli.rs
tests/cli_frontmatter.rs
tests/code_emphasis.rs
tests/markdownlint.rs
Document the new textwrap-based wrapping architecture and trailing-space behaviour, and add an execution plan for the migration.
  • Update architecture docs to describe the tokenizer and wrap flow with textwrap::wrap_first_fit and unicode-width, replacing the bespoke tokenizer diagrams.
  • Clarify trailing space behaviour under the textwrap-backed inline wrapper in docs/trailing-spaces.md.
  • Revise README.md to mention textwrap-based line fitting while preserving Markdown-aware token grouping.
  • Add a detailed execplan document explaining constraints, risks, design decisions, and validation steps for replacing bespoke wrapping with textwrap and unicode-width.
docs/architecture.md
docs/trailing-spaces.md
README.md
docs/execplans/replace-bespoke-wrapping-with-textwrap-and-unicode-width.md
Apply small documentation formatting and style cleanups unrelated to the core wrapping logic.
  • Normalize fenced code block delimiters and language annotations (e.g., mermaid, rust,no_run) in various docs.
  • Reflow long prose lines and tighten risk/progress sections in existing execplans such as yaml-frontmatter.md.
  • Align Markdown tables and links in rust-testing-with-rstest-fixtures.md and rust-doctest-dry-guide.md for better readability.
docs/execplans/yaml-frontmatter.md
docs/rust-testing-with-rstest-fixtures.md
docs/rust-doctest-dry-guide.md
docs/documentation-style-guide.md
Add the textwrap dependency alongside existing unicode-width usage.
  • Declare textwrap = "0.16.2" in Cargo.toml, compatible with Rust 1.89 and the 2024 edition.
  • Regenerate Cargo.lock to include textwrap and any of its transitive dependencies.
Cargo.toml
Cargo.lock
Maintain the existing wrap_text and tokenize_markdown public entry points and streaming wrap pipeline while delegating line-fitting to textwrap.
  • Keep wrap_text as the high-level entry performing block classification (tables, fences, YAML frontmatter, headings, directives) and paragraph buffering.
  • Continue to expose Token and tokenize_markdown without signature changes, documenting that tokenization remains out of scope for this refactor.
  • Ensure non-wrappable lines (fenced/indented code, directives) still bypass the inline wrapping path and are emitted verbatim.
src/wrap.rs
src/lib.rs
src/textproc.rs
src/footnotes/mod.rs
src/footnotes/renumber.rs
src/code_emphasis.rs

Assessment against linked issues

Issue Objective Addressed Explanation
#80 Refactor the wrapping engine to delegate line-breaking to the textwrap crate with unicode-width-based width calculations, removing the bespoke LineBuffer-based wrapping logic and other obsolete custom wrapping internals.
#80 Consolidate prefix handling into a single helper (e.g., wrap_with_prefix) that computes prefix width once and is used by the main wrapping flow instead of multiple ad-hoc prefix functions, simplifying the main loop to call this helper.
#80 Modernize surrounding APIs and documentation for the wrapping subsystem: keep or adapt tokenization as needed, remove only wrapping-specific obsolete logic, ensure all existing wrap-related tests pass unchanged, and update docs (README, architecture, trailing-spaces, execplan) to describe the new textwrap + unicode-width based implementation.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@leynos leynos changed the title Refactor wrapping engine to use textwrap and unicode-width Refactor wrapping engine to use textwrap with unified prefix handling Apr 22, 2026
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 23, 2026

@coderabbitai Have the following failed checks now been resolved?

If further work is required, please provide an AI agent prompt for the remaining work to be done to address these failures.

Assessment against linked issues

Issue Objective Addressed Explanation
#80 Consolidate and simplify prefix handling by introducing a single helper (e.g., wrap_with_prefix) that computes available width once and applies prefixes/indents for each wrapped line, and simplify the main wrapping loop to use this helper. No changes are made to src/wrap/paragraph.rs, src/wrap.rs, or related prefix-handling code in the PR. The added document describes an intention to consolidate prefix handling, but the implementation has not been altered accordingly.
#80 Update tests and documentation so that existing tests pass unchanged under the new implementation, and docs describe the new textwrap + unicode-width based wrapping engine. The PR does not modify any test files or existing documentation such as README.md, docs/architecture.md, or docs/trailing-spaces.md. It only introduces a new planning document; there is no evidence that tests were adapted or that existing docs were updated to reflect a new implementation.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# List all files changed in this PR branch vs main
git diff --name-only origin/main...HEAD

echo "---"
# Check if wrap.rs or paragraph.rs have been modified
git diff origin/main...HEAD -- src/wrap.rs src/wrap/paragraph.rs src/wrap/inline.rs

echo "---DOCS---"
git diff origin/main...HEAD -- README.md docs/architecture.md docs/trailing-spaces.md

echo "---TESTS---"
git diff origin/main...HEAD -- src/wrap/tests.rs tests/wrap_unit.rs tests/wrap_cli.rs tests/wrap_renumber.rs

Length of output: 25500


@leynos: Both previously failed checks are now resolved. Here is the updated assessment:

Issue Objective Addressed Evidence
#80 Consolidate prefix handling via a single helper paragraph.rs now introduces wrap_with_prefix(prefix, continuation_prefix, text) that computes available width once; both append_wrapped_with_prefix and push_wrapped_segment delegate to it.
#80 Update tests and documentation README.md, docs/architecture.md, and docs/trailing-spaces.md all updated to describe the textwrap-backed engine. src/wrap/tests.rs removes dead LineBuffer tests and adds three new regression tests (wrap_text_repeats_nested_blockquote_prefix, wrap_text_aligns_footnote_url_continuation, wrap_text_preserves_checkbox_indentation).

No further work is required for these two items.

@leynos leynos marked this pull request as ready for review April 23, 2026 09:54
@coderabbitai coderabbitai Bot added the Issue label Apr 23, 2026
sourcery-ai[bot]

This comment was marked as resolved.

Copy link
Copy Markdown

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

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: 120e96d72f

ℹ️ 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/wrap/inline.rs Outdated
coderabbitai[bot]

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

@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

♻️ Duplicate comments (1)
docs/architecture.md (1)

387-391: ⚠️ Potential issue | 🟡 Minor

Correct the flow diagram to the real wrapping path.

Rename this adapter step to the actual fragment-building/post-process helpers, and point the line-fitting step at textwrap::wrap_algorithms::wrap_first_fit. The implementation does not expose an InlineTextwrapAdapter, and it does not call textwrap::wrap here, so the diagram still misstates the runtime flow.

Triage: [type:docstyle]

Run this read-only check to compare the diagram with the implementation:

#!/bin/bash
set -euo pipefail

echo "Diagram excerpt:"
sed -n '383,392p' docs/architecture.md

echo
echo "Implementation call sites:"
rg -n 'wrap_first_fit|textwrap::wrap\b|InlineTextwrapAdapter' src/wrap/inline.rs docs/architecture.md

As per coding guidelines: "Ensure that any API or behavioural changes are reflected in the documentation in docs/."

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

In `@docs/architecture.md` around lines 387 - 391, The diagram incorrectly names
and points to a non-existent InlineTextwrapAdapter and textwrap::wrap; update
the nodes in docs/architecture.md so node I is renamed to "fragment-building /
post-process helpers" (or similar) and change the line-fitting step (node J) to
reference textwrap::wrap_algorithms::wrap_first_fit instead of textwrap::wrap;
ensure the arrow flow G -> I, H -> I, I -> J, and J -> K remain but with the
corrected labels so the diagram matches the actual implementation (look for node
labels I and J in the snippet and the identifier
textwrap::wrap_algorithms::wrap_first_fit).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/execplans/replace-bespoke-wrapping-with-textwrap-and-unicode-width.md`:
- Around line 50-51: The doc line that instructs contributors to "Add `textwrap`
using a caret requirement" should be changed to instruct using implicit semver
(omit the leading caret) so it follows repository policy; update the sentence to
say something like "Add `textwrap` to Cargo.toml using implicit semver (no
leading `^`) compatible with Rust 1.89 and the 2024 edition" and ensure the
guidance references Cargo.toml and the `textwrap` dependency so contributors
know where to edit.

In `@src/wrap/inline/postprocess.rs`:
- Around line 52-54: Remove panicking expect/unwrap calls in the post-processing
path: replace the pop().expect(...) that sets previous_atomic (and the similar
expects in the 99-109 region) with non-panicking control flow that returns a
proper Result/Err or gracefully skips the fragment instead of panicking.
Concretely, change the previous_atomic assignment from
previous_line.pop().expect(...) to match previous_line.pop() { Some(prev) =>
prev, None => return Err(/* appropriate PostprocessError or contextual error */)
} (or an early continue if skipping is acceptable), and apply the same pattern
for each other expect/unwrap in this file so the function signature propagates
errors (e.g., Result<..., PostprocessError>) rather than aborting. Ensure error
variants include context about the missing fragment to aid debugging.

In `@src/wrap/tests.rs`:
- Around line 314-326: The test wrap_text_repeats_nested_blockquote_prefix
currently only checks line prefixes; update it to also validate the wrapped
payload content by either (a) stripping the repeated prefix string "> > " from
each line produced by wrap_text(&input, 80), concatenating the stripped lines
and asserting that this reassembled text equals the original unwrapped quote (or
equals the original after expected whitespace normalization), or (b) asserting
the exact expected wrapped lines array against the returned wrapped variable;
locate the test function wrap_text_repeats_nested_blockquote_prefix and add the
reassembly/assertion logic so the test fails if wrap_text drops or duplicates
content rather than only checking prefixes.

---

Duplicate comments:
In `@docs/architecture.md`:
- Around line 387-391: The diagram incorrectly names and points to a
non-existent InlineTextwrapAdapter and textwrap::wrap; update the nodes in
docs/architecture.md so node I is renamed to "fragment-building / post-process
helpers" (or similar) and change the line-fitting step (node J) to reference
textwrap::wrap_algorithms::wrap_first_fit instead of textwrap::wrap; ensure the
arrow flow G -> I, H -> I, I -> J, and J -> K remain but with the corrected
labels so the diagram matches the actual implementation (look for node labels I
and J in the snippet and the identifier
textwrap::wrap_algorithms::wrap_first_fit).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: be0bb100-6467-4e16-8fa6-e80abdaa3e46

📥 Commits

Reviewing files that changed from the base of the PR and between 120e96d and 2389052.

📒 Files selected for processing (6)
  • docs/architecture.md
  • docs/execplans/replace-bespoke-wrapping-with-textwrap-and-unicode-width.md
  • src/wrap/inline.rs
  • src/wrap/inline/postprocess.rs
  • src/wrap/tests.rs
  • tests/wrap_unit.rs

Comment thread docs/execplans/replace-bespoke-wrapping-with-textwrap-and-unicode-width.md Outdated
Comment thread src/wrap/inline/postprocess.rs Outdated
Comment thread src/wrap/tests.rs Outdated
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 23, 2026

@coderabbitai Have the following now been resolved?

Annul any requirements that violate the en-GB-oxendict spelling (-ize / -yse / -our) conventions (for example a request to replace "normalize" with "normalise" or "artefact" with "artifact"), or where the requirement unnecessarily increases cyclomatic complexity.

Please address the comments from this code review:

## Overall Comments
- The new `wrap_preserving_code` path repeatedly clones `buffer` into `candidate` and re-runs `wrap_first_fit` on the whole fragment list for each incoming fragment; consider restructuring to track the current line width or reusing previous wrap state so you don’t pay an O(n²) cost on long lines.
- The whitespace/atomic-span handling in `merge_whitespace_only_lines` and `rebalance_atomic_tails` is quite dense with nested conditionals and implicit invariants; factoring some of these checks into smaller, well-named helpers would make the wrapping heuristics easier to follow and maintain.

## Individual Comments

### Comment 1
<location path="src/wrap/inline.rs" line_range="365-368" />
<code_context>
+    let mut lines = Vec::new();
+    let mut buffer: Vec<InlineFragment> = Vec::new();
+
+    for fragment in fragments {
+        let mut candidate = buffer.clone();
+        candidate.push(fragment);
+        let wrapped = wrap_first_fit(&candidate, &[width_as_f64(width)]);
+        let raw_lines = wrapped.iter().map(|line| line.to_vec()).collect::<Vec<_>>();
+        let mut grouped_lines = merge_whitespace_only_lines(&raw_lines);
</code_context>
<issue_to_address>
**suggestion (performance):** Re-wrapping the entire prefix for each fragment with cloning leads to quadratic behavior and unnecessary allocations.

`wrap_preserving_code` clones `buffer` into `candidate` on every iteration and re-runs `wrap_first_fit` over the full prefix each time, making long paragraphs O(n²) in time and allocations. To improve this, consider an incremental strategy (e.g., only re-wrapping the last line or using a streaming/incremental wrapping API) or reuse a single `candidate` buffer and mutate it instead of cloning each time.

Suggested implementation:

```rust
    let fragments = build_fragments(&tokens);
    let mut lines = Vec::new();
    let mut buffer: Vec<InlineFragment> = Vec::new();

    for fragment in fragments {
        // Incrementally extend the current buffer instead of cloning it on every iteration.
        buffer.push(fragment);
        let wrapped = wrap_first_fit(&buffer, &[width_as_f64(width)]);

```

```rust
        let raw_lines = wrapped.iter().map(|line| line.to_vec()).collect::<Vec<_>>();
        let mut grouped_lines = merge_whitespace_only_lines(&raw_lines);
        rebalance_atomic_tails(&mut grouped_lines);

        // If everything still fits on a single line, keep the extended buffer as-is.
        if grouped_lines.len() == 1 {
            continue;
        }

```

This edit removes the per-iteration clone and allocation while preserving the existing wrapping behavior. It assumes the remainder of `wrap_preserving_code` already updates `buffer` to contain the fragments of the last (incomplete) line when multiple lines are produced. If that logic does not exist yet, you should:
1. After the `for line in &grouped_lines[..grouped_lines.len() - 1] { ... }` loop, set `buffer = grouped_lines.last().cloned().unwrap_or_default();` so that `buffer` only holds the fragments of the trailing line that may still accept more fragments.
</issue_to_address>

### Comment 2
<location path="src/wrap/inline.rs" line_range="171" />
<code_context>
-    if span.start >= span.end {
-        return;
-    }
+#[derive(Debug, Clone, PartialEq, Eq)]
+struct InlineFragment {
+    text: String,
</code_context>
<issue_to_address>
**issue (complexity):** Consider adding a `FragmentKind` enum and helper methods on `InlineFragment` so fragment classification is centralized and branching in the line post‑processing functions is greatly simplified.

You can reduce a lot of the branching and duplicated logic by making `InlineFragment` carry explicit “kind” metadata and centralizing the predicates. That keeps the current behavior but makes `merge_whitespace_only_lines` and `rebalance_atomic_tails` much easier to read/maintain.

### 1. Add `FragmentKind` and helper methods

```rust
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum FragmentKind {
    Whitespace,
    InlineCode,
    Link,
    Plain,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct InlineFragment {
    text: String,
    width: usize,
    kind: FragmentKind,
}

impl InlineFragment {
    fn new(text: String) -> Self {
        let width = UnicodeWidthStr::width(text.as_str());
        let kind = if text.chars().all(char::is_whitespace) {
            FragmentKind::Whitespace
        } else if is_inline_code_token(&text) {
            FragmentKind::InlineCode
        } else if looks_like_link(&text) {
            FragmentKind::Link
        } else {
            FragmentKind::Plain
        };
        Self { text, width, kind }
    }

    fn is_whitespace(&self) -> bool {
        self.kind == FragmentKind::Whitespace
    }

    fn is_atomic(&self) -> bool {
        matches!(self.kind, FragmentKind::InlineCode | FragmentKind::Link)
    }

    fn is_plain(&self) -> bool {
        self.kind == FragmentKind::Plain
    }
}
```

You can then simplify the construction in `build_fragments` (no behavior change):

```rust
fn build_fragments(tokens: &[String]) -> Vec<InlineFragment> {
    let mut fragments = Vec::new();
    let mut i = 0;

    while i < tokens.len() {
        let (group_end, _) = determine_token_span(tokens, i);
        let span = i..group_end;
        let mut text = String::new();
        push_span_text(&mut text, tokens, span);
        fragments.push(InlineFragment::new(text));
        i = group_end;
    }

    fragments
}
```

### 2. Use `FragmentKind` in the line post‑processing

With the kind metadata, `merge_whitespace_only_lines` and `rebalance_atomic_tails` lose most of their `chars().all(char::is_whitespace)`, `is_inline_code_token`, and `looks_like_link` calls:

```rust
fn merge_whitespace_only_lines(lines: &[Vec<InlineFragment>]) -> Vec<Vec<InlineFragment>> {
    let mut merged = Vec::with_capacity(lines.len());
    let mut pending_whitespace = Vec::new();

    for (index, mut line) in lines.iter().cloned().enumerate() {
        let is_whitespace_only = line.iter().all(|f| f.is_whitespace());

        if is_whitespace_only {
            let next_starts_atomic = lines
                .get(index + 1)
                .and_then(|next| next.first())
                .is_some_and(|f| f.is_atomic());

            let line_is_single_space = line.iter().map(|f| f.text.as_str()).collect::<String>() == " ";
            let previous_line_has_single_fragment =
                merged.last().is_some_and(|prev| prev.len() == 1);

            let mut should_carry_whitespace = !line_is_single_space;

            if line_is_single_space && !next_starts_atomic {
                if let Some(previous_line) = merged.last_mut() {
                    let should_move_previous_atomic =
                        previous_line.last().is_some_and(|f| f.is_atomic());
                    if should_move_previous_atomic {
                        let previous_atomic = previous_line.pop().unwrap();
                        pending_whitespace.push(previous_atomic);
                        if previous_line.is_empty() {
                            merged.pop();
                        }
                        should_carry_whitespace = true;
                    }
                }
            }

            if line_is_single_space && previous_line_has_single_fragment {
                should_carry_whitespace = true;
            }

            if should_carry_whitespace {
                pending_whitespace.extend(line);
            }
            continue;
        }

        if pending_whitespace.is_empty() {
            merged.push(line);
        } else {
            pending_whitespace.append(&mut line);
            merged.push(std::mem::take(&mut pending_whitespace));
        }
    }

    if !pending_whitespace.is_empty() {
        if let Some(last_line) = merged.last_mut() {
            last_line.append(&mut pending_whitespace);
        } else {
            merged.push(pending_whitespace);
        }
    }

    merged
}
```

And similarly in `rebalance_atomic_tails`:

```rust
fn rebalance_atomic_tails(lines: &mut [Vec<InlineFragment>]) {
    for index in 0..lines.len().saturating_sub(1) {
        let next = &mut lines[index + 1];
        let next_starts_with_single_space =
            next.first().is_some_and(|f| f.text == " " && f.is_whitespace());
        let next_continues_with_plain_text =
            next.get(1).is_some_and(|f| f.is_plain());

        if !next_starts_with_single_space || !next_continues_with_plain_text {
            continue;
        }

        let current = &mut lines[index];
        let should_move_atomic_tail = current.last().is_some_and(|f| f.is_atomic());
        let should_move_plain_tail =
            current.len() > 1 && current.last().is_some_and(|f| f.is_plain());

        if should_move_atomic_tail || should_move_plain_tail {
            let trailing_fragment = current.pop().unwrap();
            next.insert(0, trailing_fragment);
        }
    }
}
```

This keeps your current multi‑pass behavior and all layout rules, but:

- Removes repeated classification logic from every call site.
- Reduces boolean noise by exposing higher‑level predicates (`is_whitespace`, `is_atomic`, `is_plain`) that describe intent.
- Makes it much easier to later collapse `merge_whitespace_only_lines` and `rebalance_atomic_tails` into a single “line normalizer” if you decide to, without touching the classification logic again.
</issue_to_address>

…twrap and unicode-width

Add a detailed execution plan document describing the refactor to replace the bespoke line-wrapping engine in the mdtablefix project with the `textwrap` and `unicode-width` crates. This living document outlines the purpose, constraints, risks, test coverage, and stepwise implementation plan. It serves to guide the maintainability-focused refactor ensuring preserved observable behavior while modernizing and simplifying the wrapping engine.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check current state of inline.rs and postprocess.rs
cat src/wrap/inline.rs
echo "=== POSTPROCESS ==="
cat src/wrap/inline/postprocess.rs 2>/dev/null || echo "File not found"

Length of output: 12924


@leynos: Both comments are now resolved. Here is the updated assessment:

Comment Issue Resolved Evidence
1 – O(n²) clone Performance: per-iteration buffer.clone() in wrap_preserving_code buffer.push(fragment) is called directly; wrap_first_fit(&buffer, …) operates on the shared buffer without cloning. When the wrapped output overflows onto a new line, buffer is reset to grouped_lines.pop().unwrap_or_default(), retaining only the incomplete trailing line's fragments.
2 – FragmentKind enum Complexity: repeated classification calls in post-processing FragmentKind (Whitespace, InlineCode, Link, Plain) and classify_fragment are present. InlineFragment::new stores kind at construction time. is_whitespace(), is_atomic(), and is_plain() helper methods are defined. postprocess.rs introduces fine-grained named predicates (is_whitespace_only_line, line_starts_with_atomic, line_starts_with_single_space_then_plain, line_has_rebalanceable_tail, line_width) so neither merge_whitespace_only_lines nor rebalance_atomic_tails repeats classification logic inline.

No requirements are annulled. The original suggestions contained no en-GB-oxendict spelling violations (all uses of "normalize" etc. follow -ize). The one deviation from the suggested rebalance_atomic_tails — the added width overflow guard — is a deliberate correctness hardening from the subsequent post-wrap commit, not an unnecessary increase in cyclomatic complexity.

leynos and others added 3 commits April 23, 2026 12:07
- Replaced the custom LineBuffer-based inline wrapping with a fragment-based approach leveraging textwrap::wrap_algorithms::wrap_first_fit for line fitting.
- Preserved Markdown-aware token grouping and whitespace semantics by adapting fragment merging and suffix rebalancing.
- Removed the now-obsolete src/wrap/line_buffer.rs module.
- Updated paragraph wrapping to use the new inline wrapper with continuation prefixes.
- Added active regression tests for nested blockquotes, footnote continuation alignment, and checkbox indentation.
- Updated documentation reflecting the new wrapping strategy and integration with textwrap and unicode-width crates.
- Added textwrap crate as a dependency and adjusted Cargo files accordingly.

This improves maintainability by using a well-tested wrapping library while preserving existing formatting behaviors through a thin adaptation layer.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
Added comprehensive Mermaid sequence and flow diagrams illustrating the control flow and sequence
of the wrap_text function and its components. Included detailed explanations of how the wrapper
decides on line classification, paragraph buffering, prefix-aware wrapping, use of unicode-width
for display width measurement, and integration with the textwrap crate. These additions improve
documentation clarity around the wrapping process and internal collaboration of components.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
Ensure that the post-wrap fragment rebalancing respects line width constraints by checking the width before moving trailing fragments across lines. This prevents overflow lines that violate the width guarantees of `wrap_first_fit` and maintains correct wrapping behavior after post-processing.

- Added width-aware checks in `rebalance_atomic_tails` to avoid creating overflow lines.
- Refactored fragment classification to track fragment kinds for better whitespace and tail handling.
- Moved whitespace-only line merging and tail rebalancing logic into a dedicated `postprocess` module.
- Updated tests to verify no overflow after tail rebalancing, including adding a regression test for the `a four five` / width `6` case.
- Updated documentation and sequence diagrams to reflect the new wrapping flow and helper module usage.

This fix improves stability and correctness of inline wrapping post-processing steps.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@devboxerhub devboxerhub Bot force-pushed the refactor-text-wrapping-to-use-textwrap-ek5oxh branch from 2389052 to 6ce5bc8 Compare April 23, 2026 12:08
@leynos leynos changed the title Refactor wrapping engine to use textwrap with unified prefix handling Refactor wrapping to textwrap with adapter and unified prefix handling Apr 23, 2026
Copy link
Copy Markdown
Contributor

@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.

♻️ Duplicate comments (5)
docs/execplans/yaml-frontmatter.md (1)

99-100: ⚠️ Potential issue | 🟡 Minor

Run make nixie unconditionally in the checklist and progress log.

Replace the conditional wording and require make nixie every time so the validation flow remains deterministic and policy-compliant.

Triage: [type:docstyle]

As per coding guidelines: "**/*.md: Validate Mermaid diagrams in Markdown files by running make nixie."

Also applies to: 264-265

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

In `@docs/execplans/yaml-frontmatter.md` around lines 99 - 100, Update the
checklist and progress log entries so they require running `make nixie`
unconditionally (remove conditional phrasing like "if Mermaid content changes");
specifically edit the "checklist" and "progress log" sections to state "Run
`make nixie`" every time to ensure deterministic validation and compliance with
the `**/*.md` Mermaid validation guideline.
docs/architecture.md (1)

420-420: ⚠️ Potential issue | 🟡 Minor

Rename the InlineTextwrapAdapter step to match real implementation terms.

Use concrete names already present in the code path (for example, wrap_preserving_code and inline helpers) or explicitly label this as a conceptual stage.

Triage: [type:docstyle]

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

In `@docs/architecture.md` at line 420, The diagram step labeled
InlineTextwrapAdapter is misleading; update the docs to use the concrete
implementation names used in the code path (e.g., rename the node to
wrap_preserving_code and/or mention the inline helpers), or explicitly mark it
as a conceptual stage called "inline text-wrapping (wrap_preserving_code /
inline helpers)" so the documentation matches actual symbols like
wrap_preserving_code and any inline helper functions.
src/wrap/tests.rs (1)

342-354: ⚠️ Potential issue | 🟡 Minor

Assert payload integrity, not only prefix shape.

Reassemble the stripped payload and assert exact content so the test fails on dropped or duplicated text.

Patch
 #[test]
 fn wrap_text_repeats_nested_blockquote_prefix() {
@@
     let wrapped = wrap_text(&input, 80);
     assert!(wrapped.len() > 1);
     assert!(wrapped.iter().all(|line| line.starts_with("> > ")));
+    let payload = wrapped
+        .iter()
+        .map(|line| line.strip_prefix("> > ").expect("expected nested blockquote prefix"))
+        .collect::<String>();
+    assert_eq!(
+        payload,
+        "This nested quote contains enough text to require wrapping so that we can verify multi-level handling."
+    );
 }

As per coding guidelines: "Write unit and behavioural tests for new Rust functionality; run both before and after making any change."

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

In `@src/wrap/tests.rs` around lines 342 - 354, The current test only asserts
every wrapped line starts with the nested blockquote prefix but misses verifying
the wrapped content integrity; update the test for wrap_text to strip the "> > "
prefix from each line in wrapped (use the same prefix string used in the test),
reassemble those stripped lines into a single string (preserving
spacing/newlines the same way the original input payload would be joined) and
assert that this reassembled payload exactly equals the original input payload
(or the original input with leading prefix removed), so any dropped or
duplicated words will cause the test to fail.
docs/execplans/replace-bespoke-wrapping-with-textwrap-and-unicode-width.md (1)

50-51: ⚠️ Potential issue | 🟡 Minor

Replace the explicit caret requirement.

State this as implicit semver versioning, not a caret requirement. The current wording conflicts with the repository policy for Cargo.toml dependencies.

📝 Proposed fix
-- Add `textwrap` using a caret requirement that is compatible with the
-  repository's Rust `1.89` minimum version and existing 2024 edition build.
+- Add `textwrap` to `Cargo.toml` using implicit semver versioning (no leading
+  `^`) compatible with the repository's Rust `1.89` minimum version and
+  existing 2024 edition build.

Triage: [type:docstyle]

As per coding guidelines: "Dependency versions in Cargo.toml should make use of implicit semver caret versioning and must not explicitly include carets."

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

In `@docs/execplans/replace-bespoke-wrapping-with-textwrap-and-unicode-width.md`
around lines 50 - 51, The docs currently say "Add `textwrap` using a caret
requirement..." which explicitly mentions a caret requirement; update the
wording to describe implicit semver caret versioning instead (remove any mention
of "caret requirement") and reference the dependency name `textwrap`, the target
Rust minimum version `1.89`, and the `2024` edition/build so readers know
compatibility while conforming to the Cargo.toml policy (i.e., say "use implicit
semver versioning for `textwrap` compatible with Rust 1.89 and the 2024 edition"
rather than "use a caret requirement").
src/wrap/inline/postprocess.rs (1)

52-54: ⚠️ Potential issue | 🟠 Major

Remove the production expect calls from post-processing.

Skip the optimisation when the fragment shape is missing instead of panicking. These branches already predicate-check the shape, so any drift here should degrade gracefully rather than abort wrapping.

🛡️ Proposed fix
-                let previous_atomic = previous_line
-                    .pop()
-                    .expect("line with an atomic tail contains that fragment");
+                let Some(previous_atomic) = previous_line.pop() else {
+                    continue;
+                };
                 pending_whitespace.push(previous_atomic);
@@
-        let trailing_width = lines[index]
-            .last()
-            .map(|fragment| fragment.width)
-            .expect("line selected for tail rebalancing contains a trailing fragment");
+        let Some(trailing_width) = lines[index].last().map(|fragment| fragment.width) else {
+            continue;
+        };
         if line_width(&lines[index + 1]) + trailing_width > width {
             continue;
         }

-        let trailing_fragment = lines[index]
-            .pop()
-            .expect("line selected for tail rebalancing contains a trailing fragment");
+        let Some(trailing_fragment) = lines[index].pop() else {
+            continue;
+        };
         lines[index + 1].insert(0, trailing_fragment);

As per coding guidelines: ".expect() and .unwrap() are forbidden outside of tests. Errors must be propagated."

Also applies to: 99-109

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

In `@src/wrap/inline/postprocess.rs` around lines 52 - 54, The code uses
expect(...) on previous_line.pop() (variable previous_atomic) and elsewhere
(lines ~99-109) which causes panics; instead make these non-panicking by
checking the pop result (e.g., if let Some(previous_atomic) =
previous_line.pop() { ... } else { skip this optimisation/return early }) so the
post-processing gracefully skips the optimisation when the atomic fragment is
missing; update both the previous_atomic usage and the other expect sites in
postprocess.rs to conditional handling and ensure no unwrap/expect remains so
errors are propagated or degraded gracefully.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@docs/architecture.md`:
- Line 420: The diagram step labeled InlineTextwrapAdapter is misleading; update
the docs to use the concrete implementation names used in the code path (e.g.,
rename the node to wrap_preserving_code and/or mention the inline helpers), or
explicitly mark it as a conceptual stage called "inline text-wrapping
(wrap_preserving_code / inline helpers)" so the documentation matches actual
symbols like wrap_preserving_code and any inline helper functions.

In `@docs/execplans/replace-bespoke-wrapping-with-textwrap-and-unicode-width.md`:
- Around line 50-51: The docs currently say "Add `textwrap` using a caret
requirement..." which explicitly mentions a caret requirement; update the
wording to describe implicit semver caret versioning instead (remove any mention
of "caret requirement") and reference the dependency name `textwrap`, the target
Rust minimum version `1.89`, and the `2024` edition/build so readers know
compatibility while conforming to the Cargo.toml policy (i.e., say "use implicit
semver versioning for `textwrap` compatible with Rust 1.89 and the 2024 edition"
rather than "use a caret requirement").

In `@docs/execplans/yaml-frontmatter.md`:
- Around line 99-100: Update the checklist and progress log entries so they
require running `make nixie` unconditionally (remove conditional phrasing like
"if Mermaid content changes"); specifically edit the "checklist" and "progress
log" sections to state "Run `make nixie`" every time to ensure deterministic
validation and compliance with the `**/*.md` Mermaid validation guideline.

In `@src/wrap/inline/postprocess.rs`:
- Around line 52-54: The code uses expect(...) on previous_line.pop() (variable
previous_atomic) and elsewhere (lines ~99-109) which causes panics; instead make
these non-panicking by checking the pop result (e.g., if let
Some(previous_atomic) = previous_line.pop() { ... } else { skip this
optimisation/return early }) so the post-processing gracefully skips the
optimisation when the atomic fragment is missing; update both the
previous_atomic usage and the other expect sites in postprocess.rs to
conditional handling and ensure no unwrap/expect remains so errors are
propagated or degraded gracefully.

In `@src/wrap/tests.rs`:
- Around line 342-354: The current test only asserts every wrapped line starts
with the nested blockquote prefix but misses verifying the wrapped content
integrity; update the test for wrap_text to strip the "> > " prefix from each
line in wrapped (use the same prefix string used in the test), reassemble those
stripped lines into a single string (preserving spacing/newlines the same way
the original input payload would be joined) and assert that this reassembled
payload exactly equals the original input payload (or the original input with
leading prefix removed), so any dropped or duplicated words will cause the test
to fail.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5cbad128-c227-49c0-883f-2fc54a7978d9

📥 Commits

Reviewing files that changed from the base of the PR and between 2389052 and 6ce5bc8.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • Cargo.toml
  • README.md
  • docs/architecture.md
  • docs/execplans/replace-bespoke-wrapping-with-textwrap-and-unicode-width.md
  • docs/execplans/yaml-frontmatter.md
  • docs/trailing-spaces.md
  • src/wrap.rs
  • src/wrap/inline.rs
  • src/wrap/inline/postprocess.rs
  • src/wrap/line_buffer.rs
  • src/wrap/paragraph.rs
  • src/wrap/tests.rs
  • tests/wrap_unit.rs
💤 Files with no reviewable changes (2)
  • src/wrap.rs
  • src/wrap/line_buffer.rs

@leynos

This comment was marked as resolved.

- Added new tests to verify nested blockquote prefix repetition in wrap_text.
- Improved robustness in postprocess.rs by handling pop operations gracefully with else continue.
- Updated architecture and execplan docs to reflect wrapping improvements and dependency changes with textwrap.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@coderabbitai

This comment was marked as resolved.

Copy link
Copy Markdown
Contributor

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

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

Inline comments:
In `@docs/execplans/replace-bespoke-wrapping-with-textwrap-and-unicode-width.md`:
- Around line 183-186: Edit the sentence describing `rebalance_atomic_tails` so
it inserts a comma before "or" in the compound clause; specifically update the
line that currently reads "...must be width-aware or it can regress downstream
layout assumptions." to read "...must be width-aware, or it can regress
downstream layout assumptions." referencing the `rebalance_atomic_tails` mention
to locate the sentence.
- Around line 111-114: Edit the sentence "Risk: the architecture docs currently
describe the bespoke tokenizer flow and module relationships in detail.
Severity: low Likelihood: high Mitigation: update the docs as part of the same
change so the repository does not advertise an implementation that no longer
exists." by inserting a comma before "so" (i.e., "...update the docs as part of
the same change, so the repository...") to properly separate the two independent
clauses and improve readability.
- Around line 188-220: Every "Decision:" bullet entry in the Decision Log ends
with a "Date/Author: YYYY-MM-DD / Name" line but lacks terminal punctuation;
update each Decision entry so the "Date/Author:" line ends with a period. Locate
the Decision Log block containing the "Decision: ..." lines and the
corresponding "Date/Author:" suffixes and add a full stop to each of those lines
to ensure consistent terminal punctuation across entries.

In `@src/wrap/inline/postprocess.rs`:
- Around line 91-110: Add unit tests for rebalance_atomic_tails that exercise
its mutation logic using InlineFragment arrays: write tests that cover empty
lines, single-fragment lines, lines whose last fragment is rebalanceable and not
rebalanceable (use line_has_rebalanceable_tail), cases where the next line
starts with a single-space plain fragment
(line_starts_with_single_space_then_plain) and where it does not, and width
boundary conditions where line_width + trailing_width is equal to, less than, or
greater than the given width; assert that the trailing fragment is moved into
the next line only when all conditions are met and that other cases leave lines
unchanged. Ensure tests include fragments with varying width values to validate
the width checks and verify behavior when popping/inserting the
trailing_fragment.
- Around line 28-89: Add focused unit tests for merge_whitespace_only_lines that
cover all conditional branches: create inputs using Vec<InlineFragment> with
FragmentKind variants to exercise whitespace-only lines, single-space lines
(is_single_space_line), lines that start with atomic fragments
(line_starts_with_atomic), the inline-code-tail removal path (previous line last
fragment with FragmentKind::InlineCode), the previous-line-single-fragment path,
carry-over of pending_whitespace into the next non-empty line, and the final
pending_whitespace append behavior when the input ends with whitespace-only
lines; place tests in the same module as merge_whitespace_only_lines (or a tests
submodule) and assert the exact merged Vec<Vec<InlineFragment>> shapes and
fragment ordering for each scenario to ensure each branch behaves as expected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 570989e3-ed33-4140-be1f-383777928446

📥 Commits

Reviewing files that changed from the base of the PR and between 6ce5bc8 and acb7a03.

📒 Files selected for processing (4)
  • docs/architecture.md
  • docs/execplans/replace-bespoke-wrapping-with-textwrap-and-unicode-width.md
  • src/wrap/inline/postprocess.rs
  • src/wrap/tests.rs

Comment thread docs/execplans/replace-bespoke-wrapping-with-textwrap-and-unicode-width.md Outdated
Comment thread src/wrap/inline/postprocess.rs
Comment thread src/wrap/inline/postprocess.rs
@coderabbitai

This comment was marked as resolved.

…pping

- Delegate line fitting to `textwrap::wrap_algorithms::wrap_first_fit` for greedy, efficient wrapping
- Introduce `InlineFragment` holding display width and fragment kind for correct Unicode width handling
- Centralize prefix handling and line wrapping in `ParagraphWriter` with prefix-aware width calculation
- Add post-processing to merge whitespace-only lines and rebalance atomic tails
- Add detailed ADR explaining rationale and design decisions
- Update developers and users guides with architecture and usage details
- Maintain API stability, improve correctness for non-ASCII text and markdown inline elements
- Add extensive tests verifying wrapping behavior, post-processing, and prefix handling

This removes the fragile, bespoke LineBuffer state machine and relies on a well-tested upstream crate, improving correctness and maintainability.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@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

♻️ Duplicate comments (1)
src/wrap/tests.rs (1)

158-164: ⚠️ Potential issue | 🟡 Minor

Strengthen the new prefix regressions.

Route wrap_with_prefix_single_line through wrap_text with an actual prefix, and assert exact wrapped output in the multiline cases. The current checks can still pass if the prefix path regresses, drops payload, or fails to split at all.

As per coding guidelines: "Write unit and behavioural tests for new Rust functionality; run both before and after making any change."

Also applies to: 350-356, 391-396

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

In `@src/wrap/tests.rs` around lines 158 - 164, The test
wrap_with_prefix_single_line currently calls wrap_preserving_code directly and
doesn't exercise prefix handling; update it to call wrap_text with a real prefix
(e.g., ">> ") and assert the exact output including the prefix so regressions in
prefix handling or payload splitting are caught; do the same strengthening for
the related tests around lines 350-356 and 391-396 by replacing direct
wrap_preserving_code calls with wrap_text using an explicit prefix and asserting
the exact wrapped lines (including prefix and line breaks) to validate multiline
wrapping behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/adrs/0002-textwrap-wrapping-engine.md`:
- Around line 27-30: Update the ADR to use Oxford -ize spellings: replace
"normalises" with "normalizes", "centralised" with "centralized", and
"centralises" with "centralizes" in the text that references
src/wrap/inline/postprocess.rs and ParagraphWriter::wrap_with_prefix, and apply
the same replacements for the other occurrences noted (around the later block
mentioned as "also applies to: 46-48"); ensure all instances in this ADR use the
en-GB -ize forms consistently.

In `@docs/developers-guide.md`:
- Around line 115-124: The API reference table is missing a caption; insert a
short caption line immediately before the Markdown table (the block that lists
symbols such as FragmentKind, InlineFragment, classify_fragment,
build_fragments, wrap_preserving_code, merge_whitespace_only_lines,
rebalance_atomic_tails, ParagraphWriter, wrap_with_prefix, ParagraphState,
PrefixLine) — e.g. a one-line caption like "Table: Key types and functions" — so
the table has an explicit caption and meets the documentation guideline to
caption every table or diagram.
- Around line 101-113: The docs use British Oxford -ize convention but currently
contain "tokenised" and "normalise"; update those to "tokenized" and "normalize"
respectively so prose matches the repository's en-GB-oxendict -ize rule and
avoids mixing spellings next to the tokenize::segment_inline symbol; ensure
occurrences in the new wrapping section around references to
tokenize::segment_inline, build_fragments, InlineFragment, classify_fragment,
wrap_preserving_code, textwrap::wrap_algorithms::wrap_first_fit,
merge_whitespace_only_lines and rebalance_atomic_tails are all changed.

In `@src/wrap/tests.rs`:
- Around line 350-441: The test module has grown past the 400-line limit;
extract the prefix/continuation tests
(wrap_with_prefix_multiline_uses_continuation,
wrap_text_repeats_nested_blockquote_prefix,
wrap_with_prefix_plain_indent_both_lines,
wrap_text_preserves_prefixed_continuation_alignment) into a new test module/file
(e.g., a submodule tests::prefix or a new integration test file) and update any
use/imports so wrap_text is visible to the new module; keep the tests unchanged
aside from their module relocation so behavior and assertions remain identical.

---

Duplicate comments:
In `@src/wrap/tests.rs`:
- Around line 158-164: The test wrap_with_prefix_single_line currently calls
wrap_preserving_code directly and doesn't exercise prefix handling; update it to
call wrap_text with a real prefix (e.g., ">> ") and assert the exact output
including the prefix so regressions in prefix handling or payload splitting are
caught; do the same strengthening for the related tests around lines 350-356 and
391-396 by replacing direct wrap_preserving_code calls with wrap_text using an
explicit prefix and asserting the exact wrapped lines (including prefix and line
breaks) to validate multiline wrapping behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 263916e4-e3e3-4cec-80fa-cd2e08a88671

📥 Commits

Reviewing files that changed from the base of the PR and between acb7a03 and 4169227.

📒 Files selected for processing (7)
  • docs/adrs/0002-textwrap-wrapping-engine.md
  • docs/developers-guide.md
  • docs/users-guide.md
  • src/wrap/inline.rs
  • src/wrap/inline/postprocess.rs
  • src/wrap/paragraph.rs
  • src/wrap/tests.rs

Comment thread docs/adrs/0002-textwrap-wrapping-engine.md
Comment thread docs/developers-guide.md Outdated
Comment thread docs/developers-guide.md
Comment thread src/wrap/tests.rs Outdated
This large refactor removes the old custom inline wrapping implementation and integrates the textwrap crate's `wrap_first_fit` algorithm combined with unicode-width for accurate display width calculations.

Highlights:
- New `InlineFragment` model carries precomputed unicode display widths and fragment kinds (Whitespace, InlineCode, Link, Plain).
- Wrapping logic uses markdown-aware fragmentation to avoid splitting inline code spans and links.
- Postprocessing merges whitespace-only lines and rebalances atomic tails to preserve legacy spacing semantics.
- Paragraph wrapping now centralizes prefix width calculation and continuation prefixes to correctly align wrapped output.
- Extensive test additions ensure behavior preservation and correctness across prefixes, code spans, links, and whitespace handling.

Docs and guides updated to reflect the new wrapping architecture and pipeline stages, providing clarity on block classification, inline fragment construction, line fitting, and rendering steps.

Overall this improves maintainability, correctness, and consistent unicode support for markdown wrapping.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

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

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

Inline comments:
In `@docs/users-guide.md`:
- Around line 41-52: In the sentences "subsequent wrapped lines are indented to
the same visual column so the text stays aligned." (in the paragraph about
wrapped lines and prefixes) and "`mdtablefix --wrap` preserves those trailing
spaces on the final wrapped line so hard-break semantics are not lost after
reformatting." add a comma before "so" in each compound sentence so they read
"...visual column, so the text stays aligned." and "...final wrapped line, so
hard-break semantics are not lost after reformatting." This is the only change
needed: insert the comma before each "so" to separate the independent clauses.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: a5d2236c-89ae-4595-b5e8-9705cc8c7d29

📥 Commits

Reviewing files that changed from the base of the PR and between 4169227 and 665ab56.

📒 Files selected for processing (11)
  • docs/adrs/0002-textwrap-wrapping-engine.md
  • docs/developers-guide.md
  • docs/execplans/replace-bespoke-wrapping-with-textwrap-and-unicode-width.md
  • docs/users-guide.md
  • src/wrap/inline.rs
  • src/wrap/inline/postprocess.rs
  • src/wrap/inline/test_support.rs
  • src/wrap/inline/tests.rs
  • src/wrap/paragraph.rs
  • src/wrap/tests.rs
  • src/wrap/tests/prefix.rs

Comment thread docs/users-guide.md Outdated
@leynos
Copy link
Copy Markdown
Owner Author

leynos commented Apr 23, 2026

@coderabbitai pause

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

✅ Actions performed

Reviews paused.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

@coderabbitai

This comment was marked as resolved.

…-export

- Re-export FenceTracker and is_fence in src/wrap.rs for downstream use.
- Refactor tokenize_markdown to improve inline markdown tokenization logic.
- Extract and relocate push_newline_if_needed function within tokenize module.

These changes improve fence detection utilities and streamline tokenization logic for inline markdown processing.

Co-authored-by: devboxerhub[bot] <devboxerhub[bot]@users.noreply.github.com>
@leynos leynos changed the title Refactor wrapping to textwrap with adapter and unified prefix handling Switch wrapping to textwrap with adapter and unified prefixes Apr 23, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 23, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== src/wrap.rs (full) ==="
cat src/wrap.rs

echo ""
echo "=== tokenize_markdown source declaration ==="
fd -e rs . src/wrap/ | xargs grep -l "fn tokenize_markdown" 2>/dev/null
grep -n -B8 "fn tokenize_markdown\|pub fn tokenize_markdown" src/wrap/tokenize.rs 2>/dev/null || \
  fd -e rs . src/ | xargs grep -rn -B8 "fn tokenize_markdown" 2>/dev/null | head -40

echo ""
echo "=== fence.rs - FenceTracker and is_fence doc comments ==="
grep -n -B3 "pub struct FenceTracker\|pub fn is_fence" src/wrap/fence.rs 2>/dev/null | head -30

echo ""
echo "=== wrap_preserving_code direct tests ==="
grep -n "wrap_preserving_code\|#\[test\]\|fn test_\|fn wrap_" src/wrap/tests.rs | head -60

echo ""
echo "=== inline.rs test module for wrap_preserving_code ==="
grep -n "wrap_preserving_code\|#\[test\]\|fn " src/wrap/inline.rs | grep -A1 "#\[test\]" | head -40
grep -n "wrap_preserving_code" src/wrap/inline.rs src/wrap/tests.rs tests/wrap_unit.rs 2>/dev/null | head -20

echo ""
echo "=== src/wrap/inline/tests.rs ==="
cat src/wrap/inline/tests.rs 2>/dev/null || echo "FILE NOT FOUND"

Length of output: 10537


@leynos: Both checks are now resolved. Here is the updated assessment:

Check Resolved Evidence
Testing src/wrap/tests.rs imports wrap_preserving_code directly and contains five named unit-test functions calling it — wrap_preserving_code_splits_after_consecutive_whitespace, wrap_preserving_code_glues_punctuation_after_code, wrap_preserving_code_breaks_between_inline_code_spans, wrap_preserving_code_retains_punctuation_after_separate_spans, wrap_preserving_code_preserves_carry_whitespace — plus two additional call sites at Lines 295 and 311.
Module-Level Documentation pub use fence::{FenceTracker, is_fence}; in src/wrap.rs now carries a full /// block explaining both re-exported items with rustdoc links. tokenize_markdown is re-exported with #[doc(inline)], and its source definition in src/wrap/tokenize/mod.rs (Line 273) has comprehensive /// docs including a code example, so the re-export inherits full documentation.

No further work is required for either check.

@leynos leynos merged commit 2de1134 into main Apr 23, 2026
3 checks passed
@leynos leynos deleted the refactor-text-wrapping-to-use-textwrap-ek5oxh branch April 23, 2026 22:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Refactor text wrapping and prefix logic to use textwrap and unicode-width crates

1 participant