Skip to content

Add Bun.wrapAnsi() for text wrapping with ANSI escape code preservation - #26061

Merged
Jarred-Sumner merged 20 commits into
mainfrom
claude/add-wrap-ansi
Jan 17, 2026
Merged

Jarred-Sumner merged 20 commits into
mainfrom
claude/add-wrap-ansi

Conversation

@sosukesuzuki

@sosukesuzuki sosukesuzuki commented Jan 14, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds Bun.wrapAnsi(), a native implementation of the popular wrap-ansi npm package for wrapping text with ANSI escape codes.

API

Bun.wrapAnsi(string: string, columns: number, options?: WrapAnsiOptions): string

interface WrapAnsiOptions {
  hard?: boolean;              // default: false - Break words longer than columns
  wordWrap?: boolean;          // default: true - Wrap at word boundaries
  trim?: boolean;              // default: true - Trim leading/trailing whitespace
  ambiguousIsNarrow?: boolean; // default: true - Treat ambiguous-width chars as narrow
}

Features

  • Wraps text to fit within specified column width
  • Preserves ANSI escape codes (SGR colors/styles)
  • Supports OSC 8 hyperlinks
  • Respects Unicode display widths (full-width characters, emoji)
  • Normalizes \r\n to \n

Implementation Details

The implementation closes and reopens ANSI codes around line breaks for robust terminal compatibility. This differs slightly from the npm package in edge cases but produces visually equivalent output.

Behavioral Differences from npm wrap-ansi

  1. ANSI code preservation: Bun always maintains complete ANSI escape sequences. The npm version can output malformed codes (missing ESC character) in certain edge cases with wordWrap: false, trim: false.

  2. Newline ANSI handling: Bun closes and reopens ANSI codes around newlines for robustness. The npm version sometimes keeps them spanning across newlines. The visual output is equivalent.

Tests

  • 27 custom tests covering basic functionality, ANSI codes, Unicode, and options
  • 23 tests ported from the npm package (MIT licensed, credited in file header)
  • All 50 tests pass

Benchmark

$ cd /Users/sosuke/code/bun/bench && ../build/release/bun snippets/wrap-ansi.js
clk: ~3.82 GHz
cpu: Apple M4 Max
runtime: bun 1.3.7 (arm64-darwin)

benchmark                    avg (min … max) p75   p99    (min … top 1%)
-------------------------------------------- -------------------------------
Short text (45 chars) - npm    25.81 µs/iter  21.71 µs  █
                      (16.79 µs … 447.38 µs) 110.96 µs ▆█▃▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁
Short text (45 chars) - Bun   685.55 ns/iter 667.00 ns    █
                       (459.00 ns … 2.16 ms)   1.42 µs ▁▁▁█▃▂▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁

summary
  Short text (45 chars) - Bun
   37.65x faster than Short text (45 chars) - npm

-------------------------------------------- -------------------------------
Medium text (810 chars) - npm 568.12 µs/iter 578.00 µs  ▄▅█▆▆▃
                     (525.25 µs … 944.71 µs) 700.75 µs ▄██████▆▅▄▃▃▂▂▂▁▁▁▁▁▁
Medium text (810 chars) - Bun  11.22 µs/iter  11.28 µs                     █
                       (11.04 µs … 11.46 µs)  11.33 µs █▁▁▁██▁█▁▁▁▁█▁█▁▁█▁▁█

summary
  Medium text (810 chars) - Bun
   50.62x faster than Medium text (810 chars) - npm

-------------------------------------------- -------------------------------
Long text (8100 chars) - npm    7.66 ms/iter   7.76 ms     ▂▂▅█   ▅
                         (7.31 ms … 8.10 ms)   8.06 ms ▃▃▄▃█████▇▇███▃▆▆▆▄▁▃
Long text (8100 chars) - Bun  112.14 µs/iter 113.50 µs        █
                     (102.50 µs … 146.04 µs) 124.92 µs ▁▁▁▁▁▁██▇▅█▃▂▂▂▂▁▁▁▁▁

summary
  Long text (8100 chars) - Bun
   68.27x faster than Long text (8100 chars) - npm

-------------------------------------------- -------------------------------
Colored short - npm            28.46 µs/iter  28.56 µs              █
                       (27.90 µs … 29.34 µs)  28.93 µs ▆▁▆▁▁▆▁▁▆▆▆▁▆█▁▁▁▁▁▁▆
Colored short - Bun           861.64 ns/iter 867.54 ns         ▂  ▇█▄▂
                     (839.68 ns … 891.12 ns) 882.04 ns ▃▅▄▅▆▆▇▆██▇████▆▃▅▅▅▂

summary
  Colored short - Bun
   33.03x faster than Colored short - npm

-------------------------------------------- -------------------------------
Colored medium - npm          557.84 µs/iter 562.63 µs      ▂▃█▄
                     (508.08 µs … 911.92 µs) 637.96 µs ▁▁▁▂▄█████▅▂▂▁▁▁▁▁▁▁▁
Colored medium - Bun           14.91 µs/iter  14.94 µs ██  ████ ██ █      ██
                       (14.77 µs … 15.17 µs)  15.06 µs ██▁▁████▁██▁█▁▁▁▁▁▁██

summary
  Colored medium - Bun
   37.41x faster than Colored medium - npm

-------------------------------------------- -------------------------------
Colored long - npm              7.84 ms/iter   7.90 ms       █  ▅
                         (7.53 ms … 8.38 ms)   8.19 ms ▂▂▂▄▃▆██▇██▇▃▂▃▃▃▄▆▂▂
Colored long - Bun            176.73 µs/iter 175.42 µs       █
                       (162.50 µs … 1.37 ms) 204.46 µs ▁▁▂▄▇██▅▂▂▂▁▁▁▁▁▁▁▁▁▁

summary
  Colored long - Bun
   44.37x faster than Colored long - npm

-------------------------------------------- -------------------------------
Hard wrap long - npm            8.05 ms/iter   8.12 ms       ▃ ▇█
                         (7.67 ms … 8.53 ms)   8.50 ms ▄▁▁▁▃▄█████▄▃▂▆▄▃▂▂▂▂
Hard wrap long - Bun          111.85 µs/iter 112.33 µs         ▇█
                     (101.42 µs … 145.42 µs) 123.88 µs ▁▁▁▁▁▁▁████▄▃▂▂▂▁▁▁▁▁

summary
  Hard wrap long - Bun
   72.01x faster than Hard wrap long - npm

-------------------------------------------- -------------------------------
Hard wrap colored - npm         8.82 ms/iter   8.92 ms   ▆ ██
                         (8.55 ms … 9.47 ms)   9.32 ms ▆▆████▆▆▄▆█▄▆▄▄▁▃▁▃▄▃
Hard wrap colored - Bun       174.38 µs/iter 175.54 µs   █ ▂
                     (165.75 µs … 210.25 µs) 199.50 µs ▁▃█▆███▃▂▃▂▂▂▂▂▁▁▁▁▁▁

summary
  Hard wrap colored - Bun
   50.56x faster than Hard wrap colored - npm

-------------------------------------------- -------------------------------
Japanese (full-width) - npm    51.00 µs/iter  52.67 µs    █▂   █▄
                      (40.71 µs … 344.88 µs)  66.13 µs ▁▁▃██▄▃▅██▇▄▃▄▃▂▂▁▁▁▁
Japanese (full-width) - Bun     7.46 µs/iter   7.46 µs       █
                        (6.50 µs … 34.92 µs)   9.38 µs ▁▁▁▁▁██▆▂▁▂▁▁▁▁▁▁▁▁▁▁

summary
  Japanese (full-width) - Bun
   6.84x faster than Japanese (full-width) - npm

-------------------------------------------- -------------------------------
Emoji text - npm              173.63 µs/iter 222.17 µs   █
                     (129.42 µs … 527.25 µs) 249.58 µs ▁▃█▆▃▃▃▁▁▁▁▁▁▁▂▄▆▄▂▂▁
Emoji text - Bun                9.42 µs/iter   9.47 µs           ██
                         (9.32 µs … 9.52 µs)   9.50 µs █▁▁███▁▁█▁██▁▁▁▁██▁▁█

summary
  Emoji text - Bun
   18.44x faster than Emoji text - npm

-------------------------------------------- -------------------------------
Hyperlink (OSC 8) - npm       208.00 µs/iter 254.25 µs   █
                     (169.58 µs … 542.17 µs) 281.00 µs ▁▇█▃▃▂▂▂▁▁▁▁▁▁▁▃▃▅▃▂▁
Hyperlink (OSC 8) - Bun         6.00 µs/iter   6.06 µs      █           ▄
                         (5.88 µs … 6.11 µs)   6.10 µs ▅▅▅▁▅█▅▁▅▁█▁▁▅▅▅▅█▅▁█

summary
  Hyperlink (OSC 8) - Bun
   34.69x faster than Hyperlink (OSC 8) - npm

-------------------------------------------- -------------------------------
No trim long - npm              8.32 ms/iter   8.38 ms  █▇
                        (7.61 ms … 13.67 ms)  11.74 ms ▃████▄▂▃▂▂▃▁▁▁▁▁▁▁▁▁▂
No trim long - Bun             93.92 µs/iter  94.42 µs           █▂
                      (82.75 µs … 162.38 µs) 103.83 µs ▁▁▁▁▁▁▁▁▄███▄▃▂▂▁▁▁▁▁

summary
  No trim long - Bun
   88.62x faster than No trim long - npm

@sosukesuzuki
sosukesuzuki requested a review from alii as a code owner January 14, 2026 09:10
@robobun

robobun commented Jan 14, 2026 •

Copy link
Copy Markdown
Collaborator
Updated 4:45 AM PT - Jan 16th, 2026

❌ @autofix-ci[bot], your commit 6a4674a has 3 failures in Build #35017 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 26061

That installs a local version of the PR into your bun-26061 executable, so you can run:

bun-26061 --bun

@coderabbitai

coderabbitai Bot commented Jan 14, 2026 •

Copy link
Copy Markdown
Contributor

Walkthrough

Adds Bun.wrapAnsi with full C++ binding and helpers, TypeScript types, tests, benchmarks, width/ANSI utilities, and a bench dependency for wrap-ansi; wires a new JS export and exposes low-level visible-width functions to native code.

Changes

Cohort / File(s) Summary
API Surface & Types
bench/runner.mjs, packages/bun-types/bun.d.ts
Added summary export in bench runner; added WrapAnsiOptions and wrapAnsi(input, columns, options?) declaration in bun.d.ts.
C++ Binding & LUT
src/bun.js/bindings/wrapAnsi.cpp, src/bun.js/bindings/wrapAnsi.h, src/bun.js/bindings/BunObject.cpp
Implemented host function jsFunctionBunWrapAnsi, WrapAnsiOptions handling, encoding paths (UTF‑16/Latin1), and added wrapAnsi entry to BunObject LUT.
ANSI Parsing Helpers
src/bun.js/bindings/ANSIHelpers.h, src/bun.js/bindings/stripANSI.cpp
Introduced ANSI::isEscapeCharacter / findEscapeCharacter / consumeANSI; refactored stripANSI.cpp to use these helpers.
Width Calculation Exports
src/string/immutable/visible.zig
Exported Bun__visibleWidthExcludeANSI_utf8/utf16/latin1 and Bun__codepointWidth for native width computations.
Core Wrapping Logic
src/bun.js/bindings/wrapAnsi.cpp
Full wrapping implementation: row management, word wrapping, ANSI/OSC8/SGR state preservation, trimming, per-line processing, and join logic.
Tests
test/js/bun/util/wrapAnsi.test.ts, test/js/bun/util/wrapAnsi.npm.test.ts
Added extensive unit tests covering wrapping modes, ANSI preservation, Unicode (fullwidth/emoji/surrogates), hyperlinks, trim/wordWrap/hard options, and edge cases.
Benchmarks & Dependency
bench/package.json, bench/snippets/wrap-ansi.js
Added wrap-ansi dependency to bench/package.json and a new benchmark script exercising multiple text scenarios against npm and Bun implementations.
Minor Import Reorder
src/bun.js/webcore/Request.zig
Reordered FetchRedirect import relative to FetchCacheMode (no behavior change).

Possibly related PRs

Suggested reviewers

  • alii
  • zackradisic
  • taylordotfish
🚥 Pre-merge checks | ✅ 1 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Description check ❓ Inconclusive The description provides comprehensive details including API signature, features, implementation notes, behavioral differences from npm, test coverage, and benchmark results. However, it does not follow the repository's required template structure with 'What does this PR do?' and 'How did you verify your code works?' sections. Restructure the description to explicitly follow the template sections: move content into 'What does this PR do?' and 'How did you verify your code works?' for consistency with repository standards.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main feature added: Bun.wrapAnsi() for wrapping text while preserving ANSI escape codes, which is the primary focus of the changeset.

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



📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f9f6935 and 6a4674a.

📒 Files selected for processing (1)
  • src/bun.js/webcore/Request.zig
🧰 Additional context used
📓 Path-based instructions (1)
src/**/*.zig

📄 CodeRabbit inference engine (src/CLAUDE.md)

src/**/*.zig: Use the # prefix for private fields in Zig structs, e.g., struct { #foo: u32 };
Use Decl literals in Zig, e.g., const decl: Decl = .{ .binding = 0, .value = 0 };
Place @import statements at the bottom of the file in Zig (auto formatter will handle positioning)
Never use @import() inline inside functions in Zig; always place imports at the bottom of the file or containing struct

Files:

  • src/bun.js/webcore/Request.zig
🧠 Learnings (6)
📓 Common learnings
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24212
File: src/cli/publish_command.zig:782-788
Timestamp: 2025-10-30T21:52:04.707Z
Learning: In the Bun codebase (oven-sh/bun), `enable_ansi_colors` flags are used to gate both ANSI color codes and Unicode box-drawing characters/emoji. This is the established pattern across the codebase.
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/js/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:37:11.466Z
Learning: Write JS builtins for Bun's Node.js compatibility and APIs, and run `bun bd` after changes
📚 Learning: 2026-01-07T23:45:24.551Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/CLAUDE.md:0-0
Timestamp: 2026-01-07T23:45:24.551Z
Learning: Applies to src/**/*.zig : Place `import` statements at the bottom of the file in Zig (auto formatter will handle positioning)

Applied to files:

  • src/bun.js/webcore/Request.zig
📚 Learning: 2026-01-07T23:45:24.551Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/CLAUDE.md:0-0
Timestamp: 2026-01-07T23:45:24.551Z
Learning: Applies to src/**/*.zig : Never use `import()` inline inside functions in Zig; always place imports at the bottom of the file or containing struct

Applied to files:

  • src/bun.js/webcore/Request.zig
📚 Learning: 2025-09-06T03:37:41.154Z
Learnt from: taylordotfish
Repo: oven-sh/bun PR: 22229
File: src/bundler/LinkerGraph.zig:0-0
Timestamp: 2025-09-06T03:37:41.154Z
Learning: In Bun's codebase, when checking import record source indices in src/bundler/LinkerGraph.zig, prefer using `if (import_index >= self.import_records.len)` bounds checking over `isValid()` checks, as the bounds check is more robust and `isValid()` is a strict subset of this condition.

Applied to files:

  • src/bun.js/webcore/Request.zig
📚 Learning: 2025-08-30T09:09:18.384Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 22231
File: src/bundler/bundle_v2.zig:48-48
Timestamp: 2025-08-30T09:09:18.384Z
Learning: In Zig, when a module exports a top-level struct, import("./Module.zig") directly returns that struct type and can be used as a type alias without needing to access a field within the module. This is a common pattern in the Bun codebase.

Applied to files:

  • src/bun.js/webcore/Request.zig
📚 Learning: 2026-01-05T16:32:07.551Z
Learnt from: alii
Repo: oven-sh/bun PR: 25474
File: src/bun.js/event_loop/Sigusr1Handler.zig:0-0
Timestamp: 2026-01-05T16:32:07.551Z
Learning: In Zig codebases (e.g., Bun), treat std.posix.sigaction as returning void and do not perform runtime error handling for its failure. The Zig standard library views sigaction failures as programmer errors (unreachable) because they only occur with invalid signals like SIGKILL/SIGSTOP. Apply this pattern across Zig files that call sigaction (e.g., crash_handler.zig, main.zig, filter_run.zig, process.zig) and ensure failures are not handled as recoverable errors; prefer reaching an explicit unreachable/compile-time assumption when such failures are detected.

Applied to files:

  • src/bun.js/webcore/Request.zig
🔇 Additional comments (1)
src/bun.js/webcore/Request.zig (1)

1093-1093: LGTM! Import reordering from auto-formatter.

This is a benign import reordering change, likely from running the Zig auto-formatter. The imports remain at the bottom of the file as per coding guidelines, and there's no functional impact.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.


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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🤖 Fix all issues with AI agents
In `@packages/bun-types/bun.d.ts`:
- Around line 613-645: The JSDoc for WrapAnsiOptions.wordWrap is incorrect: when
false it still wraps to fill the column and will split words at character
boundaries rather than only breaking at explicit newlines; update the JSDoc on
the wordWrap property (interface WrapAnsiOptions, symbol wordWrap) to say that
true prefers wrapping at word boundaries when possible, and false performs
character-level wrapping to fill each line and will split words as needed (do
not claim it only wraps at explicit newlines).

In `@src/string/immutable/wrap_ansi.zig`:
- Around line 25-38: Replace the byte-by-byte CRLF normalization loop with a
bulk replace using std.mem.replace: allocate or create a mutable copy of input
(used in place of the manual append loop), call std.mem.replace to replace the
pattern "\r\n" with "\n" into the normalized buffer (reference symbols:
normalized, input, std.mem.replace), and adjust the resulting slice/length based
on the replace result (account for the number of replacements to compute the
final normalized length) before using/deinitializing normalized; this keeps
behavior identical but is more efficient for large inputs.
- Around line 193-216: The code currently swallows allocation failures with
`catch {}` when appending to `new_row` and `trailing_ansi`, risking silent
corruption; change those `catch {}` usages to propagate errors (use
`try`/`return`/! as appropriate) for all allocator operations involving
`new_row.append`, `new_row.appendSlice`, and `trailing_ansi.append`, and ensure
the final `new_row.appendSlice(allocator, trailing_ansi.items)` also propagates
errors; adjust cleanup so `trailing_ansi.deinit(allocator)` and
`row.deinit(allocator)` still run on error (use defer or explicit cleanup) and
update the function's caller to handle the propagated error (the caller that
invokes this wrap/ANSI routine) accordingly.

In `@test/js/bun/util/wrapAnsi.npm.test.ts`:
- Around line 25-38: The regex literals in stripAnsi and hasAnsi use raw control
characters (ESC/BEL) which Biome flags; update both to use RegExp constructors
built from string patterns instead (e.g., build the ESC as "\\u001B" and BEL as
"\\u0007" inside the pattern string) so the patterns remain identical but avoid
embedding control chars; modify stripAnsi to use new RegExp("...","g") and
hasAnsi to use new RegExp("...") while preserving their original flags and
replacements.

In `@test/js/bun/util/wrapAnsi.test.ts`:
- Around line 165-177: The tests currently only assert Bun.wrapAnsi returns a
string but don't verify wrapping changes when ambiguousIsNarrow toggles; update
the "ambiguousIsNarrow option" tests to call Bun.wrapAnsi with the same input
(e.g., "αβγ" and a specific column width) for both the default (or explicit
ambiguousIsNarrow: true) and ambiguousIsNarrow: false, then assert the two
returned strings differ (and optionally assert expected line breaks or lengths
to demonstrate wide vs narrow behavior), referencing Bun.wrapAnsi in the
"default treats ambiguous as narrow" and "ambiguousIsNarrow false treats as
wide" cases so the test fails if wrapping behavior doesn't change.
- Around line 145-163: The tests in wrapAnsi.test.ts only assert the return type
and not correctness; update the "handles tabs", "handles Windows line endings",
and "handles consecutive spaces" tests to assert concrete expected outputs from
Bun.wrapAnsi: for example call Bun.wrapAnsi("a\tb", 10) and
expect(result).toBe("a\tb"), call Bun.wrapAnsi("hello\r\nworld", 10) and
expect(result).toBe("hello\r\nworld") (or assert that CRLF is preserved with
expect(result).toContain("\r\n") if normalization is intended), and call
Bun.wrapAnsi("hello    world", 10) and expect(result).toBe("hello    world") (or
assert the sequence of spaces is preserved with
expect(result).toMatch(/hello\s{4}world/)); modify whichever of these match the
library's intended behavior and replace the typeof assertions with these
concrete expects referencing Bun.wrapAnsi and the test names.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 967a6a2 and f55246b.

⛔ Files ignored due to path filters (1)
  • bench/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (10)
  • bench/package.json
  • bench/runner.mjs
  • bench/snippets/wrap-ansi.js
  • packages/bun-types/bun.d.ts
  • src/bun.js/api/BunObject.bind.ts
  • src/bun.js/api/BunObject.zig
  • src/bun.js/bindings/BunObject.cpp
  • src/string/immutable/wrap_ansi.zig
  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.test.ts?(x)

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.test.ts?(x): Never use bun test directly - always use bun bd test to run tests with debug build changes
For single-file tests, prefer -e flag over tempDir
For multi-file tests, prefer tempDir and Bun.spawn over single-file tests
Use normalizeBunSnapshot to normalize snapshot output of tests
Never write tests that check for 'panic', 'uncaught exception', or similar strings in test output
Use tempDir from harness to create temporary directories - do not use tmpdirSync or fs.mkdtempSync
When spawning processes in tests, expect stdout before expecting exit code for more useful error messages on test failure
Do not write flaky tests - do not use setTimeout in tests; instead await the condition to be met
Verify tests fail with USE_SYSTEM_BUN=1 bun test <file> and pass with bun bd test <file> - tests are invalid if they pass with USE_SYSTEM_BUN=1
Test files must end with .test.ts or .test.tsx
Avoid shell commands like find or grep in tests - use Bun's Glob and built-in tools instead

Files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
test/**/*.test.ts?(x)

📄 CodeRabbit inference engine (CLAUDE.md)

Always use port: 0 in tests - do not hardcode ports or use custom random port number functions

Files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}: Use bun bd test <...test file> to run tests with compiled code changes. Do not use bun test as it will not include your changes.
Use bun:test for files ending in *.test.{ts,js,jsx,tsx,mjs,cjs}. For test files without .test extension in test/js/node/test/{parallel,sequential}/*.js, use bun bd <file> instead of bun bd test <file> since they expect exit code 0.
Do not set a timeout on tests. Bun already has timeouts built-in.

Files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, be careful with allocators and use defer for cleanup

Files:

  • src/string/immutable/wrap_ansi.zig
  • src/bun.js/api/BunObject.zig
src/**/*.zig

📄 CodeRabbit inference engine (src/CLAUDE.md)

src/**/*.zig: Use the # prefix for private fields in Zig structs, e.g., struct { #foo: u32 };
Use Decl literals in Zig, e.g., const decl: Decl = .{ .binding = 0, .value = 0 };
Place @import statements at the bottom of the file in Zig (auto formatter will handle positioning)
Never use @import() inline inside functions in Zig; always place imports at the bottom of the file or containing struct

Files:

  • src/string/immutable/wrap_ansi.zig
  • src/bun.js/api/BunObject.zig
src/bun.js/bindings/**/*.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

src/bun.js/bindings/**/*.cpp: Create classes in three parts in C++ when there is a public constructor: Foo (JSDestructibleObject), FooPrototype (JSNonFinalObject), and FooConstructor (InternalFunction)
Define properties using HashTableValue arrays in C++ JavaScript class bindings
Add iso subspaces for C++ classes with fields in JavaScript class bindings
Cache structures in ZigGlobalObject for JavaScript class bindings

Files:

  • src/bun.js/bindings/BunObject.cpp
🧠 Learnings (40)
📓 Common learnings
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24212
File: src/cli/publish_command.zig:782-788
Timestamp: 2025-10-30T21:52:04.707Z
Learning: In the Bun codebase (oven-sh/bun), `enable_ansi_colors` flags are used to gate both ANSI color codes and Unicode box-drawing characters/emoji. This is the established pattern across the codebase.
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/js/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:37:11.466Z
Learning: Write JS builtins for Bun's Node.js compatibility and APIs, and run `bun bd` after changes
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/test/v8/v8.test.ts : Add corresponding test cases to test/v8/v8.test.ts using checkSameOutput() function to compare Node.js and Bun output
📚 Learning: 2025-11-20T19:51:32.288Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 24880
File: packages/bun-vscode/package.json:382-385
Timestamp: 2025-11-20T19:51:32.288Z
Learning: In the Bun repository, dependencies may be explicitly added to package.json files (even when not directly imported in code) to force version upgrades on transitive dependencies, particularly as part of Aikido security scanner remediation to ensure vulnerable transitive dependencies resolve to patched versions.

Applied to files:

  • bench/package.json
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/test/v8/v8.test.ts : Add corresponding test cases to test/v8/v8.test.ts using checkSameOutput() function to compare Node.js and Bun output

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • bench/snippets/wrap-ansi.js
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-19T02:44:46.354Z
Learnt from: theshadow27
Repo: oven-sh/bun PR: 23798
File: packages/bun-otel/context-propagation.test.ts:1-1
Timestamp: 2025-10-19T02:44:46.354Z
Learning: In the Bun repository, standalone packages under packages/ (e.g., bun-vscode, bun-inspector-protocol, bun-plugin-yaml, bun-plugin-svelte, bun-debug-adapter-protocol, bun-otel) co-locate their tests with package source code using *.test.ts files. This follows standard npm/monorepo patterns. The test/ directory hierarchy (test/js/bun/, test/cli/, test/js/node/) is reserved for testing Bun's core runtime APIs and built-in functionality, not standalone packages.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun bd test <...test file>` to run tests with compiled code changes. Do not use `bun test` as it will not include your changes.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun:test` for files ending in `*.test.{ts,js,jsx,tsx,mjs,cjs}`. For test files without .test extension in test/js/node/test/{parallel,sequential}/*.js, use `bun bd <file>` instead of `bun bd test <file>` since they expect exit code 0.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Use `normalizeBunSnapshot` to normalize snapshot output of tests

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Verify tests fail with `USE_SYSTEM_BUN=1 bun test <file>` and pass with `bun bd test <file>` - tests are invalid if they pass with USE_SYSTEM_BUN=1

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-11-14T16:07:01.064Z
Learnt from: RiskyMH
Repo: oven-sh/bun PR: 24719
File: docs/bundler/executables.mdx:527-560
Timestamp: 2025-11-14T16:07:01.064Z
Learning: In the Bun repository, certain bundler features like compile with code splitting (--compile --splitting) are CLI-only and not supported in the Bun.build() JavaScript API. Tests for CLI-only features use backend: "cli" flag (e.g., test/bundler/bundler_compile_splitting.test.ts). The CompileBuildConfig interface correctly restricts these with splitting?: never;. When documenting CLI-only bundler features, add a note clarifying they're not available via the programmatic API.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
  • packages/bun-types/bun.d.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Never use `bun test` directly - always use `bun bd test` to run tests with debug build changes

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-26T01:32:04.844Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 24082
File: test/cli/test/coverage.test.ts:60-112
Timestamp: 2025-10-26T01:32:04.844Z
Learning: In the Bun repository test files (test/cli/test/*.test.ts), when spawning Bun CLI commands with Bun.spawnSync for testing, prefer using stdio: ["inherit", "inherit", "inherit"] to inherit stdio streams rather than piping them.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Ensure V8 API tests compare identical C++ code output between Node.js and Bun through the test suite validation process

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
📚 Learning: 2025-09-30T22:53:19.887Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 23117
File: src/bun.js/test/snapshot.zig:265-276
Timestamp: 2025-09-30T22:53:19.887Z
Learning: In Bun's snapshot testing (src/bun.js/test/snapshot.zig), multiple inline snapshots at the same line and column (same call position) must have identical values. However, multiple inline snapshots on the same line at different columns are allowed to have different values. The check is position-specific (line+col), not line-wide.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
📚 Learning: 2025-10-30T21:52:04.707Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24212
File: src/cli/publish_command.zig:782-788
Timestamp: 2025-10-30T21:52:04.707Z
Learning: In the Bun codebase (oven-sh/bun), `enable_ansi_colors` flags are used to gate both ANSI color codes and Unicode box-drawing characters/emoji. This is the established pattern across the codebase.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • src/string/immutable/wrap_ansi.zig
  • src/bun.js/api/BunObject.zig
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-09-24T05:48:59.872Z
Learnt from: nektro
Repo: oven-sh/bun PR: 22806
File: scripts/runner.node.mjs:687-689
Timestamp: 2025-09-24T05:48:59.872Z
Learning: In the Bun codebase, the `startGroup` utility function in scripts/runner.node.mjs automatically closes any previously open group when called, so `startGroup("End")` correctly closes the final test group and keeps subsequent output ungrouped.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
📚 Learning: 2025-10-24T10:43:09.398Z
Learnt from: fmguerreiro
Repo: oven-sh/bun PR: 23774
File: src/install/PackageManager/updatePackageJSONAndInstall.zig:548-548
Timestamp: 2025-10-24T10:43:09.398Z
Learning: In Bun's Zig codebase, the `as(usize, intCast(...))` cast pattern triggers a Zig compiler bug that causes compilation to hang indefinitely when used in complex control flow contexts (loops + short-circuit operators + optional unwrapping). Avoid this pattern and use simpler alternatives like just `intCast(...)` if type casting is necessary.

Applied to files:

  • src/string/immutable/wrap_ansi.zig
📚 Learning: 2025-11-11T22:55:04.070Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24571
File: src/css/selectors/parser.zig:908-916
Timestamp: 2025-11-11T22:55:04.070Z
Learning: In oven-sh/bun, CSS serialization uses an arena allocator. In src/css/selectors/parser.zig, functions like PseudoClass.toCss and PseudoElement.toCss intentionally do not call deinit on std.Io.Writer.Allocating, scratch buffers, or css.Printer because dest.allocator is an arena and these temporaries are reclaimed when the CSS pass completes. Only debug-only paths (e.g., DeclarationBlock.DebugFmt in src/css/declaration.zig) may explicitly deinit.

Applied to files:

  • src/string/immutable/wrap_ansi.zig
📚 Learning: 2025-09-02T18:27:23.279Z
Learnt from: taylordotfish
Repo: oven-sh/bun PR: 22227
File: src/collections/multi_array_list.zig:24-24
Timestamp: 2025-09-02T18:27:23.279Z
Learning: The `#allocator` syntax in bun's custom Zig implementation is valid and does not require quoting with @"#allocator". Private fields using the `#` prefix work correctly throughout the codebase without special quoting syntax.

Applied to files:

  • src/string/immutable/wrap_ansi.zig
📚 Learning: 2026-01-05T16:32:07.551Z
Learnt from: alii
Repo: oven-sh/bun PR: 25474
File: src/bun.js/event_loop/Sigusr1Handler.zig:0-0
Timestamp: 2026-01-05T16:32:07.551Z
Learning: In Zig codebases (e.g., Bun), treat std.posix.sigaction as returning void and do not perform runtime error handling for its failure. The Zig standard library views sigaction failures as programmer errors (unreachable) because they only occur with invalid signals like SIGKILL/SIGSTOP. Apply this pattern across Zig files that call sigaction (e.g., crash_handler.zig, main.zig, filter_run.zig, process.zig) and ensure failures are not handled as recoverable errors; prefer reaching an explicit unreachable/compile-time assumption when such failures are detected.

Applied to files:

  • src/string/immutable/wrap_ansi.zig
  • src/bun.js/api/BunObject.zig
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/napi/napi.zig : For each new V8 C++ method, add both GCC/Clang and MSVC mangled symbol names to the V8API struct in src/napi/napi.zig using extern fn declarations

Applied to files:

  • src/bun.js/api/BunObject.zig
  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.h : Add BUN_EXPORT visibility attribute to all public V8 API functions to ensure proper symbol export across platforms

Applied to files:

  • src/bun.js/api/BunObject.zig
  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-11-24T18:37:47.899Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/AGENTS.md:0-0
Timestamp: 2025-11-24T18:37:47.899Z
Learning: Applies to src/bun.js/bindings/v8/**/<UNKNOWN> : <UNKNOWN>

Applied to files:

  • src/bun.js/api/BunObject.zig
  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-11-03T20:43:06.996Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24273
File: src/bun.js/test/snapshot.zig:19-19
Timestamp: 2025-11-03T20:43:06.996Z
Learning: In Bun's Zig codebase, when storing JSValue objects in collections like ArrayList, use `jsc.Strong.Optional` (not raw JSValue). When adding values, wrap them with `jsc.Strong.Optional.create(value, globalThis)`. In cleanup code, iterate the collection calling `.deinit()` on each Strong.Optional item before calling `.deinit()` on the ArrayList itself. This pattern automatically handles GC protection. See examples in src/bun.js/test/ScopeFunctions.zig and src/bun.js/node/node_cluster_binding.zig.

Applied to files:

  • src/bun.js/api/BunObject.zig
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.dyn : Add symbol names with leading underscore and semicolons in braces to src/symbols.dyn for each new V8 API method

Applied to files:

  • src/bun.js/api/BunObject.zig
  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-11-24T18:37:11.466Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/js/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:37:11.466Z
Learning: Write JS builtins for Bun's Node.js compatibility and APIs, and run `bun bd` after changes

Applied to files:

  • src/bun.js/api/BunObject.zig
  • bench/snippets/wrap-ansi.js
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.txt : Add symbol names without leading underscore to src/symbols.txt for each new V8 API method

Applied to files:

  • src/bun.js/api/BunObject.zig
  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2026-01-07T23:45:24.551Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/CLAUDE.md:0-0
Timestamp: 2026-01-07T23:45:24.551Z
Learning: Applies to src/**/*.zig : Place `import` statements at the bottom of the file in Zig (auto formatter will handle positioning)

Applied to files:

  • src/bun.js/api/BunObject.zig
📚 Learning: 2026-01-07T23:45:24.551Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/CLAUDE.md:0-0
Timestamp: 2026-01-07T23:45:24.551Z
Learning: Applies to src/**/*.zig : Never use `import()` inline inside functions in Zig; always place imports at the bottom of the file or containing struct

Applied to files:

  • src/bun.js/api/BunObject.zig
📚 Learning: 2025-08-30T09:09:18.384Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 22231
File: src/bundler/bundle_v2.zig:48-48
Timestamp: 2025-08-30T09:09:18.384Z
Learning: In Zig, when a module exports a top-level struct, import("./Module.zig") directly returns that struct type and can be used as a type alias without needing to access a field within the module. This is a common pattern in the Bun codebase.

Applied to files:

  • src/bun.js/api/BunObject.zig
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Add iso subspaces for C++ classes with fields in JavaScript class bindings

Applied to files:

  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.h : Create V8 class headers with .h extension following the pattern V8ClassName.h that include pragma once, v8.h, V8Local.h, V8Isolate.h, and declare classes extending from Data with BUN_EXPORT static methods

Applied to files:

  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Create classes in three parts in C++ when there is a public constructor: Foo (JSDestructibleObject), FooPrototype (JSNonFinalObject), and FooConstructor (InternalFunction)

Applied to files:

  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Define properties using HashTableValue arrays in C++ JavaScript class bindings

Applied to files:

  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-10-01T21:59:54.571Z
Learnt from: taylordotfish
Repo: oven-sh/bun PR: 23169
File: src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h:47-74
Timestamp: 2025-10-01T21:59:54.571Z
Learning: In the new bindings generator (bindgenv2) for `src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h`, the context-aware enumeration conversion overloads intentionally use stricter validation (requiring `value.isString()` without ToString coercion), diverging from Web IDL semantics. This is a design decision documented in comments.

Applied to files:

  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-09-20T05:35:57.318Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 22534
File: src/bun.js/bindings/headers.h:729-731
Timestamp: 2025-09-20T05:35:57.318Z
Learning: symbols.txt in the Bun codebase is specifically for V8 API mangled symbols (without leading underscore), not for general Bun host functions declared with BUN_DECLARE_HOST_FUNCTION. Host functions are handled through different build mechanisms.

Applied to files:

  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Do not set a timeout on tests. Bun already has timeouts built-in.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-09-20T03:39:41.770Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 22534
File: test/regression/issue/21830.fixture.ts:14-63
Timestamp: 2025-09-20T03:39:41.770Z
Learning: Bun's test runner supports async describe callbacks, unlike Jest/Vitest where describe callbacks must be synchronous. The syntax `describe("name", async () => { ... })` is valid in Bun.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-08T13:48:02.430Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 23373
File: test/js/bun/tarball/extract.test.ts:107-111
Timestamp: 2025-10-08T13:48:02.430Z
Learning: In Bun's test runner, use `expect(async () => { await ... }).toThrow()` to assert async rejections. Unlike Jest/Vitest, Bun does not require `await expect(...).rejects.toThrow()` - the async function wrapper with `.toThrow()` is the correct pattern for async error assertions in Bun tests.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-25T17:20:19.041Z
Learnt from: theshadow27
Repo: oven-sh/bun PR: 24063
File: test/js/bun/telemetry/server-header-injection.test.ts:5-20
Timestamp: 2025-10-25T17:20:19.041Z
Learning: In the Bun telemetry codebase, tests are organized into two distinct layers: (1) Internal API tests in test/js/bun/telemetry/ use numeric InstrumentKind enum values to test Zig↔JS injection points and low-level integration; (2) Public API tests in packages/bun-otel/test/ use string InstrumentKind values ("http", "fetch", etc.) to test the public-facing BunSDK and instrumentation APIs. This separation allows internal tests to use efficient numeric enums for refactoring flexibility while the public API maintains a developer-friendly string-based interface.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-19T02:52:37.412Z
Learnt from: theshadow27
Repo: oven-sh/bun PR: 23798
File: packages/bun-otel/tsconfig.json:1-15
Timestamp: 2025-10-19T02:52:37.412Z
Learning: In the Bun repository, packages under packages/ (e.g., bun-otel) can follow a TypeScript-first pattern where package.json exports point directly to .ts files (not compiled .js files). Bun natively runs TypeScript, so consumers import .ts sources directly and receive full type information without needing compiled .d.ts declaration files. For such packages, adding "declaration": true or "outDir" in tsconfig.json is unnecessary and would break the export structure.
<!-- [remove_learning]
ceedde95-980e-4898-a2c6-40ff73913664

Applied to files:

  • packages/bun-types/bun.d.ts
🧬 Code graph analysis (3)
test/js/bun/util/wrapAnsi.npm.test.ts (1)
bench/snippets/wrap-ansi.js (3)
  • red (10-10)
  • green (11-11)
  • blue (12-12)
bench/snippets/wrap-ansi.js (2)
bench/runner.mjs (5)
  • summary (17-17)
  • summary (17-17)
  • bench (15-15)
  • bench (15-15)
  • run (7-13)
src/bun.js/api/BunObject.bind.ts (1)
  • wrapAnsi (45-53)
packages/bun-types/bun.d.ts (1)
src/bun.js/api/BunObject.bind.ts (2)
  • WrapAnsiOptions (38-43)
  • wrapAnsi (45-53)
🪛 Biome (2.1.2)
test/js/bun/util/wrapAnsi.npm.test.ts

[error] 36-36: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 36-36: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 36-36: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 36-36: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 37-37: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)

🔇 Additional comments (16)
bench/package.json (1)

21-21: LGTM!

The wrap-ansi dependency is correctly added for benchmarking the new Bun.wrapAnsi() API against the npm package.

bench/runner.mjs (1)

17-17: LGTM!

The summary export follows the established pattern for exposing Mitata utilities (bench, group).

src/bun.js/api/BunObject.bind.ts (1)

38-53: LGTM!

The WrapAnsiOptions dictionary and wrapAnsi function binding are well-structured and follow the established pattern from StringWidthOptions/stringWidth. All option defaults align with the PR objectives and match wrap-ansi compatibility.

src/bun.js/bindings/BunObject.cpp (1)

805-805: LGTM!

The wrapAnsi LUT entry correctly follows the established pattern for generated functions in BunObject. The placement near related ANSI functions (stripANSI, stringWidth) is logical, and DontDelete|Function 2 appropriately reflects the required argument count.

bench/snippets/wrap-ansi.js (1)

1-103: LGTM!

Comprehensive benchmark suite with excellent coverage:

  • Plain text at various lengths
  • ANSI SGR color codes
  • Full-width Unicode (Japanese)
  • Emoji with surrogate pairs
  • OSC 8 hyperlinks
  • hard wrap and trim: false options

The structured summary() groupings provide clear npm vs Bun comparisons.

test/js/bun/util/wrapAnsi.npm.test.ts (2)

1-24: License header inclusion looks correct and complete.


39-255: Test coverage is strong and the “Bun vs npm wrap-ansi” expectation deltas are clearly documented. The explicit notes around ANSI close/reopen behavior and the malformed-ANSI expectation are particularly helpful for future maintainers.

packages/bun-types/bun.d.ts (1)

647-702: Type shape/signature looks consistent with the runtime binding (options + defaults) and existing bun.d.ts patterns.

src/bun.js/api/BunObject.zig (2)

1384-1408: LGTM! Clean implementation with proper memory management.

The function correctly:

  • Handles empty input as an edge case
  • Uses defer for cleanup of both input and result allocations
  • Properly maps options from the JS binding struct to the Zig wrap options
  • Handles OOM errors consistently with throwOutOfMemoryValue()

2076-2076: Import correctly placed at bottom of file.

Follows the Zig coding guidelines for the Bun codebase.

src/string/immutable/wrap_ansi.zig (4)

479-499: SGR parser only captures first code in compound sequences.

For sequences like \x1b[1;31m (bold + red), this only returns 1, missing the 31. This limits style preservation to single-code sequences.

Given the PR notes that the implementation "differs in some edge cases from npm wrap-ansi but yields visually equivalent output", this may be an intentional simplification.

Consider whether compound SGR sequences (e.g., \x1b[1;4;31m for bold+underline+red) are used in practice and if the current behavior is acceptable.


540-573: LGTM! Robust character width calculation.

The function properly:

  • Has an ASCII fast path for performance
  • Handles UTF-8 multibyte sequences correctly
  • Gracefully falls back for invalid sequences
  • Delegates to visibleCodepointWidth for Unicode width handling

603-606: Imports correctly placed at bottom of file.

Follows the Bun Zig coding guidelines.


575-601: Good basic test coverage for the Zig implementation.

The tests properly use std.testing.allocator and defer for cleanup. The comprehensive edge case testing is appropriately delegated to the JS test suite.

test/js/bun/util/wrapAnsi.test.ts (2)

3-30: LGTM! Good coverage of basic wrapping behavior.

Tests properly cover word boundaries, empty input, no-wrap scenarios, and the edge case of columns = 0.


61-97: LGTM! Comprehensive ANSI escape code testing.

Good coverage of:

  • Simple color preservation
  • Color continuity across line breaks (checking for close/restore codes)
  • Multiple colors and styles
  • Width calculation excluding ANSI codes

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +613 to +645
interface WrapAnsiOptions {
/**
* If `true`, break words in the middle if they don't fit on a line.
* If `false`, only break at word boundaries.
*
* @default false
*/
hard?: boolean;

/**
* If `true`, wrap at word boundaries when possible.
* If `false`, don't perform word wrapping (only wrap at explicit newlines).
*
* @default true
*/
wordWrap?: boolean;

/**
* If `true`, trim leading and trailing whitespace from each line.
* If `false`, preserve whitespace.
*
* @default true
*/
trim?: boolean;

/**
* When it's ambiguous and `true`, count ambiguous width characters as 1 character wide.
* If `false`, count them as 2 characters wide.
*
* @default true
*/
ambiguousIsNarrow?: boolean;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

wrap-ansi npm package options wordWrap hard trim semantics documentation

💡 Result:

Summary of wrap-ansi options and their semantics:

  • hard (boolean, default false): when false, wrapping is "soft" (long words may extend past the column width). When true, lines are hard-wrapped at the column width (words will be broken to fit). (npmjs.com)

  • wordWrap (boolean, default true): when true, the wrapper prefers to split at spaces so words don’t extend past the column width. When false, wrapping fills each line completely and will split words as necessary. (npmjs.com)

  • trim (boolean, default true): when true (the default), whitespace at the start/end of all output lines is removed. Set to false to preserve leading/trailing whitespace. (npmjs.com)

If you want, I can show short examples demonstrating each option.

Citations:


🏁 Script executed:

# Find test files related to wrapAnsi in the repository
fd -t f 'test|spec' | head -20
find . -type f -name '*wrap*' -o -name '*test*' | grep -i wrap | head -20

Repository: oven-sh/bun

Length of output: 2075


🏁 Script executed:

# Search for wrapAnsi usage and tests in the codebase
rg -i 'wrapAnsi|wrap-ansi' --type ts --type js -A 5 -B 5 | head -100

Repository: oven-sh/bun

Length of output: 5829


🏁 Script executed:

cat -n ./test/js/bun/util/wrapAnsi.test.ts | head -150

Repository: oven-sh/bun

Length of output: 6003


🏁 Script executed:

cat -n ./test/js/bun/util/wrapAnsi.npm.test.ts | head -150

Repository: oven-sh/bun

Length of output: 7475


Update wordWrap JSDoc: it still wraps by character when false, not "only at explicit newlines".

The current documentation for wordWrap: false is inaccurate. Tests demonstrate that wordWrap: false still wraps text to fill the column width, splitting words as needed (character-level wrapping). This matches the upstream wrap-ansi semantics: "wrapping fills each line completely and will split words as necessary."

Proposed fix
     /**
      * If `true`, wrap at word boundaries when possible.
-     * If `false`, don't perform word wrapping (only wrap at explicit newlines).
+     * If `false`, wrap by character (may split words) instead of preferring word boundaries.
      *
      * `@default` true
      */
     wordWrap?: boolean;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
interface WrapAnsiOptions {
/**
* If `true`, break words in the middle if they don't fit on a line.
* If `false`, only break at word boundaries.
*
* @default false
*/
hard?: boolean;
/**
* If `true`, wrap at word boundaries when possible.
* If `false`, don't perform word wrapping (only wrap at explicit newlines).
*
* @default true
*/
wordWrap?: boolean;
/**
* If `true`, trim leading and trailing whitespace from each line.
* If `false`, preserve whitespace.
*
* @default true
*/
trim?: boolean;
/**
* When it's ambiguous and `true`, count ambiguous width characters as 1 character wide.
* If `false`, count them as 2 characters wide.
*
* @default true
*/
ambiguousIsNarrow?: boolean;
}
interface WrapAnsiOptions {
/**
* If `true`, break words in the middle if they don't fit on a line.
* If `false`, only break at word boundaries.
*
* `@default` false
*/
hard?: boolean;
/**
* If `true`, wrap at word boundaries when possible.
* If `false`, wrap by character (may split words) instead of preferring word boundaries.
*
* `@default` true
*/
wordWrap?: boolean;
/**
* If `true`, trim leading and trailing whitespace from each line.
* If `false`, preserve whitespace.
*
* `@default` true
*/
trim?: boolean;
/**
* When it's ambiguous and `true`, count ambiguous width characters as 1 character wide.
* If `false`, count them as 2 characters wide.
*
* `@default` true
*/
ambiguousIsNarrow?: boolean;
}
🤖 Prompt for AI Agents
In `@packages/bun-types/bun.d.ts` around lines 613 - 645, The JSDoc for
WrapAnsiOptions.wordWrap is incorrect: when false it still wraps to fill the
column and will split words at character boundaries rather than only breaking at
explicit newlines; update the JSDoc on the wordWrap property (interface
WrapAnsiOptions, symbol wordWrap) to say that true prefers wrapping at word
boundaries when possible, and false performs character-level wrapping to fill
each line and will split words as needed (do not claim it only wraps at explicit
newlines).

Comment thread src/string/immutable/wrap_ansi.zig Outdated
Comment on lines +25 to +38
// Normalize \r\n to \n
var normalized: std.ArrayListUnmanaged(u8) = .{};
defer normalized.deinit(allocator);

var i: usize = 0;
while (i < input.len) {
if (i + 1 < input.len and input[i] == '\r' and input[i + 1] == '\n') {
try normalized.append(allocator, '\n');
i += 2;
} else {
try normalized.append(allocator, input[i]);
i += 1;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider using std.mem.replace for CRLF normalization.

The current byte-by-byte approach works correctly but could be more efficient for large inputs using a bulk operation.

♻️ Optional optimization
-    // Normalize \r\n to \n
-    var normalized: std.ArrayListUnmanaged(u8) = .{};
-    defer normalized.deinit(allocator);
-
-    var i: usize = 0;
-    while (i < input.len) {
-        if (i + 1 < input.len and input[i] == '\r' and input[i + 1] == '\n') {
-            try normalized.append(allocator, '\n');
-            i += 2;
-        } else {
-            try normalized.append(allocator, input[i]);
-            i += 1;
-        }
-    }
+    // Normalize \r\n to \n
+    var normalized: std.ArrayListUnmanaged(u8) = .{};
+    defer normalized.deinit(allocator);
+    try normalized.appendSlice(allocator, input);
+    _ = std.mem.replace(u8, normalized.items, "\r\n", "\n");

Note: This would require adjusting the slice length after replacement since std.mem.replace returns the count. The current approach is simpler and correct.

🤖 Prompt for AI Agents
In `@src/string/immutable/wrap_ansi.zig` around lines 25 - 38, Replace the
byte-by-byte CRLF normalization loop with a bulk replace using std.mem.replace:
allocate or create a mutable copy of input (used in place of the manual append
loop), call std.mem.replace to replace the pattern "\r\n" with "\n" into the
normalized buffer (reference symbols: normalized, input, std.mem.replace), and
adjust the resulting slice/length based on the replace result (account for the
number of replacements to compute the final normalized length) before
using/deinitializing normalized; this keeps behavior identical but is more
efficient for large inputs.

Comment thread src/string/immutable/wrap_ansi.zig Outdated
Comment on lines +193 to +216
while (words_iter2.next()) |word| {
if (idx < last) {
if (idx > 0) {
new_row.append(allocator, ' ') catch {};
}
new_row.appendSlice(allocator, word) catch {};
} else {
// Append ANSI sequences from trailing words
for (word) |c| {
if (c == '\x1b' or trailing_ansi.items.len > 0) {
trailing_ansi.append(allocator, c) catch {};
}
}
}
idx += 1;
}

// Append trailing ANSI codes
new_row.appendSlice(allocator, trailing_ansi.items) catch {};
trailing_ansi.deinit(allocator);

// Replace row content
row.deinit(allocator);
row.* = new_row;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Silent allocation failures could cause data corruption.

Using catch {} to swallow allocation errors means the function silently produces incomplete or incorrect output if memory allocation fails. This is inconsistent with the error handling elsewhere in this file.

🛠️ Suggested fix: propagate errors
-fn stringVisibleTrimSpacesRight(allocator: std.mem.Allocator, row: *std.ArrayListUnmanaged(u8)) void {
+fn stringVisibleTrimSpacesRight(allocator: std.mem.Allocator, row: *std.ArrayListUnmanaged(u8)) !void {
     // ... existing code until rebuild ...
     
     while (words_iter2.next()) |word| {
         if (idx < last) {
             if (idx > 0) {
-                new_row.append(allocator, ' ') catch {};
+                try new_row.append(allocator, ' ');
             }
-            new_row.appendSlice(allocator, word) catch {};
+            try new_row.appendSlice(allocator, word);
         } else {
             // Append ANSI sequences from trailing words
             for (word) |c| {
                 if (c == '\x1b' or trailing_ansi.items.len > 0) {
-                    trailing_ansi.append(allocator, c) catch {};
+                    try trailing_ansi.append(allocator, c);
                 }
             }
         }
         idx += 1;
     }

     // Append trailing ANSI codes
-    new_row.appendSlice(allocator, trailing_ansi.items) catch {};
+    try new_row.appendSlice(allocator, trailing_ansi.items);

Then update the caller at line 161:

-            stringVisibleTrimSpacesRight(allocator, row);
+            try stringVisibleTrimSpacesRight(allocator, row);
🤖 Prompt for AI Agents
In `@src/string/immutable/wrap_ansi.zig` around lines 193 - 216, The code
currently swallows allocation failures with `catch {}` when appending to
`new_row` and `trailing_ansi`, risking silent corruption; change those `catch
{}` usages to propagate errors (use `try`/`return`/! as appropriate) for all
allocator operations involving `new_row.append`, `new_row.appendSlice`, and
`trailing_ansi.append`, and ensure the final `new_row.appendSlice(allocator,
trailing_ansi.items)` also propagates errors; adjust cleanup so
`trailing_ansi.deinit(allocator)` and `row.deinit(allocator)` still run on error
(use defer or explicit cleanup) and update the function's caller to handle the
propagated error (the caller that invokes this wrap/ANSI routine) accordingly.

Comment thread src/string/immutable/wrap_ansi.zig Outdated
Comment on lines +25 to +38
import { expect, test } from "bun:test";

// ANSI color helpers (equivalent to chalk with level 1)
const red = (s: string) => `\u001B[31m${s}\u001B[39m`;
const green = (s: string) => `\u001B[32m${s}\u001B[39m`;
const blue = (s: string) => `\u001B[34m${s}\u001B[39m`;
const bgGreen = (s: string) => `\u001B[42m${s}\u001B[49m`;
const bgRed = (s: string) => `\u001B[41m${s}\u001B[49m`;
const black = (s: string) => `\u001B[30m${s}\u001B[39m`;

// Helper functions
const stripAnsi = (s: string) => s.replace(/\u001B\[[0-9;]*m|\u001B\]8;;[^\u0007]*\u0007/g, "");
const hasAnsi = (s: string) => /\u001B\[[0-9;]*m/.test(s);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix Biome “control character in regex” errors (likely CI-blocking). Biome flags the regex literals using ESC/BEL on Line 36-37; switch to RegExp constructed from strings to satisfy the linter while keeping behavior unchanged.

Proposed fix
 // Helper functions
-const stripAnsi = (s: string) => s.replace(/\u001B\[[0-9;]*m|\u001B\]8;;[^\u0007]*\u0007/g, "");
-const hasAnsi = (s: string) => /\u001B\[[0-9;]*m/.test(s);
+const stripAnsiRe = new RegExp("\\x1B\\[[0-9;]*m|\\x1B\\]8;;[^\\x07]*\\x07", "g");
+const hasAnsiRe = new RegExp("\\x1B\\[[0-9;]*m");
+const stripAnsi = (s: string) => s.replace(stripAnsiRe, "");
+const hasAnsi = (s: string) => hasAnsiRe.test(s);
🧰 Tools
🪛 Biome (2.1.2)

[error] 36-36: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 36-36: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 36-36: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 36-36: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 37-37: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)

🤖 Prompt for AI Agents
In `@test/js/bun/util/wrapAnsi.npm.test.ts` around lines 25 - 38, The regex
literals in stripAnsi and hasAnsi use raw control characters (ESC/BEL) which
Biome flags; update both to use RegExp constructors built from string patterns
instead (e.g., build the ESC as "\\u001B" and BEL as "\\u0007" inside the
pattern string) so the patterns remain identical but avoid embedding control
chars; modify stripAnsi to use new RegExp("...","g") and hasAnsi to use new
RegExp("...") while preserving their original flags and replacements.

Comment on lines +42 to +49
describe("wordWrap option", () => {
test("wordWrap false disables wrapping", () => {
// Without wordWrap, only explicit newlines should cause breaks
const result = Bun.wrapAnsi("hello world", 5, { wordWrap: false });
// The behavior may vary - just check it doesn't crash
expect(typeof result).toBe("string");
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Weak assertion for wordWrap=false behavior.

The test only checks that the result is a string, which doesn't validate the actual behavior. Consider documenting the expected behavior or adding a more specific assertion.

💡 Suggested improvement
   describe("wordWrap option", () => {
     test("wordWrap false disables wrapping", () => {
-      // Without wordWrap, only explicit newlines should cause breaks
       const result = Bun.wrapAnsi("hello world", 5, { wordWrap: false });
-      // The behavior may vary - just check it doesn't crash
-      expect(typeof result).toBe("string");
+      // With wordWrap false, words are not broken at boundaries
+      // but characters may still be wrapped if they exceed column width
+      expect(result).toContain("hello");
+      expect(result).toContain("world");
     });
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
describe("wordWrap option", () => {
test("wordWrap false disables wrapping", () => {
// Without wordWrap, only explicit newlines should cause breaks
const result = Bun.wrapAnsi("hello world", 5, { wordWrap: false });
// The behavior may vary - just check it doesn't crash
expect(typeof result).toBe("string");
});
});
describe("wordWrap option", () => {
test("wordWrap false disables wrapping", () => {
const result = Bun.wrapAnsi("hello world", 5, { wordWrap: false });
// With wordWrap false, words are not broken at boundaries
// but characters may still be wrapped if they exceed column width
expect(result).toContain("hello");
expect(result).toContain("world");
});
});

Comment on lines +145 to +163
describe("edge cases", () => {
test("handles tabs", () => {
const input = "a\tb";
const result = Bun.wrapAnsi(input, 10);
expect(typeof result).toBe("string");
});

test("handles Windows line endings", () => {
const input = "hello\r\nworld";
const result = Bun.wrapAnsi(input, 10);
expect(typeof result).toBe("string");
});

test("handles consecutive spaces", () => {
const input = "hello world";
const result = Bun.wrapAnsi(input, 10);
expect(typeof result).toBe("string");
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Edge case tests only verify no crash, not correctness.

These tests check that the function returns a string but don't validate the actual output. Consider adding specific expectations for at least one case.

💡 Example improvement for tabs test
     test("handles tabs", () => {
       const input = "a\tb";
       const result = Bun.wrapAnsi(input, 10);
-      expect(typeof result).toBe("string");
+      expect(result).toContain("a");
+      expect(result).toContain("b");
     });
🤖 Prompt for AI Agents
In `@test/js/bun/util/wrapAnsi.test.ts` around lines 145 - 163, The tests in
wrapAnsi.test.ts only assert the return type and not correctness; update the
"handles tabs", "handles Windows line endings", and "handles consecutive spaces"
tests to assert concrete expected outputs from Bun.wrapAnsi: for example call
Bun.wrapAnsi("a\tb", 10) and expect(result).toBe("a\tb"), call
Bun.wrapAnsi("hello\r\nworld", 10) and expect(result).toBe("hello\r\nworld") (or
assert that CRLF is preserved with expect(result).toContain("\r\n") if
normalization is intended), and call Bun.wrapAnsi("hello    world", 10) and
expect(result).toBe("hello    world") (or assert the sequence of spaces is
preserved with expect(result).toMatch(/hello\s{4}world/)); modify whichever of
these match the library's intended behavior and replace the typeof assertions
with these concrete expects referencing Bun.wrapAnsi and the test names.

Comment on lines +165 to +177
describe("ambiguousIsNarrow option", () => {
test("default treats ambiguous as narrow", () => {
// By default, ambiguous width chars should be treated as width 1
const result1 = Bun.wrapAnsi("αβγ", 3);
// Greek letters are ambiguous width
expect(typeof result1).toBe("string");
});

test("ambiguousIsNarrow false treats as wide", () => {
const result = Bun.wrapAnsi("αβγ", 3, { ambiguousIsNarrow: false });
expect(typeof result).toBe("string");
});
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

ambiguousIsNarrow tests don't verify width difference.

The tests check that the function runs without error but don't verify that setting ambiguousIsNarrow: false actually changes the wrapping behavior. Greek letters treated as wide (2 columns each) vs narrow (1 column each) should produce different wrapping results.

💡 Suggested improvement
   describe("ambiguousIsNarrow option", () => {
     test("default treats ambiguous as narrow", () => {
-      // By default, ambiguous width chars should be treated as width 1
-      const result1 = Bun.wrapAnsi("αβγ", 3);
-      // Greek letters are ambiguous width
-      expect(typeof result1).toBe("string");
+      // Greek letters are ambiguous width, treated as 1 column each by default
+      // "αβγ" = 3 columns, should fit in width 3
+      const result = Bun.wrapAnsi("αβγ", 3, { hard: true });
+      expect(result).toBe("αβγ"); // No wrap needed
     });

     test("ambiguousIsNarrow false treats as wide", () => {
-      const result = Bun.wrapAnsi("αβγ", 3, { ambiguousIsNarrow: false });
-      expect(typeof result).toBe("string");
+      // When treated as wide, each letter = 2 columns
+      // "αβγ" = 6 columns, should wrap at width 4
+      const result = Bun.wrapAnsi("αβγ", 4, { hard: true, ambiguousIsNarrow: false });
+      expect(result).toContain("\n"); // Should wrap
     });
   });
🤖 Prompt for AI Agents
In `@test/js/bun/util/wrapAnsi.test.ts` around lines 165 - 177, The tests
currently only assert Bun.wrapAnsi returns a string but don't verify wrapping
changes when ambiguousIsNarrow toggles; update the "ambiguousIsNarrow option"
tests to call Bun.wrapAnsi with the same input (e.g., "αβγ" and a specific
column width) for both the default (or explicit ambiguousIsNarrow: true) and
ambiguousIsNarrow: false, then assert the two returned strings differ (and
optionally assert expected line breaks or lengths to demonstrate wide vs narrow
behavior), referencing Bun.wrapAnsi in the "default treats ambiguous as narrow"
and "ambiguousIsNarrow false treats as wide" cases so the test fails if wrapping
behavior doesn't change.

@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

Very cool. Are you sure we don’t need to use any Unicode-aware word iteration? Is space character good enough?

also I imagine doing this in C++ would be faster because then it can use StringBuilder and avoid utf8 -> utf16 conversion. And it’s a little easier to write SIMD for some parts of this since it can use WebKit’s helper functions

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/bun.js/bindings/wrapAnsi.cpp`:
- Around line 1185-1190: The current code casts the result of
columnsValue.toIntegerOrInfinity(...) directly to size_t, which will wrap
negative values; to fix, first store the result in a signed/integer type (e.g.,
int64_t or double) from toIntegerOrInfinity(globalObject), check
RETURN_IF_EXCEPTION(scope, {}) as before, then validate: if the numeric result
is negative set columns = 0, if it is positive infinity set columns = SIZE_MAX
(or otherwise clamp to a reasonable max), otherwise cast the non-negative finite
value to size_t and assign to columns; update references to columnsValue,
columns, and toIntegerOrInfinity accordingly.
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f55246b and f070780.

📒 Files selected for processing (4)
  • src/bun.js/api/BunObject.zig
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
  • src/bun.js/bindings/wrapAnsi.h
🧰 Additional context used
📓 Path-based instructions (3)
src/bun.js/bindings/**/*.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

src/bun.js/bindings/**/*.cpp: Create classes in three parts in C++ when there is a public constructor: Foo (JSDestructibleObject), FooPrototype (JSNonFinalObject), and FooConstructor (InternalFunction)
Define properties using HashTableValue arrays in C++ JavaScript class bindings
Add iso subspaces for C++ classes with fields in JavaScript class bindings
Cache structures in ZigGlobalObject for JavaScript class bindings

Files:

  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, be careful with allocators and use defer for cleanup

Files:

  • src/bun.js/api/BunObject.zig
src/**/*.zig

📄 CodeRabbit inference engine (src/CLAUDE.md)

src/**/*.zig: Use the # prefix for private fields in Zig structs, e.g., struct { #foo: u32 };
Use Decl literals in Zig, e.g., const decl: Decl = .{ .binding = 0, .value = 0 };
Place @import statements at the bottom of the file in Zig (auto formatter will handle positioning)
Never use @import() inline inside functions in Zig; always place imports at the bottom of the file or containing struct

Files:

  • src/bun.js/api/BunObject.zig
🧠 Learnings (27)
📓 Common learnings
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24212
File: src/cli/publish_command.zig:782-788
Timestamp: 2025-10-30T21:52:04.707Z
Learning: In the Bun codebase (oven-sh/bun), `enable_ansi_colors` flags are used to gate both ANSI color codes and Unicode box-drawing characters/emoji. This is the established pattern across the codebase.
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.h : Add BUN_EXPORT visibility attribute to all public V8 API functions to ensure proper symbol export across platforms

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/api/BunObject.zig
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.h : Create V8 class headers with .h extension following the pattern V8ClassName.h that include pragma once, v8.h, V8Local.h, V8Isolate.h, and declare classes extending from Data with BUN_EXPORT static methods

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Add iso subspaces for C++ classes with fields in JavaScript class bindings

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Create classes in three parts in C++ when there is a public constructor: Foo (JSDestructibleObject), FooPrototype (JSNonFinalObject), and FooConstructor (InternalFunction)

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Create V8 class implementations with .cpp extension following the pattern V8ClassName.cpp that include the header, v8_compatibility_assertions.h, use ASSERT_V8_TYPE_LAYOUT_MATCHES macro, and implement methods using isolate->currentHandleScope()->createLocal<T>() for handle creation

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:37:47.899Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/AGENTS.md:0-0
Timestamp: 2025-11-24T18:37:47.899Z
Learning: Applies to src/bun.js/bindings/v8/**/<UNKNOWN> : <UNKNOWN>

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/api/BunObject.zig
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.dyn : Add symbol names with leading underscore and semicolons in braces to src/symbols.dyn for each new V8 API method

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/api/BunObject.zig
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/napi/napi.zig : For each new V8 C++ method, add both GCC/Clang and MSVC mangled symbol names to the V8API struct in src/napi/napi.zig using extern fn declarations

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/api/BunObject.zig
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.txt : Add symbol names without leading underscore to src/symbols.txt for each new V8 API method

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/api/BunObject.zig
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-09-20T05:35:57.318Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 22534
File: src/bun.js/bindings/headers.h:729-731
Timestamp: 2025-09-20T05:35:57.318Z
Learning: symbols.txt in the Bun codebase is specifically for V8 API mangled symbols (without leading underscore), not for general Bun host functions declared with BUN_DECLARE_HOST_FUNCTION. Host functions are handled through different build mechanisms.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Cache structures in ZigGlobalObject for JavaScript class bindings

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/api/BunObject.zig
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Use localToJSValue() to convert V8 handles to JSC values and perform JSC operations within V8 method implementations

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-11-24T18:37:11.466Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/js/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:37:11.466Z
Learning: Write JS builtins for Bun's Node.js compatibility and APIs, and run `bun bd` after changes

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/api/BunObject.zig
📚 Learning: 2025-12-23T06:50:31.577Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 25429
File: src/bun.js/bindings/helpers.h:422-422
Timestamp: 2025-12-23T06:50:31.577Z
Learning: In Bun's C++ bindings, when returning an empty JSC::Identifier and a VM is accessible, prefer using vm.propertyNames->emptyIdentifier over constructing with JSC::Identifier(JSC::Identifier::EmptyIdentifierFlag::EmptyIdentifier). The cached identifier from the VM's property names table is more efficient and consistent with WebKit upgrade patterns. Apply this guidance to src/bun.js/bindings/helpers.h and similar header files in the same bindings directory (i.e., any file that constructs an EmptyIdentifier).

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/test/v8/v8-module/main.cpp : Register new V8 API test functions in the Init method using NODE_SET_METHOD with exports object

Applied to files:

  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-10-30T21:52:04.707Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24212
File: src/cli/publish_command.zig:782-788
Timestamp: 2025-10-30T21:52:04.707Z
Learning: In the Bun codebase (oven-sh/bun), `enable_ansi_colors` flags are used to gate both ANSI color codes and Unicode box-drawing characters/emoji. This is the established pattern across the codebase.

Applied to files:

  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-12-23T06:50:41.142Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 25429
File: src/bun.js/bindings/helpers.h:422-422
Timestamp: 2025-12-23T06:50:41.142Z
Learning: In Bun's C++ bindings (src/bun.js/bindings/helpers.h and similar files), when returning an empty JSC::Identifier and a VM is accessible, prefer using `vm.propertyNames->emptyIdentifier` over constructing with `JSC::Identifier(JSC::Identifier::EmptyIdentifierFlag::EmptyIdentifier)`. The cached identifier from the VM's property names table is more efficient and consistent with WebKit upgrade patterns.

Applied to files:

  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Use V8_UNIMPLEMENTED() macro for functions not yet implemented in V8 compatibility classes

Applied to files:

  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/test/v8/v8-module/main.cpp : Create test functions in test/v8/v8-module/main.cpp that take FunctionCallbackInfo<Value> parameter, use the test V8 API, print results for comparison with Node.js, and return Undefined

Applied to files:

  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-08-30T09:09:18.384Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 22231
File: src/bundler/bundle_v2.zig:48-48
Timestamp: 2025-08-30T09:09:18.384Z
Learning: In Zig, when a module exports a top-level struct, import("./Module.zig") directly returns that struct type and can be used as a type alias without needing to access a field within the module. This is a common pattern in the Bun codebase.

Applied to files:

  • src/bun.js/api/BunObject.zig
📚 Learning: 2025-11-20T19:51:32.288Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 24880
File: packages/bun-vscode/package.json:382-385
Timestamp: 2025-11-20T19:51:32.288Z
Learning: In the Bun repository, dependencies may be explicitly added to package.json files (even when not directly imported in code) to force version upgrades on transitive dependencies, particularly as part of Aikido security scanner remediation to ensure vulnerable transitive dependencies resolve to patched versions.

Applied to files:

  • src/bun.js/api/BunObject.zig
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Code generation happens automatically during the build process - bundled modules can be reloaded without rebuilding Zig by running `bun run build`

Applied to files:

  • src/bun.js/api/BunObject.zig
📚 Learning: 2026-01-07T23:45:24.551Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/CLAUDE.md:0-0
Timestamp: 2026-01-07T23:45:24.551Z
Learning: Applies to src/**/*.zig : Place `import` statements at the bottom of the file in Zig (auto formatter will handle positioning)

Applied to files:

  • src/bun.js/api/BunObject.zig
📚 Learning: 2026-01-07T23:45:24.551Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/CLAUDE.md:0-0
Timestamp: 2026-01-07T23:45:24.551Z
Learning: Applies to src/**/*.zig : Never use `import()` inline inside functions in Zig; always place imports at the bottom of the file or containing struct

Applied to files:

  • src/bun.js/api/BunObject.zig
📚 Learning: 2026-01-05T16:32:07.551Z
Learnt from: alii
Repo: oven-sh/bun PR: 25474
File: src/bun.js/event_loop/Sigusr1Handler.zig:0-0
Timestamp: 2026-01-05T16:32:07.551Z
Learning: In Zig codebases (e.g., Bun), treat std.posix.sigaction as returning void and do not perform runtime error handling for its failure. The Zig standard library views sigaction failures as programmer errors (unreachable) because they only occur with invalid signals like SIGKILL/SIGSTOP. Apply this pattern across Zig files that call sigaction (e.g., crash_handler.zig, main.zig, filter_run.zig, process.zig) and ensure failures are not handled as recoverable errors; prefer reaching an explicit unreachable/compile-time assumption when such failures are detected.

Applied to files:

  • src/bun.js/api/BunObject.zig
📚 Learning: 2025-10-01T21:59:54.571Z
Learnt from: taylordotfish
Repo: oven-sh/bun PR: 23169
File: src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h:47-74
Timestamp: 2025-10-01T21:59:54.571Z
Learning: In the new bindings generator (bindgenv2) for `src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h`, the context-aware enumeration conversion overloads intentionally use stricter validation (requiring `value.isString()` without ToString coercion), diverging from Web IDL semantics. This is a design decision documented in comments.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
🪛 Cppcheck (2.19.0)
src/bun.js/bindings/wrapAnsi.cpp

[information] 42-42: Include file

(missingInclude)


[information] 52-52: Include file

(missingIncludeSystem)


[information] 59-59: Include file

(missingIncludeSystem)


[error] 66-66: failed to evaluate #if condition, undefined function-like macro invocation

(syntaxError)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Format
🔇 Additional comments (17)
src/bun.js/bindings/wrapAnsi.h (1)

1-9: LGTM!

The header follows the Bun codebase conventions with proper include guard, namespace scoping, and use of JSC_DECLARE_HOST_FUNCTION macro.

src/bun.js/bindings/BunObject.cpp (2)

80-80: LGTM!

The host function declaration follows the existing pattern (alongside jsFunctionBunStripANSI) and is properly scoped within the Bun namespace.


806-806: LGTM!

The LUT entry is correctly configured with DontDelete|Function 3, matching the expected API signature of Bun.wrapAnsi(string, columns, options?).

src/bun.js/bindings/wrapAnsi.cpp (13)

1-18: LGTM!

The includes and WrapAnsiOptions struct are well-organized with appropriate defaults matching the documented API (hard: false, wordWrap: true, trim: true, ambiguousIsNarrow: true).


24-114: LGTM!

Comprehensive zero-width codepoint detection covering combining marks, invisible characters, variation selectors, and script-specific combining marks. The constexpr if optimization for 8-bit characters is a nice touch.


116-265: LGTM!

Comprehensive full-width codepoint detection with proper early exit optimization. The coverage of CJK, Hangul, emoji, and supplementary planes aligns with standard terminal width behavior.


267-331: LGTM!

The ambiguous width handling and getVisibleWidth composition are correct. The subset approach for ambiguous characters is a reasonable performance trade-off.


350-383: UTF-8 decoding is lenient with malformed sequences.

The decoder doesn't validate continuation bytes beyond masking. While this won't crash, malformed UTF-8 like [0xC2, 0x00] would produce incorrect codepoints rather than replacement characters. This matches the pragmatic approach of other text processing in Bun, but worth noting for edge cases.


406-568: LGTM!

The ANSI escape sequence handling is well-structured with SIMD-accelerated scanning and a complete state machine for CSI, OSC, DCS, and other sequences. The design mirrors stripANSI.cpp for consistency.


635-690: LGTM with note on escape detection.

The trimLeadingSpaces method correctly preserves ANSI codes while removing whitespace. The simplified escape detection (checking only m and 0x07 terminators) covers the common SGR and OSC cases, which is sufficient for typical terminal styling.


696-769: LGTM!

The word wrapping logic correctly handles ANSI escapes (including OSC 8 hyperlinks), tracks visible width, and has a nice edge case fix at the end to merge rows containing only escape codes.


771-830: LGTM!

Trailing whitespace trimming correctly preserves ANSI escape codes while removing non-visible trailing content.


914-988: LGTM!

The style preservation logic correctly closes and reopens ANSI styles (including hyperlinks) around line breaks. This ensures terminal robustness as noted in the PR description, even if the byte representation differs slightly from the npm package.


994-1098: LGTM!

The line processing logic handles the various wrapping modes (hard, wordWrap, trim) correctly with proper word boundary detection and column limit enforcement.


1104-1159: LGTM!

The main implementation correctly handles edge cases (empty input, zero columns), normalizes line endings, and processes lines with proper ANSI style preservation across wrapped output.


1219-1228: LGTM!

The encoding dispatch correctly handles both Latin1 (8-bit) and UTF-16 (16-bit) string representations, and the result is properly wrapped as a JSString.

src/bun.js/api/BunObject.zig (1)

2050-2050: This review comment is based on incorrect context.

The wrap_ansi import referenced at line 2050 does not exist in src/bun.js/api/BunObject.zig. Line 2050 contains const std = @import("std");, and no wrap_ansi or wrapAnsi references appear anywhere in the file.

Likely an incorrect or invalid review comment.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread src/bun.js/bindings/wrapAnsi.cpp
// ============================================================================

template<typename Char>
static size_t stringWidth(const Char* start, const Char* end, bool ambiguousIsNarrow)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

lol this should use the existing code to do this and not another implementation

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@src/bun.js/bindings/wrapAnsi.cpp`:
- Around line 518-539: END_CODE named as 39 is misleading because it represents
the default foreground SGR value, not a general reset; rename the constant
(e.g., SGR_DEFAULT_FOREGROUND or DEFAULT_FOREGROUND_CODE) and update all
references (such as in parseSgrCode usage sites) to the new name, and add a
short comment noting that SGR reset is 0 to avoid confusion.
- Around line 35-68: The decodeUTF8 function currently assembles multi-byte
sequences without validating that each continuation byte matches the 10xxxxxx
pattern, so malformed input can produce invalid codepoints; update decodeUTF8
to, after computing seqLen and before assembling cp, verify for each
continuation byte ptr[i] (i=1..seqLen-1) that (static_cast<uint8_t>(ptr[i]) &
0xC0) == 0x80 and if any check fails set outLen = 1 and return 0xFFFD; after
assembling cp also validate against overlong encodings, UTF-16 surrogate range
(0xD800–0xDFFF), and max codepoint > 0x10FFFF and return 0xFFFD if invalid,
otherwise set outLen = seqLen and return cp (references: function decodeUTF8,
helper utf8SequenceLength, variables ptr, available, outLen).
- Around line 414-420: When starting a new row in the wrap logic (the block that
calls rows.push_back(Row<Char>()); rows.back().append(it, it + charLen);) you
reset the visible width counter vis to 0 but fail to account for the width of
the character just appended; change the fix so vis is initialized/updated with
the character's displayed width (use charWidth) after appending instead of
setting it to 0 (i.e., set vis = charWidth or vis += charWidth as appropriate),
keeping the existing handling for isInsideEscape and multibyte append calls that
use charLen and it.

In `@src/string/immutable/visible.zig`:
- Around line 1152-1156: The UTF-8 export Bun__visibleWidthExcludeANSI_utf8
currently ignores ambiguous_as_wide whereas the UTF-16 path forwards it to
visible.width.exclude_ansi_colors.utf16, causing inconsistent semantics; fix by
propagating the ambiguous_as_wide flag through the UTF‑8 path—either add/use an
overload of visible.width.exclude_ansi_colors.utf8 that accepts an
ambiguousAsWide parameter and call it with ambiguous_as_wide from
Bun__visibleWidthExcludeANSI_utf8, or if such an API cannot be added, explicitly
document the difference and remove/mark the unused parameter to avoid the
misleading identical C signature; reference Bun__visibleWidthExcludeANSI_utf8,
ambiguous_as_wide, visible.width.exclude_ansi_colors.utf8 and
visible.width.exclude_ansi_colors.utf16 when making the change.
♻️ Duplicate comments (1)
src/bun.js/bindings/wrapAnsi.cpp (1)

867-872: Handle negative and infinite column values.

This was flagged in a previous review. toIntegerOrInfinity can return negative values (which wrap to large size_t) or infinity. Consider clamping:

Suggested fix
     // Get columns
     size_t columns = 0;
     if (!columnsValue.isUndefined()) {
-        columns = static_cast<size_t>(columnsValue.toIntegerOrInfinity(globalObject));
+        double colsDouble = columnsValue.toIntegerOrInfinity(globalObject);
         RETURN_IF_EXCEPTION(scope, {});
+        if (colsDouble > 0 && std::isfinite(colsDouble))
+            columns = static_cast<size_t>(std::min(colsDouble, static_cast<double>(SIZE_MAX)));
     }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between f070780 and 14d492a81a640b75477c8e96452a03d8fff3f55d.

📒 Files selected for processing (2)
  • src/bun.js/bindings/wrapAnsi.cpp
  • src/string/immutable/visible.zig
🧰 Additional context used
📓 Path-based instructions (3)
src/bun.js/bindings/**/*.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

src/bun.js/bindings/**/*.cpp: Create classes in three parts in C++ when there is a public constructor: Foo (JSDestructibleObject), FooPrototype (JSNonFinalObject), and FooConstructor (InternalFunction)
Define properties using HashTableValue arrays in C++ JavaScript class bindings
Add iso subspaces for C++ classes with fields in JavaScript class bindings
Cache structures in ZigGlobalObject for JavaScript class bindings

Files:

  • src/bun.js/bindings/wrapAnsi.cpp
**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, be careful with allocators and use defer for cleanup

Files:

  • src/string/immutable/visible.zig
src/**/*.zig

📄 CodeRabbit inference engine (src/CLAUDE.md)

src/**/*.zig: Use the # prefix for private fields in Zig structs, e.g., struct { #foo: u32 };
Use Decl literals in Zig, e.g., const decl: Decl = .{ .binding = 0, .value = 0 };
Place @import statements at the bottom of the file in Zig (auto formatter will handle positioning)
Never use @import() inline inside functions in Zig; always place imports at the bottom of the file or containing struct

Files:

  • src/string/immutable/visible.zig
🧠 Learnings (21)
📓 Common learnings
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24212
File: src/cli/publish_command.zig:782-788
Timestamp: 2025-10-30T21:52:04.707Z
Learning: In the Bun codebase (oven-sh/bun), `enable_ansi_colors` flags are used to gate both ANSI color codes and Unicode box-drawing characters/emoji. This is the established pattern across the codebase.
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/js/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:37:11.466Z
Learning: Write JS builtins for Bun's Node.js compatibility and APIs, and run `bun bd` after changes
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.h : Add BUN_EXPORT visibility attribute to all public V8 API functions to ensure proper symbol export across platforms

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
  • src/string/immutable/visible.zig
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Add iso subspaces for C++ classes with fields in JavaScript class bindings

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:37:47.899Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/AGENTS.md:0-0
Timestamp: 2025-11-24T18:37:47.899Z
Learning: Applies to src/bun.js/bindings/v8/**/<UNKNOWN> : <UNKNOWN>

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.txt : Add symbol names without leading underscore to src/symbols.txt for each new V8 API method

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-09-20T05:35:57.318Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 22534
File: src/bun.js/bindings/headers.h:729-731
Timestamp: 2025-09-20T05:35:57.318Z
Learning: symbols.txt in the Bun codebase is specifically for V8 API mangled symbols (without leading underscore), not for general Bun host functions declared with BUN_DECLARE_HOST_FUNCTION. Host functions are handled through different build mechanisms.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.dyn : Add symbol names with leading underscore and semicolons in braces to src/symbols.dyn for each new V8 API method

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-10-01T21:59:54.571Z
Learnt from: taylordotfish
Repo: oven-sh/bun PR: 23169
File: src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h:47-74
Timestamp: 2025-10-01T21:59:54.571Z
Learning: In the new bindings generator (bindgenv2) for `src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h`, the context-aware enumeration conversion overloads intentionally use stricter validation (requiring `value.isString()` without ToString coercion), diverging from Web IDL semantics. This is a design decision documented in comments.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.h : Create V8 class headers with .h extension following the pattern V8ClassName.h that include pragma once, v8.h, V8Local.h, V8Isolate.h, and declare classes extending from Data with BUN_EXPORT static methods

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Create classes in three parts in C++ when there is a public constructor: Foo (JSDestructibleObject), FooPrototype (JSNonFinalObject), and FooConstructor (InternalFunction)

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/napi/napi.zig : For each new V8 C++ method, add both GCC/Clang and MSVC mangled symbol names to the V8API struct in src/napi/napi.zig using extern fn declarations

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
  • src/string/immutable/visible.zig
📚 Learning: 2025-10-24T10:43:09.398Z
Learnt from: fmguerreiro
Repo: oven-sh/bun PR: 23774
File: src/install/PackageManager/updatePackageJSONAndInstall.zig:548-548
Timestamp: 2025-10-24T10:43:09.398Z
Learning: In Bun's Zig codebase, the `as(usize, intCast(...))` cast pattern triggers a Zig compiler bug that causes compilation to hang indefinitely when used in complex control flow contexts (loops + short-circuit operators + optional unwrapping). Avoid this pattern and use simpler alternatives like just `intCast(...)` if type casting is necessary.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-10T00:57:09.173Z
Learnt from: franciscop
Repo: oven-sh/bun PR: 24514
File: src/bun.js/api/crypto/PasswordObject.zig:86-101
Timestamp: 2025-11-10T00:57:09.173Z
Learning: In Bun's Zig codebase (PasswordObject.zig), when validating the parallelism parameter for Argon2, the upper limit is set to 65535 (2^16 - 1) rather than using `std.math.maxInt(u24)` because the latter triggers Zig's truncation limit checks. The value 65535 is a practical upper bound that avoids compiler issues while being sufficient for thread parallelism use cases.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Use localToJSValue() to convert V8 handles to JSC values and perform JSC operations within V8 method implementations

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Create V8 class implementations with .cpp extension following the pattern V8ClassName.cpp that include the header, v8_compatibility_assertions.h, use ASSERT_V8_TYPE_LAYOUT_MATCHES macro, and implement methods using isolate->currentHandleScope()->createLocal<T>() for handle creation

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/test/v8/v8-module/main.cpp : Create test functions in test/v8/v8-module/main.cpp that take FunctionCallbackInfo<Value> parameter, use the test V8 API, print results for comparison with Node.js, and return Undefined

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-10-30T21:52:04.707Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24212
File: src/cli/publish_command.zig:782-788
Timestamp: 2025-10-30T21:52:04.707Z
Learning: In the Bun codebase (oven-sh/bun), `enable_ansi_colors` flags are used to gate both ANSI color codes and Unicode box-drawing characters/emoji. This is the established pattern across the codebase.

Applied to files:

  • src/string/immutable/visible.zig
📚 Learning: 2025-10-15T20:19:38.580Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 23680
File: cmake/targets/BuildBun.cmake:822-822
Timestamp: 2025-10-15T20:19:38.580Z
Learning: In the Bun codebase, FFI is compiled with tcc (TinyCC), which barely supports C99. The headers `src/bun.js/api/FFI.h` and `src/bun.js/api/ffi-stdbool.h` are only used for FFI compilation with tcc, not for the main Bun target. Therefore, C23 compatibility concerns (such as bool/true/false keyword conflicts) do not apply to these FFI headers.

Applied to files:

  • src/string/immutable/visible.zig
📚 Learning: 2025-10-15T20:19:37.256Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 23680
File: cmake/targets/BuildBun.cmake:822-822
Timestamp: 2025-10-15T20:19:37.256Z
Learning: In the Bun codebase, FFI (Foreign Function Interface) code is compiled separately using TinyCC (tcc), which barely supports C99. Headers like src/bun.js/api/FFI.h and src/bun.js/api/ffi-stdbool.h are only used for FFI compilation with tcc, not with the main compiler. Therefore, C standard changes to the main Bun target do not affect FFI code compilation.

Applied to files:

  • src/string/immutable/visible.zig
📚 Learning: 2025-11-12T04:11:52.293Z
Learnt from: cirospaciari
Repo: oven-sh/bun PR: 24622
File: src/deps/uws/us_socket_t.zig:112-113
Timestamp: 2025-11-12T04:11:52.293Z
Learning: In Bun's Zig codebase, when passing u32 values to C FFI functions that expect c_uint parameters, no explicit intCast is needed because c_uint is equivalent to u32 on Bun's target platforms and Zig allows implicit coercion between equivalent types. This pattern is used consistently throughout src/deps/uws/us_socket_t.zig in functions like setTimeout, setLongTimeout, and setKeepalive.

Applied to files:

  • src/string/immutable/visible.zig
📚 Learning: 2026-01-05T16:32:07.551Z
Learnt from: alii
Repo: oven-sh/bun PR: 25474
File: src/bun.js/event_loop/Sigusr1Handler.zig:0-0
Timestamp: 2026-01-05T16:32:07.551Z
Learning: In Zig codebases (e.g., Bun), treat std.posix.sigaction as returning void and do not perform runtime error handling for its failure. The Zig standard library views sigaction failures as programmer errors (unreachable) because they only occur with invalid signals like SIGKILL/SIGSTOP. Apply this pattern across Zig files that call sigaction (e.g., crash_handler.zig, main.zig, filter_run.zig, process.zig) and ensure failures are not handled as recoverable errors; prefer reaching an explicit unreachable/compile-time assumption when such failures are detected.

Applied to files:

  • src/string/immutable/visible.zig
🪛 Cppcheck (2.19.0)
src/bun.js/bindings/wrapAnsi.cpp

[information] 42-42: Include file

(missingInclude)


[information] 52-52: Include file

(missingIncludeSystem)


[information] 59-59: Include file

(missingIncludeSystem)


[error] 66-66: failed to evaluate #if condition, undefined function-like macro invocation

(syntaxError)

🔇 Additional comments (5)
src/string/immutable/visible.zig (1)

1164-1173: LGTM for Latin-1 and codepoint width exports.

The Latin-1 version correctly omits the ambiguous_as_wide parameter since Latin-1 has no ambiguous-width characters. The Bun__codepointWidth export properly delegates to the existing implementation.

src/bun.js/bindings/wrapAnsi.cpp (4)

272-284: Good refactor: Properly delegates to Zig visible width implementation.

This addresses the review feedback to reuse existing code rather than reimplementing width calculation. The Zig implementation provides SIMD-optimized width calculation.


595-670: LGTM: ANSI style preservation logic is well-structured.

The logic correctly tracks SGR codes and OSC 8 hyperlinks, closing them before newlines and reopening after. The character casting from Char to UChar is correct since Latin-1 values fit within UTF-16.


874-899: LGTM: Options parsing is well-implemented.

The code properly handles exceptions after each property access and uses appropriate type conversions. Default values are cleanly defined in the WrapAnsiOptions struct.


786-841: Implementation looks solid overall.

The main implementation correctly handles:

  • Empty input and zero columns edge cases
  • CRLF to LF normalization
  • Per-line processing with proper ANSI preservation

The memory reservation strategy is conservative, which is appropriate for text wrapping that may need additional space.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread src/bun.js/bindings/wrapAnsi.cpp Outdated
Comment on lines +35 to +68
static char32_t decodeUTF8(const Latin1Character* ptr, size_t available, size_t& outLen)
{
uint8_t byte = static_cast<uint8_t>(ptr[0]);

if (byte < 0x80) {
outLen = 1;
return byte;
}

uint8_t seqLen = utf8SequenceLength(byte);
if (seqLen > available) {
outLen = 1;
return 0xFFFD; // Replacement character
}

char32_t cp = 0;
switch (seqLen) {
case 2:
cp = ((byte & 0x1F) << 6) | (ptr[1] & 0x3F);
break;
case 3:
cp = ((byte & 0x0F) << 12) | ((ptr[1] & 0x3F) << 6) | (ptr[2] & 0x3F);
break;
case 4:
cp = ((byte & 0x07) << 18) | ((ptr[1] & 0x3F) << 12) | ((ptr[2] & 0x3F) << 6) | (ptr[3] & 0x3F);
break;
default:
outLen = 1;
return 0xFFFD;
}

outLen = seqLen;
return cp;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing continuation byte validation in UTF-8 decoding.

The decoding doesn't validate that continuation bytes have the correct 10xxxxxx pattern. Malformed UTF-8 input could produce incorrect codepoints instead of the replacement character.

Suggested validation
 static char32_t decodeUTF8(const Latin1Character* ptr, size_t available, size_t& outLen)
 {
     uint8_t byte = static_cast<uint8_t>(ptr[0]);

     if (byte < 0x80) {
         outLen = 1;
         return byte;
     }

     uint8_t seqLen = utf8SequenceLength(byte);
     if (seqLen > available) {
         outLen = 1;
         return 0xFFFD; // Replacement character
     }

+    // Validate continuation bytes
+    for (size_t i = 1; i < seqLen; ++i) {
+        if ((ptr[i] & 0xC0) != 0x80) {
+            outLen = 1;
+            return 0xFFFD;
+        }
+    }
+
     char32_t cp = 0;
     switch (seqLen) {
🧰 Tools
🪛 Cppcheck (2.19.0)

[information] 42-42: Include file

(missingInclude)


[information] 52-52: Include file

(missingIncludeSystem)


[information] 59-59: Include file

(missingIncludeSystem)


[error] 66-66: failed to evaluate #if condition, undefined function-like macro invocation

(syntaxError)

🤖 Prompt for AI Agents
In `@src/bun.js/bindings/wrapAnsi.cpp` around lines 35 - 68, The decodeUTF8
function currently assembles multi-byte sequences without validating that each
continuation byte matches the 10xxxxxx pattern, so malformed input can produce
invalid codepoints; update decodeUTF8 to, after computing seqLen and before
assembling cp, verify for each continuation byte ptr[i] (i=1..seqLen-1) that
(static_cast<uint8_t>(ptr[i]) & 0xC0) == 0x80 and if any check fails set outLen
= 1 and return 0xFFFD; after assembling cp also validate against overlong
encodings, UTF-16 surrogate range (0xD800–0xDFFF), and max codepoint > 0x10FFFF
and return 0xFFFD if invalid, otherwise set outLen = seqLen and return cp
(references: function decodeUTF8, helper utf8SequenceLength, variables ptr,
available, outLen).

Comment on lines +317 to +371
void trimLeadingSpaces()
{
size_t removeCount = 0;
bool inEscape = false;

// Count leading spaces (preserving ANSI)
for (size_t i = 0; i < m_data.size(); ++i) {
Char c = m_data[i];
if (c == 0x1b) {
inEscape = true;
continue;
}
if (inEscape) {
if (c == 'm' || c == 0x07)
inEscape = false;
continue;
}
if (c == ' ' || c == '\t')
removeCount++;
else
break;
}

if (removeCount == 0)
return;

// Remove spaces while preserving ANSI codes
std::vector<Char> newData;
newData.reserve(m_data.size() - removeCount);

inEscape = false;
size_t removed = 0;

for (size_t i = 0; i < m_data.size(); ++i) {
Char c = m_data[i];
if (c == 0x1b) {
inEscape = true;
newData.push_back(c);
continue;
}
if (inEscape) {
if (c == 'm' || c == 0x07)
inEscape = false;
newData.push_back(c);
continue;
}
if ((c == ' ' || c == '\t') && removed < removeCount) {
removed++;
continue;
}
newData.push_back(c);
}

m_data = std::move(newData);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Incomplete OSC sequence terminator handling in trimLeadingSpaces.

The escape sequence detection only handles BEL (0x07) and m terminators, but OSC sequences can also be terminated by ST (ESC \). This could cause incorrect behavior when trimming strings containing OSC sequences with ST terminators.

Suggested fix
         for (size_t i = 0; i < m_data.size(); ++i) {
             Char c = m_data[i];
             if (c == 0x1b) {
                 inEscape = true;
+                // Check for ST terminator (ESC \) ending previous sequence
+                if (i + 1 < m_data.size() && m_data[i + 1] == '\\') {
+                    newData.push_back(c);
+                    continue;
+                }
                 continue;
             }
             if (inEscape) {
-                if (c == 'm' || c == 0x07)
+                if (c == 'm' || c == 0x07 || c == 0x9c)
                     inEscape = false;
                 continue;
             }

Comment thread src/bun.js/bindings/wrapAnsi.cpp
Comment on lines +518 to +539
static constexpr uint32_t END_CODE = 39;

template<typename Char>
static std::optional<uint32_t> parseSgrCode(const Char* start, const Char* end)
{
if (end - start < 3 || start[0] != 0x1b || start[1] != '[')
return std::nullopt;

uint32_t code = 0;
for (const Char* it = start + 2; it < end; ++it) {
Char c = *it;
if (c >= '0' && c <= '9') {
code = code * 10 + (c - '0');
} else if (c == 'm') {
return code;
} else {
break;
}
}

return std::nullopt;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Minor: Consider renaming END_CODE for clarity.

END_CODE = 39 represents "default foreground color", not a general reset code. The SGR reset code is actually 0. While the logic at line 625 correctly handles both, the constant name could be misleading.

-static constexpr uint32_t END_CODE = 39;
+static constexpr uint32_t DEFAULT_FOREGROUND_CODE = 39;
🤖 Prompt for AI Agents
In `@src/bun.js/bindings/wrapAnsi.cpp` around lines 518 - 539, END_CODE named as
39 is misleading because it represents the default foreground SGR value, not a
general reset; rename the constant (e.g., SGR_DEFAULT_FOREGROUND or
DEFAULT_FOREGROUND_CODE) and update all references (such as in parseSgrCode
usage sites) to the new name, and add a short comment noting that SGR reset is 0
to avoid confusion.

Comment on lines +1152 to +1156
export fn Bun__visibleWidthExcludeANSI_utf8(ptr: [*]const u8, len: usize, ambiguous_as_wide: bool) usize {
_ = ambiguous_as_wide; // UTF-8 version doesn't use this parameter
const input = ptr[0..len];
return visible.width.exclude_ansi_colors.utf8(input);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the exported functions mentioned in the review
sed -n '1149,1173p' src/string/immutable/visible.zig

Repository: oven-sh/bun

Length of output: 1161


🏁 Script executed:

# Check the UTF-16 version to see if it actually uses ambiguous_as_wide
sed -n '1159,1165p' src/string/immutable/visible.zig

Repository: oven-sh/bun

Length of output: 433


🏁 Script executed:

# Examine the underlying width functions referenced
rg -n "fn width|exclude_ansi_colors" src/string/immutable/visible.zig | head -20

Repository: oven-sh/bun

Length of output: 626


🏁 Script executed:

# Check the implementation of utf8 and utf16 width functions
sed -n '1120,1145p' src/string/immutable/visible.zig

Repository: oven-sh/bun

Length of output: 866


🏁 Script executed:

# Look for the visibleUTF8WidthFn function around line 752
sed -n '745,785p' src/string/immutable/visible.zig

Repository: oven-sh/bun

Length of output: 1493


API inconsistency: ambiguous_as_wide is ignored for UTF-8 but used for UTF-16.

The UTF-8 export ignores the ambiguous_as_wide parameter while the UTF-16 version (line 1161) passes it through. The exported C functions have identical signatures, but the underlying Zig implementations differ: visible.width.exclude_ansi_colors.utf8() doesn't accept an ambiguousAsWide parameter at all, whereas visible.width.exclude_ansi_colors.utf16() does. This means users won't get consistent width calculations for ambiguous-width characters across different string encodings.

Consider either:

  1. Propagating the parameter through the UTF-8 path to match the UTF-16 behavior, or
  2. Documenting this as intentional behavior
🤖 Prompt for AI Agents
In `@src/string/immutable/visible.zig` around lines 1152 - 1156, The UTF-8 export
Bun__visibleWidthExcludeANSI_utf8 currently ignores ambiguous_as_wide whereas
the UTF-16 path forwards it to visible.width.exclude_ansi_colors.utf16, causing
inconsistent semantics; fix by propagating the ambiguous_as_wide flag through
the UTF‑8 path—either add/use an overload of
visible.width.exclude_ansi_colors.utf8 that accepts an ambiguousAsWide parameter
and call it with ambiguous_as_wide from Bun__visibleWidthExcludeANSI_utf8, or if
such an API cannot be added, explicitly document the difference and remove/mark
the unused parameter to avoid the misleading identical C signature; reference
Bun__visibleWidthExcludeANSI_utf8, ambiguous_as_wide,
visible.width.exclude_ansi_colors.utf8 and
visible.width.exclude_ansi_colors.utf16 when making the change.

Comment thread src/bun.js/bindings/wrapAnsi.cpp Outdated
}

// Normalize \r\n to \n
std::vector<Char> normalized;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

WTF::Vector should be used instead

sosukesuzuki and others added 7 commits January 15, 2026 11:32
Implement a wrap-ansi compatible text wrapping function that:
- Wraps text to fit within specified column width
- Preserves ANSI escape codes (SGR colors/styles, OSC 8 hyperlinks)
- Respects Unicode display widths (full-width chars, emoji)
- Supports options: hard, wordWrap, trim, ambiguousIsNarrow

The implementation closes and reopens ANSI codes around line breaks
for robust terminal compatibility. This differs slightly from the NPM
package in edge cases but produces visually equivalent output.

Tests include 27 custom tests and 23 tests ported from the NPM package
(MIT licensed, credited in file header).

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 0
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# wrapAnsi関数の実装計画

## 概要
BunにNPMの`wrap-ansi`ライブラリと互換性のある`Bun.wrapAnsi()`関数を追加する。

## APIシグネチャ
```typescript
Bun.wrapAnsi(string: string, columns: number, options?: WrapAnsiOptions): string

interface WrapAnsiOptions {
  hard?: boolean;            // default: false - 長い単語を強制分割
  wordWrap?: boolean;        // default: true - 単語境界で折り返し
  trim?: boolean;            // default: true - 行の先頭・末尾の空白を削除
  ambiguousIsNarrow?: boolean; // default: true - 曖昧幅文字をnarrow(幅1)として扱う
}
```

## 実装アプローチ
`stringWidth`と同様にZig + bindgenシステムを使用。既存のANSI/文字幅計算コードを再利用。

## 変更するファイル

### 1. バインディング定義
**ファイル**: `src/bun.js/api/BunObject.bind.ts`

```typescript
export const WrapAnsiOptions = t.dictionary({
  hard: t.boolean.default(false),
  wordWrap: t.boolean.default(true),
  trim: t.boolean.default(true),
  ambiguousIsNarrow: t.boolean.default(true),
});

export const wrapAnsi = fn({
  args: {
    global: t.globalObject,
    str: t.DOMString.default(""),
    columns: t.usize,
    opts: WrapAnsiOptions.default({}),
  },
  ret: t.any, // JSValue (String)
});
```

### 2. Zig実装
**ファイル**: `src/bun.js/api/BunObject.zig`

エントリーポイント関数を追加。コア実装を呼び出す。

### 3. コア実装(新規)
**ファイル**: `src/string/immutable/wrap_ansi.zig`

主要な処理:
1. 改行で分割し、各行を処理
2. スペースで単語に分割し、表示幅を計算(ANSI除外)
3. 列幅に基づいて折り返し
4. ANSIエスケープコードの追跡(SGR、OSC 8ハイパーリンク)
5. 行跨ぎでスタイルを閉じて再開

既存コードの再利用:
- `src/string/immutable/visible.zig` - 表示幅計算
- ANSI CSI/OSCパースロジック

### 4. テスト
**ファイル**: `test/js/bun/util/wrapAnsi.test.ts`

NPMライブラリとの比較テスト:
- 基本的な折り返し
- ANSIカラー付き文字列
- hard/wordWrap/trimオプション
- 全角文字、絵文字、サロゲートペア
- ハイパーリンク

### 5. 型定義
**ファイル**: `packages/bun-types/bun.d.ts`

TypeScript型定義を追加。

## 実装ステップ

### Step 1: bindgen定義追加
`BunObject.bind.ts`にオプションと関数シグネチャを追加。

### Step 2: コア実装
`wrap_ansi.zig`に以下を実装:
- `wrapAnsi()` - メイン関数
- `wrapLine()` - 1行の処理
- `wrapWord()` - 長い単語の分割(hardモード)
- ANSIスタイル追跡構造体
- SGRコードの開始→終了マッピング

### Step 3: BunObjectへの統合
`BunObject.zig`にエントリーポイント関数を追加。

### Step 4: テスト作成
NPMとの互換性テストを作成。

### Step 5: 型定義追加
`bun.d.ts`にインターフェースを追加。

## SGRコードマッピング
行跨ぎでスタイルを復元するために必要:

| 開始コード | 終了コード | 説明 |
|-----------|-----------|------|
| 1 | 22 | bold |
| 2 | 22 | dim |
| 3 | 23 | italic |
| 4 | 24 | underline |
| 7 | 27 | inverse |
| 8 | 28 | hidden |
| 9 | 29 | strikethrough |
| 30-37 | 39 | foreground color |
| 40-47 | 49 | background color |
| 90-97 | 39 | bright foreground |
| 100-107 | 49 | bright background |

## 検証方法

```bash
# テスト実行
bun bd test test/js/bun/util/wrapAnsi.test.ts

# システムBunでは失敗することを確認
USE_SYSTEM_BUN=1 bun test test/js/bun/util/wrapAnsi.test.ts

# 手動テスト
bun bd -e "console.log(Bun.wrapAnsi('hello world', 5))"
```

## 注意事項
- 全角文字は幅2としてカウント
- ANSIエスケープは幅0
- Unicode正規化(normalize())を適用
- `\r\n`を`\n`に正規化
</claude-plan>
- Port wrapAnsi implementation from Zig to C++ using WebKit's StringBuilder
- Use std::vector for internal row management
- Maintain ANSI escape code preservation across line breaks
- Support OSC 8 hyperlinks
- Handle full-width characters, emoji, and zero-width characters
- Keep the same API and test coverage (all 27 tests pass)

This addresses the review feedback requesting C++ implementation for:
- Better performance using StringBuilder
- Avoiding UTF-8 to UTF-16 conversion overhead
- Easier SIMD optimization potential

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 2
Claude-Permission-Prompts: 1
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# wrapAnsi関数の実装計画

## 概要
BunにNPMの`wrap-ansi`ライブラリと互換性のある`Bun.wrapAnsi()`関数を追加する。

## APIシグネチャ
```typescript
Bun.wrapAnsi(string: string, columns: number, options?: WrapAnsiOptions): string

interface WrapAnsiOptions {
  hard?: boolean;            // default: false - 長い単語を強制分割
  wordWrap?: boolean;        // default: true - 単語境界で折り返し
  trim?: boolean;            // default: true - 行の先頭・末尾の空白を削除
  ambiguousIsNarrow?: boolean; // default: true - 曖昧幅文字をnarrow(幅1)として扱う
}
```

## 実装アプローチ
`stringWidth`と同様にZig + bindgenシステムを使用。既存のANSI/文字幅計算コードを再利用。

## 変更するファイル

### 1. バインディング定義
**ファイル**: `src/bun.js/api/BunObject.bind.ts`

```typescript
export const WrapAnsiOptions = t.dictionary({
  hard: t.boolean.default(false),
  wordWrap: t.boolean.default(true),
  trim: t.boolean.default(true),
  ambiguousIsNarrow: t.boolean.default(true),
});

export const wrapAnsi = fn({
  args: {
    global: t.globalObject,
    str: t.DOMString.default(""),
    columns: t.usize,
    opts: WrapAnsiOptions.default({}),
  },
  ret: t.any, // JSValue (String)
});
```

### 2. Zig実装
**ファイル**: `src/bun.js/api/BunObject.zig`

エントリーポイント関数を追加。コア実装を呼び出す。

### 3. コア実装(新規)
**ファイル**: `src/string/immutable/wrap_ansi.zig`

主要な処理:
1. 改行で分割し、各行を処理
2. スペースで単語に分割し、表示幅を計算(ANSI除外)
3. 列幅に基づいて折り返し
4. ANSIエスケープコードの追跡(SGR、OSC 8ハイパーリンク)
5. 行跨ぎでスタイルを閉じて再開

既存コードの再利用:
- `src/string/immutable/visible.zig` - 表示幅計算
- ANSI CSI/OSCパースロジック

### 4. テスト
**ファイル**: `test/js/bun/util/wrapAnsi.test.ts`

NPMライブラリとの比較テスト:
- 基本的な折り返し
- ANSIカラー付き文字列
- hard/wordWrap/trimオプション
- 全角文字、絵文字、サロゲートペア
- ハイパーリンク

### 5. 型定義
**ファイル**: `packages/bun-types/bun.d.ts`

TypeScript型定義を追加。

## 実装ステップ

### Step 1: bindgen定義追加
`BunObject.bind.ts`にオプションと関数シグネチャを追加。

### Step 2: コア実装
`wrap_ansi.zig`に以下を実装:
- `wrapAnsi()` - メイン関数
- `wrapLine()` - 1行の処理
- `wrapWord()` - 長い単語の分割(hardモード)
- ANSIスタイル追跡構造体
- SGRコードの開始→終了マッピング

### Step 3: BunObjectへの統合
`BunObject.zig`にエントリーポイント関数を追加。

### Step 4: テスト作成
NPMとの互換性テストを作成。

### Step 5: 型定義追加
`bun.d.ts`にインターフェースを追加。

## SGRコードマッピング
行跨ぎでスタイルを復元するために必要:

| 開始コード | 終了コード | 説明 |
|-----------|-----------|------|
| 1 | 22 | bold |
| 2 | 22 | dim |
| 3 | 23 | italic |
| 4 | 24 | underline |
| 7 | 27 | inverse |
| 8 | 28 | hidden |
| 9 | 29 | strikethrough |
| 30-37 | 39 | foreground color |
| 40-47 | 49 | background color |
| 90-97 | 39 | bright foreground |
| 100-107 | 49 | bright background |

## 検証方法

```bash
# テスト実行
bun bd test test/js/bun/util/wrapAnsi.test.ts

# システムBunでは失敗することを確認
USE_SYSTEM_BUN=1 bun test test/js/bun/util/wrapAnsi.test.ts

# 手動テスト
bun bd -e "console.log(Bun.wrapAnsi('hello world', 5))"
```

## 注意事項
- 全角文字は幅2としてカウント
- ANSIエスケープは幅0
- Unicode正規化(normalize())を適用
- `\r\n`を`\n`に正規化
</claude-plan>
- Remove redundant character width calculation code from C++
- Add Zig exports for visible width functions that C++ can call
- Keep UTF-8/UTF-16 decoding utilities needed for hard wrap
- Reuse existing SIMD-optimized visible width calculation from visible.zig

This addresses the review feedback to use existing code instead of
reimplementing width calculation.

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 1
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# wrapAnsi関数の実装計画

## 概要
BunにNPMの`wrap-ansi`ライブラリと互換性のある`Bun.wrapAnsi()`関数を追加する。

## APIシグネチャ
```typescript
Bun.wrapAnsi(string: string, columns: number, options?: WrapAnsiOptions): string

interface WrapAnsiOptions {
  hard?: boolean;            // default: false - 長い単語を強制分割
  wordWrap?: boolean;        // default: true - 単語境界で折り返し
  trim?: boolean;            // default: true - 行の先頭・末尾の空白を削除
  ambiguousIsNarrow?: boolean; // default: true - 曖昧幅文字をnarrow(幅1)として扱う
}
```

## 実装アプローチ
`stringWidth`と同様にZig + bindgenシステムを使用。既存のANSI/文字幅計算コードを再利用。

## 変更するファイル

### 1. バインディング定義
**ファイル**: `src/bun.js/api/BunObject.bind.ts`

```typescript
export const WrapAnsiOptions = t.dictionary({
  hard: t.boolean.default(false),
  wordWrap: t.boolean.default(true),
  trim: t.boolean.default(true),
  ambiguousIsNarrow: t.boolean.default(true),
});

export const wrapAnsi = fn({
  args: {
    global: t.globalObject,
    str: t.DOMString.default(""),
    columns: t.usize,
    opts: WrapAnsiOptions.default({}),
  },
  ret: t.any, // JSValue (String)
});
```

### 2. Zig実装
**ファイル**: `src/bun.js/api/BunObject.zig`

エントリーポイント関数を追加。コア実装を呼び出す。

### 3. コア実装(新規)
**ファイル**: `src/string/immutable/wrap_ansi.zig`

主要な処理:
1. 改行で分割し、各行を処理
2. スペースで単語に分割し、表示幅を計算(ANSI除外)
3. 列幅に基づいて折り返し
4. ANSIエスケープコードの追跡(SGR、OSC 8ハイパーリンク)
5. 行跨ぎでスタイルを閉じて再開

既存コードの再利用:
- `src/string/immutable/visible.zig` - 表示幅計算
- ANSI CSI/OSCパースロジック

### 4. テスト
**ファイル**: `test/js/bun/util/wrapAnsi.test.ts`

NPMライブラリとの比較テスト:
- 基本的な折り返し
- ANSIカラー付き文字列
- hard/wordWrap/trimオプション
- 全角文字、絵文字、サロゲートペア
- ハイパーリンク

### 5. 型定義
**ファイル**: `packages/bun-types/bun.d.ts`

TypeScript型定義を追加。

## 実装ステップ

### Step 1: bindgen定義追加
`BunObject.bind.ts`にオプションと関数シグネチャを追加。

### Step 2: コア実装
`wrap_ansi.zig`に以下を実装:
- `wrapAnsi()` - メイン関数
- `wrapLine()` - 1行の処理
- `wrapWord()` - 長い単語の分割(hardモード)
- ANSIスタイル追跡構造体
- SGRコードの開始→終了マッピング

### Step 3: BunObjectへの統合
`BunObject.zig`にエントリーポイント関数を追加。

### Step 4: テスト作成
NPMとの互換性テストを作成。

### Step 5: 型定義追加
`bun.d.ts`にインターフェースを追加。

## SGRコードマッピング
行跨ぎでスタイルを復元するために必要:

| 開始コード | 終了コード | 説明 |
|-----------|-----------|------|
| 1 | 22 | bold |
| 2 | 22 | dim |
| 3 | 23 | italic |
| 4 | 24 | underline |
| 7 | 27 | inverse |
| 8 | 28 | hidden |
| 9 | 29 | strikethrough |
| 30-37 | 39 | foreground color |
| 40-47 | 49 | background color |
| 90-97 | 39 | bright foreground |
| 100-107 | 49 | bright background |

## 検証方法

```bash
# テスト実行
bun bd test test/js/bun/util/wrapAnsi.test.ts

# システムBunでは失敗することを確認
USE_SYSTEM_BUN=1 bun test test/js/bun/util/wrapAnsi.test.ts

# 手動テスト
bun bd -e "console.log(Bun.wrapAnsi('hello world', 5))"
```

## 注意事項
- 全角文字は幅2としてカウント
- ANSIエスケープは幅0
- Unicode正規化(normalize())を適用
- `\r\n`を`\n`に正規化
</claude-plan>
Move common ANSI escape sequence handling code to a shared header:
- isEscapeCharacter() - check for ANSI escape introducers
- findEscapeCharacter() - SIMD-optimized escape sequence search
- consumeANSI() - state machine to consume ANSI sequences

This eliminates ~170 lines of duplicate code between wrapAnsi.cpp
and stripANSI.cpp.

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 1
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# wrapAnsi関数の実装計画

## 概要
BunにNPMの`wrap-ansi`ライブラリと互換性のある`Bun.wrapAnsi()`関数を追加する。

## APIシグネチャ
```typescript
Bun.wrapAnsi(string: string, columns: number, options?: WrapAnsiOptions): string

interface WrapAnsiOptions {
  hard?: boolean;            // default: false - 長い単語を強制分割
  wordWrap?: boolean;        // default: true - 単語境界で折り返し
  trim?: boolean;            // default: true - 行の先頭・末尾の空白を削除
  ambiguousIsNarrow?: boolean; // default: true - 曖昧幅文字をnarrow(幅1)として扱う
}
```

## 実装アプローチ
`stringWidth`と同様にZig + bindgenシステムを使用。既存のANSI/文字幅計算コードを再利用。

## 変更するファイル

### 1. バインディング定義
**ファイル**: `src/bun.js/api/BunObject.bind.ts`

```typescript
export const WrapAnsiOptions = t.dictionary({
  hard: t.boolean.default(false),
  wordWrap: t.boolean.default(true),
  trim: t.boolean.default(true),
  ambiguousIsNarrow: t.boolean.default(true),
});

export const wrapAnsi = fn({
  args: {
    global: t.globalObject,
    str: t.DOMString.default(""),
    columns: t.usize,
    opts: WrapAnsiOptions.default({}),
  },
  ret: t.any, // JSValue (String)
});
```

### 2. Zig実装
**ファイル**: `src/bun.js/api/BunObject.zig`

エントリーポイント関数を追加。コア実装を呼び出す。

### 3. コア実装(新規)
**ファイル**: `src/string/immutable/wrap_ansi.zig`

主要な処理:
1. 改行で分割し、各行を処理
2. スペースで単語に分割し、表示幅を計算(ANSI除外)
3. 列幅に基づいて折り返し
4. ANSIエスケープコードの追跡(SGR、OSC 8ハイパーリンク)
5. 行跨ぎでスタイルを閉じて再開

既存コードの再利用:
- `src/string/immutable/visible.zig` - 表示幅計算
- ANSI CSI/OSCパースロジック

### 4. テスト
**ファイル**: `test/js/bun/util/wrapAnsi.test.ts`

NPMライブラリとの比較テスト:
- 基本的な折り返し
- ANSIカラー付き文字列
- hard/wordWrap/trimオプション
- 全角文字、絵文字、サロゲートペア
- ハイパーリンク

### 5. 型定義
**ファイル**: `packages/bun-types/bun.d.ts`

TypeScript型定義を追加。

## 実装ステップ

### Step 1: bindgen定義追加
`BunObject.bind.ts`にオプションと関数シグネチャを追加。

### Step 2: コア実装
`wrap_ansi.zig`に以下を実装:
- `wrapAnsi()` - メイン関数
- `wrapLine()` - 1行の処理
- `wrapWord()` - 長い単語の分割(hardモード)
- ANSIスタイル追跡構造体
- SGRコードの開始→終了マッピング

### Step 3: BunObjectへの統合
`BunObject.zig`にエントリーポイント関数を追加。

### Step 4: テスト作成
NPMとの互換性テストを作成。

### Step 5: 型定義追加
`bun.d.ts`にインターフェースを追加。

## SGRコードマッピング
行跨ぎでスタイルを復元するために必要:

| 開始コード | 終了コード | 説明 |
|-----------|-----------|------|
| 1 | 22 | bold |
| 2 | 22 | dim |
| 3 | 23 | italic |
| 4 | 24 | underline |
| 7 | 27 | inverse |
| 8 | 28 | hidden |
| 9 | 29 | strikethrough |
| 30-37 | 39 | foreground color |
| 40-47 | 49 | background color |
| 90-97 | 39 | bright foreground |
| 100-107 | 49 | bright background |

## 検証方法

```bash
# テスト実行
bun bd test test/js/bun/util/wrapAnsi.test.ts

# システムBunでは失敗することを確認
USE_SYSTEM_BUN=1 bun test test/js/bun/util/wrapAnsi.test.ts

# 手動テスト
bun bd -e "console.log(Bun.wrapAnsi('hello world', 5))"
```

## 注意事項
- 全角文字は幅2としてカウント
- ANSIエスケープは幅0
- Unicode正規化(normalize())を適用
- `\r\n`を`\n`に正規化
</claude-plan>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/bun.js/bindings/wrapAnsi.cpp`:
- Around line 706-731: The options parsing repeatedly calls
JSC::Identifier::fromString for each property which is wasteful; replace those
per-call string-to-identifier conversions with cached identifiers (e.g., static
or VM-provided JSC::Identifier instances) and use them when looking up
properties on optionsValue/optionsObj; update the WrapAnsiOptions parsing block
(symbols: WrapAnsiOptions, optionsValue, optionsObj, Identifier::fromString,
ambiguousIsNarrow, wordWrap, trim, hard) to use the cached identifiers so each
call reuses the same JSC::Identifier objects instead of recreating them.
♻️ Duplicate comments (5)
src/bun.js/bindings/wrapAnsi.cpp (5)

246-249: Width tracking incorrect after line wrap.

When a new row is created and a character is appended, vis is reset to 0 but should reflect the width of the just-appended character. This could cause off-by-one errors in column calculations.

Suggested fix
         } else if (!isInsideEscape) {
             rows.push_back(Row<Char>());
             rows.back().append(it, it + charLen);
-            vis = 0;
+            vis = charWidth;
         } else {

35-68: Missing UTF-8 continuation byte validation.

The decodeUTF8 function doesn't validate that continuation bytes follow the 10xxxxxx pattern. Malformed UTF-8 (e.g., \xC0\xC0) could produce incorrect codepoints instead of replacement characters.

Suggested validation
     char32_t cp = 0;
+    // Validate continuation bytes
+    for (size_t i = 1; i < seqLen; ++i) {
+        if ((static_cast<uint8_t>(ptr[i]) & 0xC0) != 0x80) {
+            outLen = 1;
+            return 0xFFFD;
+        }
+    }
+
     switch (seqLen) {

699-704: Handle negative and infinite column values.

toIntegerOrInfinity can return negative values (wrapping to large size_t) or positive infinity. The current code doesn't guard against these edge cases.

Suggested fix
     // Get columns
     size_t columns = 0;
     if (!columnsValue.isUndefined()) {
-        columns = static_cast<size_t>(columnsValue.toIntegerOrInfinity(globalObject));
+        double colsDouble = columnsValue.toIntegerOrInfinity(globalObject);
         RETURN_IF_EXCEPTION(scope, {});
+        if (colsDouble > 0 && std::isfinite(colsDouble))
+            columns = static_cast<size_t>(colsDouble);
     }

122-126: Consider using WTF::Vector instead of std::vector.

Per reviewer feedback, WTF::Vector is preferred in the Bun codebase for consistency and potential performance benefits with WebKit's memory allocators.

Suggested change
 template<typename Char>
 class Row {
 public:
-    std::vector<Char> m_data;
+    WTF::Vector<Char> m_data;

This would require updating the method calls (e.g., push_back → append, insert → appropriate WTF::Vector methods).


161-164: Incomplete ANSI sequence terminator detection.

The escape sequence detection only handles m (SGR) and BEL (0x07) terminators, but ANSI sequences can also end with ST (0x9c) or the two-character sequence ESC \. This could cause incorrect trimming behavior with OSC sequences using ST terminators.

Suggested fix
             if (inEscape) {
-                if (c == 'm' || c == 0x07)
+                if (c == 'm' || c == 0x07 || c == 0x9c)
                     inEscape = false;
+                // Handle ESC \ (ST) terminator
+                if (c == '\\' && i > 0 && m_data[i-1] == 0x1b)
+                    inEscape = false;
                 continue;
             }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 12b0c94f334e79e1199d616009043bcc5db9cc79 and 4ee402e3b966742fcaa72c048ba2a28f6854fc54.

📒 Files selected for processing (3)
  • src/bun.js/bindings/ANSIHelpers.h
  • src/bun.js/bindings/stripANSI.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
🧰 Additional context used
📓 Path-based instructions (1)
src/bun.js/bindings/**/*.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

src/bun.js/bindings/**/*.cpp: Create classes in three parts in C++ when there is a public constructor: Foo (JSDestructibleObject), FooPrototype (JSNonFinalObject), and FooConstructor (InternalFunction)
Define properties using HashTableValue arrays in C++ JavaScript class bindings
Add iso subspaces for C++ classes with fields in JavaScript class bindings
Cache structures in ZigGlobalObject for JavaScript class bindings

Files:

  • src/bun.js/bindings/stripANSI.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
🧠 Learnings (20)
📓 Common learnings
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24212
File: src/cli/publish_command.zig:782-788
Timestamp: 2025-10-30T21:52:04.707Z
Learning: In the Bun codebase (oven-sh/bun), `enable_ansi_colors` flags are used to gate both ANSI color codes and Unicode box-drawing characters/emoji. This is the established pattern across the codebase.
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 25462
File: src/ast/visitExpr.zig:1644-1695
Timestamp: 2025-12-11T02:11:47.024Z
Learning: In Bun's bundler feature flag implementation (src/ast/visitExpr.zig), the validation for feature() flag names intentionally only rejects UTF-16 strings (checking `is_utf16`) while allowing UTF-8 strings, even though the error message says "must be an ASCII string". This is the intended behavior and should not be changed to enforce strict ASCII validation.
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/js/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:37:11.466Z
Learning: Write JS builtins for Bun's Node.js compatibility and APIs, and run `bun bd` after changes
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.h : Add BUN_EXPORT visibility attribute to all public V8 API functions to ensure proper symbol export across platforms

Applied to files:

  • src/bun.js/bindings/stripANSI.cpp
  • src/bun.js/bindings/ANSIHelpers.h
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Add iso subspaces for C++ classes with fields in JavaScript class bindings

Applied to files:

  • src/bun.js/bindings/stripANSI.cpp
  • src/bun.js/bindings/ANSIHelpers.h
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-10-30T21:52:04.707Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24212
File: src/cli/publish_command.zig:782-788
Timestamp: 2025-10-30T21:52:04.707Z
Learning: In the Bun codebase (oven-sh/bun), `enable_ansi_colors` flags are used to gate both ANSI color codes and Unicode box-drawing characters/emoji. This is the established pattern across the codebase.

Applied to files:

  • src/bun.js/bindings/stripANSI.cpp
  • src/bun.js/bindings/ANSIHelpers.h
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.h : Create V8 class headers with .h extension following the pattern V8ClassName.h that include pragma once, v8.h, V8Local.h, V8Isolate.h, and declare classes extending from Data with BUN_EXPORT static methods

Applied to files:

  • src/bun.js/bindings/ANSIHelpers.h
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Create V8 class implementations with .cpp extension following the pattern V8ClassName.cpp that include the header, v8_compatibility_assertions.h, use ASSERT_V8_TYPE_LAYOUT_MATCHES macro, and implement methods using isolate->currentHandleScope()->createLocal<T>() for handle creation

Applied to files:

  • src/bun.js/bindings/ANSIHelpers.h
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.txt : Add symbol names without leading underscore to src/symbols.txt for each new V8 API method

Applied to files:

  • src/bun.js/bindings/ANSIHelpers.h
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-09-12T18:16:50.754Z
Learnt from: RiskyMH
Repo: oven-sh/bun PR: 22606
File: src/glob/GlobWalker.zig:449-452
Timestamp: 2025-09-12T18:16:50.754Z
Learning: For Bun codebase: prefer using `std.fs.path.sep` over manual platform separator detection, and use `bun.strings.lastIndexOfChar` instead of `std.mem.lastIndexOfScalar` for string operations.

Applied to files:

  • src/bun.js/bindings/ANSIHelpers.h
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.dyn : Add symbol names with leading underscore and semicolons in braces to src/symbols.dyn for each new V8 API method

Applied to files:

  • src/bun.js/bindings/ANSIHelpers.h
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-10-15T20:19:37.256Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 23680
File: cmake/targets/BuildBun.cmake:822-822
Timestamp: 2025-10-15T20:19:37.256Z
Learning: In the Bun codebase, FFI (Foreign Function Interface) code is compiled separately using TinyCC (tcc), which barely supports C99. Headers like src/bun.js/api/FFI.h and src/bun.js/api/ffi-stdbool.h are only used for FFI compilation with tcc, not with the main compiler. Therefore, C standard changes to the main Bun target do not affect FFI code compilation.

Applied to files:

  • src/bun.js/bindings/ANSIHelpers.h
📚 Learning: 2025-12-23T06:50:31.577Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 25429
File: src/bun.js/bindings/helpers.h:422-422
Timestamp: 2025-12-23T06:50:31.577Z
Learning: In Bun's C++ bindings, when returning an empty JSC::Identifier and a VM is accessible, prefer using vm.propertyNames->emptyIdentifier over constructing with JSC::Identifier(JSC::Identifier::EmptyIdentifierFlag::EmptyIdentifier). The cached identifier from the VM's property names table is more efficient and consistent with WebKit upgrade patterns. Apply this guidance to src/bun.js/bindings/helpers.h and similar header files in the same bindings directory (i.e., any file that constructs an EmptyIdentifier).

Applied to files:

  • src/bun.js/bindings/ANSIHelpers.h
📚 Learning: 2025-11-24T18:37:47.899Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/AGENTS.md:0-0
Timestamp: 2025-11-24T18:37:47.899Z
Learning: Applies to src/bun.js/bindings/v8/**/<UNKNOWN> : <UNKNOWN>

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/napi/napi.zig : For each new V8 C++ method, add both GCC/Clang and MSVC mangled symbol names to the V8API struct in src/napi/napi.zig using extern fn declarations

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-10-01T21:59:54.571Z
Learnt from: taylordotfish
Repo: oven-sh/bun PR: 23169
File: src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h:47-74
Timestamp: 2025-10-01T21:59:54.571Z
Learning: In the new bindings generator (bindgenv2) for `src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h`, the context-aware enumeration conversion overloads intentionally use stricter validation (requiring `value.isString()` without ToString coercion), diverging from Web IDL semantics. This is a design decision documented in comments.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-09-20T05:35:57.318Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 22534
File: src/bun.js/bindings/headers.h:729-731
Timestamp: 2025-09-20T05:35:57.318Z
Learning: symbols.txt in the Bun codebase is specifically for V8 API mangled symbols (without leading underscore), not for general Bun host functions declared with BUN_DECLARE_HOST_FUNCTION. Host functions are handled through different build mechanisms.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Create classes in three parts in C++ when there is a public constructor: Foo (JSDestructibleObject), FooPrototype (JSNonFinalObject), and FooConstructor (InternalFunction)

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-10-24T10:43:09.398Z
Learnt from: fmguerreiro
Repo: oven-sh/bun PR: 23774
File: src/install/PackageManager/updatePackageJSONAndInstall.zig:548-548
Timestamp: 2025-10-24T10:43:09.398Z
Learning: In Bun's Zig codebase, the `as(usize, intCast(...))` cast pattern triggers a Zig compiler bug that causes compilation to hang indefinitely when used in complex control flow contexts (loops + short-circuit operators + optional unwrapping). Avoid this pattern and use simpler alternatives like just `intCast(...)` if type casting is necessary.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-10T00:57:09.173Z
Learnt from: franciscop
Repo: oven-sh/bun PR: 24514
File: src/bun.js/api/crypto/PasswordObject.zig:86-101
Timestamp: 2025-11-10T00:57:09.173Z
Learning: In Bun's Zig codebase (PasswordObject.zig), when validating the parallelism parameter for Argon2, the upper limit is set to 65535 (2^16 - 1) rather than using `std.math.maxInt(u24)` because the latter triggers Zig's truncation limit checks. The value 65535 is a practical upper bound that avoids compiler issues while being sufficient for thread parallelism use cases.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-12-11T02:11:47.024Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 25462
File: src/ast/visitExpr.zig:1644-1695
Timestamp: 2025-12-11T02:11:47.024Z
Learning: In Bun's bundler feature flag implementation (src/ast/visitExpr.zig), the validation for feature() flag names intentionally only rejects UTF-16 strings (checking `is_utf16`) while allowing UTF-8 strings, even though the error message says "must be an ASCII string". This is the intended behavior and should not be changed to enforce strict ASCII validation.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Use localToJSValue() to convert V8 handles to JSC values and perform JSC operations within V8 method implementations

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
🧬 Code graph analysis (1)
src/bun.js/bindings/stripANSI.cpp (1)
src/bun.js/bindings/ANSIHelpers.h (2)
  • findEscapeCharacter (29-56)
  • consumeANSI (64-189)
🪛 Cppcheck (2.19.0)
src/bun.js/bindings/wrapAnsi.cpp

[information] 42-42: Include file

(missingInclude)


[information] 52-52: Include file

(missingIncludeSystem)


[information] 59-59: Include file

(missingIncludeSystem)


[error] 66-66: failed to evaluate #if condition, undefined function-like macro invocation

(syntaxError)

🔇 Additional comments (9)
src/bun.js/bindings/ANSIHelpers.h (3)

10-25: LGTM! Comprehensive escape character detection.

The function correctly identifies all ANSI escape sequence introducers including ESC (0x1b), CSI (0x9b), OSC (0x9d), DCS (0x90), SOS (0x98), PM (0x9e), and APC (0x9f) per ECMA-48.


27-56: LGTM! Well-optimized SIMD search with correct fallback.

The SIMD mask approach efficiently filters candidate escape characters by checking bit patterns common to 0x10-0x1f and 0x90-0x9f ranges. The scalar fallback for remaining bytes ensures correctness.


63-187: LGTM! Robust ANSI sequence state machine.

The state machine correctly handles:

  • CSI sequences with proper final byte detection (0x40-0x7e)
  • OSC sequences with BEL, ST, and ESC \ terminators
  • ST-terminated sequences (DCS, SOS, PM, APC)
  • Two-byte XTerm sequences

Returning end for unterminated sequences is the appropriate behavior for partial input handling.

src/bun.js/bindings/stripANSI.cpp (1)

3-3: LGTM! Clean refactor to shared ANSI helpers.

The delegation to ANSI::findEscapeCharacter and ANSI::consumeANSI eliminates code duplication while maintaining the same behavior. This aligns with the PR's goal of extracting shared ANSI parsing utilities.

Also applies to: 26-26, 44-44

src/bun.js/bindings/wrapAnsi.cpp (5)

352-371: parseSgrCode only handles single-parameter SGR sequences.

The function parses only the first numeric parameter and stops at the first non-digit. Complex SGR sequences like \x1b[38;5;196m (256-color) or \x1b[1;31m (bold red) will only return the first code. This may be intentional for basic style tracking, but compound styles won't be fully preserved.

Is this limitation acceptable for the use case, or should multi-parameter SGR sequences be supported?


427-502: LGTM! Robust ANSI style preservation across line breaks.

The joinRowsWithAnsiPreservation function correctly:

  • Closes active styles before newlines using appropriate reset codes
  • Reopens styles after newlines to maintain visual continuity
  • Handles both SGR codes and OSC 8 hyperlinks

508-612: LGTM! Comprehensive line processing with multiple wrapping modes.

The processLine function correctly handles the interaction between hard, wordWrap, and trim options with appropriate break decisions based on remaining column space.


618-673: LGTM! Clean main implementation flow.

The wrapAnsiImpl function correctly:

  • Handles edge cases (columns == 0, empty input)
  • Normalizes line endings before processing
  • Processes each line separately with ANSI preservation

Note: The std::vector usage on line 631 was already flagged for replacement with WTF::Vector.


104-116: Zig width calculation exports verified as correct.

The template correctly dispatches to the appropriate Zig function based on character size. Parameter types match the Zig export signatures, and the !ambiguousIsNarrow inversion is intentional (C++ parameter is inverted because Zig expects ambiguous_as_wide). UTF-8 version ignores the parameter, UTF-16 uses it.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +706 to +731
// Parse options
WrapAnsiOptions options;
if (optionsValue.isObject()) {
JSC::JSObject* optionsObj = optionsValue.toObject(globalObject);
RETURN_IF_EXCEPTION(scope, {});

JSC::JSValue hardValue = optionsObj->get(globalObject, JSC::Identifier::fromString(vm, "hard"_s));
RETURN_IF_EXCEPTION(scope, {});
if (!hardValue.isUndefined())
options.hard = hardValue.toBoolean(globalObject);

JSC::JSValue wordWrapValue = optionsObj->get(globalObject, JSC::Identifier::fromString(vm, "wordWrap"_s));
RETURN_IF_EXCEPTION(scope, {});
if (!wordWrapValue.isUndefined())
options.wordWrap = wordWrapValue.toBoolean(globalObject);

JSC::JSValue trimValue = optionsObj->get(globalObject, JSC::Identifier::fromString(vm, "trim"_s));
RETURN_IF_EXCEPTION(scope, {});
if (!trimValue.isUndefined())
options.trim = trimValue.toBoolean(globalObject);

JSC::JSValue ambiguousValue = optionsObj->get(globalObject, JSC::Identifier::fromString(vm, "ambiguousIsNarrow"_s));
RETURN_IF_EXCEPTION(scope, {});
if (!ambiguousValue.isUndefined())
options.ambiguousIsNarrow = ambiguousValue.toBoolean(globalObject);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider caching property identifiers for options parsing.

Creating Identifier::fromString for each option lookup on every call is slightly inefficient. For a frequently-called API, consider using cached identifiers from the VM's built-in strings or a static structure.

🤖 Prompt for AI Agents
In `@src/bun.js/bindings/wrapAnsi.cpp` around lines 706 - 731, The options parsing
repeatedly calls JSC::Identifier::fromString for each property which is
wasteful; replace those per-call string-to-identifier conversions with cached
identifiers (e.g., static or VM-provided JSC::Identifier instances) and use them
when looking up properties on optionsValue/optionsObj; update the
WrapAnsiOptions parsing block (symbols: WrapAnsiOptions, optionsValue,
optionsObj, Identifier::fromString, ambiguousIsNarrow, wordWrap, trim, hard) to
use the cached identifiers so each call reuses the same JSC::Identifier objects
instead of recreating them.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@packages/bun-types/bun.d.ts`:
- Around line 647-688: Update the JSDoc for the wrapAnsi function to explicitly
state that CRLF line endings ("\r\n") are normalized to LF ("\n") before
wrapping; mention that this normalization occurs on the input string (parameter:
input) and that wrapped output will use "\n" for line breaks, and note that this
normalization is performed before any other options (e.g., hard wrapping, ANSI
preservation) are applied so callers can rely on consistent line-ending behavior
from wrapAnsi.

In `@src/bun.js/bindings/wrapAnsi.cpp`:
- Around line 630-641: Replace the std::vector usage for the normalized buffer
with WTF::Vector to follow WebKit conventions: change std::vector<Char>
normalized to WTF::Vector<Char> normalized, call
normalized.reserveCapacity(input.size()) (or ensureCapacity if that pattern is
used in the file) instead of reserve, keep the same push_back usage and loop
logic, and add the appropriate `#include` for WTF::Vector (e.g. <wtf/Vector.h> or
"wtf/Vector.h") so the file compiles.
- Around line 352-371: parseSgrCode currently only returns a single simple
numeric SGR and thus misses compound/extended sequences (e.g. "38;5;n" and
"38;2;r;g;b"), and getCloseCode only maps basic color codes so extended colors
are lost across wraps; update parseSgrCode to parse full semicolon-separated SGR
parameter lists (returning either a vector/list of uint32_t or an encoded
representation) and update getCloseCode to accept and handle extended color
params (at least preserving "38;5;n" and "38;2;r;g;b" sequences when computing
closing codes), or normalize/propagate unknown parameter sequences unchanged;
modify callers that expect a single uint32_t (e.g., places using parseSgrCode's
return) to handle the new representation so extended color styles are preserved
across wrapped lines.

In `@src/string/immutable/wrap_ansi.zig`:
- Around line 219-286: parseSgrCode, joinRowsWithAnsiPreservation, parseOsc8Url,
stringWidth, and wrapWord have three related bugs: SGR parsing stops at first
non-digit, only a single escape_code is tracked losing concurrent styles, and
OSC8 termination only checks BEL not the ST sequence. Fix parseSgrCode to
collect the full semicolon-separated parameter list (e.g., parse until the final
'm' and return all params/bytes, not just the first numeric token), change
joinRowsWithAnsiPreservation to track active SGR state as a collection
(stack/set/list of codes) and replay the entire active-code sequence when
restoring styles after a line break, and make OSC8 handling consistent by
recognizing both BEL (0x07) and the ST terminator (ESC followed by '\\') in
parseOsc8Url, stringWidth, and wrapWord so escapes terminate correctly and
width/wrapping and style restoration work for both BEL- and ST-terminated OSC8
sequences.
♻️ Duplicate comments (12)
test/js/bun/util/wrapAnsi.npm.test.ts (1)

36-37: Fix Biome "control character in regex" errors.

This was flagged in a previous review. Use RegExp constructor with escaped strings to satisfy the linter:

Proposed fix
-const stripAnsi = (s: string) => s.replace(/\u001B\[[0-9;]*m|\u001B\]8;;[^\u0007]*\u0007/g, "");
-const hasAnsi = (s: string) => /\u001B\[[0-9;]*m/.test(s);
+const stripAnsiRe = new RegExp("\\x1B\\[[0-9;]*m|\\x1B\\]8;;[^\\x07]*\\x07", "g");
+const hasAnsiRe = new RegExp("\\x1B\\[[0-9;]*m");
+const stripAnsi = (s: string) => s.replace(stripAnsiRe, "");
+const hasAnsi = (s: string) => hasAnsiRe.test(s);
packages/bun-types/bun.d.ts (1)

622-628: Fix WrapAnsiOptions.wordWrap JSDoc: false is not “only explicit newlines”.

The current description is misleading vs expected wrap-ansi semantics (character wrapping/splitting words when needed).

Proposed diff
     /**
      * If `true`, wrap at word boundaries when possible.
-     * If `false`, don't perform word wrapping (only wrap at explicit newlines).
+     * If `false`, wrap by character (may split words) instead of preferring word boundaries.
      *
      * `@default` true
      */
     wordWrap?: boolean;
test/js/bun/util/wrapAnsi.test.ts (3)

42-49: Strengthen wordWrap: false test and fix the misleading comment.

Right now it doesn’t validate behavior (and the comment is likely incorrect).

Proposed diff
   describe("wordWrap option", () => {
-    test("wordWrap false disables wrapping", () => {
-      // Without wordWrap, only explicit newlines should cause breaks
-      const result = Bun.wrapAnsi("hello world", 5, { wordWrap: false });
-      // The behavior may vary - just check it doesn't crash
-      expect(typeof result).toBe("string");
+    test("wordWrap false wraps by character (may split words)", () => {
+      expect(Bun.wrapAnsi("abcdefghij", 4, { wordWrap: false })).toBe("abcd\nefgh\nij");
     });
   });

145-163: Replace edge-case “typeof” assertions with observable expectations (esp. CRLF normalization).

Proposed diff
   describe("edge cases", () => {
     test("handles tabs", () => {
       const input = "a\tb";
       const result = Bun.wrapAnsi(input, 10);
-      expect(typeof result).toBe("string");
+      expect(result).toBe("a\tb");
     });

     test("handles Windows line endings", () => {
       const input = "hello\r\nworld";
       const result = Bun.wrapAnsi(input, 10);
-      expect(typeof result).toBe("string");
+      // wrapAnsi normalizes CRLF to LF
+      expect(result).toBe("hello\nworld");
     });

     test("handles consecutive spaces", () => {
       const input = "hello    world";
-      const result = Bun.wrapAnsi(input, 10);
-      expect(typeof result).toBe("string");
+      const result = Bun.wrapAnsi(input, 20);
+      expect(result).toBe("hello    world");
     });
   });

165-177: Make ambiguousIsNarrow tests assert a wrapping difference.

Proposed diff
   describe("ambiguousIsNarrow option", () => {
     test("default treats ambiguous as narrow", () => {
-      // By default, ambiguous width chars should be treated as width 1
-      const result1 = Bun.wrapAnsi("αβγ", 3);
-      // Greek letters are ambiguous width
-      expect(typeof result1).toBe("string");
+      const result = Bun.wrapAnsi("αβγ", 4, { hard: true, ambiguousIsNarrow: true });
+      expect(result).toBe("αβγ");
     });

     test("ambiguousIsNarrow false treats as wide", () => {
-      const result = Bun.wrapAnsi("αβγ", 3, { ambiguousIsNarrow: false });
-      expect(typeof result).toBe("string");
+      const result = Bun.wrapAnsi("αβγ", 4, { hard: true, ambiguousIsNarrow: false });
+      expect(result).toBe("αβ\nγ");
     });
   });
src/string/immutable/wrap_ansi.zig (1)

158-163: Do not swallow allocator failures in stringVisibleTrimSpacesRight (silent corruption).

catch {} on append operations can silently drop bytes and produce invalid output under memory pressure.

Proposed diff
-    if (options.trim) {
-        for (rows.items) |*row| {
-            stringVisibleTrimSpacesRight(allocator, row);
-        }
-    }
+    if (options.trim) {
+        for (rows.items) |*row| {
+            try stringVisibleTrimSpacesRight(allocator, row);
+        }
+    }

 /// Trim trailing spaces ignoring invisible sequences
-fn stringVisibleTrimSpacesRight(allocator: std.mem.Allocator, row: *std.ArrayListUnmanaged(u8)) void {
+fn stringVisibleTrimSpacesRight(allocator: std.mem.Allocator, row: *std.ArrayListUnmanaged(u8)) !void {
     // Split by spaces and find last word with visible content
     var words_iter = std.mem.splitScalar(u8, row.items, ' ');
     var last: usize = 0;
     var count: usize = 0;
@@
     // Rebuild with only words up to last, keeping trailing ANSI codes
     var new_row: std.ArrayListUnmanaged(u8) = .{};
+    errdefer new_row.deinit(allocator);
     var words_iter2 = std.mem.splitScalar(u8, row.items, ' ');
     var idx: usize = 0;
     var trailing_ansi: std.ArrayListUnmanaged(u8) = .{};
+    defer trailing_ansi.deinit(allocator);

     while (words_iter2.next()) |word| {
         if (idx < last) {
             if (idx > 0) {
-                new_row.append(allocator, ' ') catch {};
+                try new_row.append(allocator, ' ');
             }
-            new_row.appendSlice(allocator, word) catch {};
+            try new_row.appendSlice(allocator, word);
         } else {
             // Append ANSI sequences from trailing words
             for (word) |c| {
                 if (c == '\x1b' or trailing_ansi.items.len > 0) {
-                    trailing_ansi.append(allocator, c) catch {};
+                    try trailing_ansi.append(allocator, c);
                 }
             }
         }
         idx += 1;
     }

     // Append trailing ANSI codes
-    new_row.appendSlice(allocator, trailing_ansi.items) catch {};
-    trailing_ansi.deinit(allocator);
+    try new_row.appendSlice(allocator, trailing_ansi.items);

     // Replace row content
     row.deinit(allocator);
     row.* = new_row;
 }

Also applies to: 169-217

src/string/immutable/visible.zig (1)

1149-1156: API inconsistency: ambiguous_as_wide is unused for UTF-8 path.

The UTF-8 export accepts ambiguous_as_wide but ignores it (line 1153), while the UTF-16 version (line 1161) passes it through. This creates inconsistent behavior across encodings for ambiguous-width characters.

Consider either propagating the parameter through the UTF-8 path or documenting this as intentional behavior.

src/bun.js/bindings/wrapAnsi.cpp (5)

35-68: Missing continuation byte validation in UTF-8 decoding.

The decodeUTF8 function doesn't validate that continuation bytes match the 10xxxxxx pattern. Malformed UTF-8 could produce incorrect codepoints.

Suggested validation
     char32_t cp = 0;
+    // Validate continuation bytes have 10xxxxxx pattern
+    for (size_t i = 1; i < seqLen; ++i) {
+        if ((static_cast<uint8_t>(ptr[i]) & 0xC0) != 0x80) {
+            outLen = 1;
+            return 0xFFFD;
+        }
+    }
+
     switch (seqLen) {

122-125: Consider using WTF::Vector instead of std::vector.

Per maintainer feedback, WTF::Vector should be preferred in WebKit/Bun bindings for consistency and to leverage WebKit's optimized allocator.


149-203: Incomplete escape sequence terminator handling in trimLeadingSpaces.

The escape detection only handles 'm' (SGR final byte) and BEL (0x07), but:

  1. CSI sequences can end with any byte in 0x40-0x7E range, not just 'm'
  2. OSC sequences can also be terminated by ST (ESC \ or 0x9c)

This could cause incorrect trimming behavior with non-SGR CSI sequences.


246-252: Width tracking is incorrect after line wrap.

When a new row is started and a character is appended (lines 247-249), vis is set to 0 but should be set to charWidth to account for the character just appended.

Suggested fix
         } else if (!isInsideEscape) {
             rows.push_back(Row<Char>());
             rows.back().append(it, it + charLen);
-            vis = 0;
+            vis = charWidth;
         } else {

699-704: Handle negative and infinite column values.

toIntegerOrInfinity can return negative values or infinity, which would cause issues when cast to size_t. Negative values wrap to large unsigned values, and infinity becomes undefined behavior.

Suggested fix
     // Get columns
     size_t columns = 0;
     if (!columnsValue.isUndefined()) {
-        columns = static_cast<size_t>(columnsValue.toIntegerOrInfinity(globalObject));
+        double colsDouble = columnsValue.toIntegerOrInfinity(globalObject);
         RETURN_IF_EXCEPTION(scope, {});
+        if (colsDouble > 0 && std::isfinite(colsDouble))
+            columns = static_cast<size_t>(std::min(colsDouble, static_cast<double>(SIZE_MAX)));
     }
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 4ee402e3b966742fcaa72c048ba2a28f6854fc54 and 96c3913.

⛔ Files ignored due to path filters (1)
  • bench/bun.lock is excluded by !**/*.lock
📒 Files selected for processing (13)
  • bench/package.json
  • bench/runner.mjs
  • bench/snippets/wrap-ansi.js
  • packages/bun-types/bun.d.ts
  • src/bun.js/bindings/ANSIHelpers.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/stripANSI.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
  • src/bun.js/bindings/wrapAnsi.h
  • src/string/immutable/visible.zig
  • src/string/immutable/wrap_ansi.zig
  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
🧰 Additional context used
📓 Path-based instructions (6)
**/*.test.ts?(x)

📄 CodeRabbit inference engine (CLAUDE.md)

**/*.test.ts?(x): Never use bun test directly - always use bun bd test to run tests with debug build changes
For single-file tests, prefer -e flag over tempDir
For multi-file tests, prefer tempDir and Bun.spawn over single-file tests
Use normalizeBunSnapshot to normalize snapshot output of tests
Never write tests that check for 'panic', 'uncaught exception', or similar strings in test output
Use tempDir from harness to create temporary directories - do not use tmpdirSync or fs.mkdtempSync
When spawning processes in tests, expect stdout before expecting exit code for more useful error messages on test failure
Do not write flaky tests - do not use setTimeout in tests; instead await the condition to be met
Verify tests fail with USE_SYSTEM_BUN=1 bun test <file> and pass with bun bd test <file> - tests are invalid if they pass with USE_SYSTEM_BUN=1
Test files must end with .test.ts or .test.tsx
Avoid shell commands like find or grep in tests - use Bun's Glob and built-in tools instead

Files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
test/**/*.test.ts?(x)

📄 CodeRabbit inference engine (CLAUDE.md)

Always use port: 0 in tests - do not hardcode ports or use custom random port number functions

Files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}: Use bun bd test <...test file> to run tests with compiled code changes. Do not use bun test as it will not include your changes.
Use bun:test for files ending in *.test.{ts,js,jsx,tsx,mjs,cjs}. For test files without .test extension in test/js/node/test/{parallel,sequential}/*.js, use bun bd <file> instead of bun bd test <file> since they expect exit code 0.
Do not set a timeout on tests. Bun already has timeouts built-in.

Files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
src/bun.js/bindings/**/*.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

src/bun.js/bindings/**/*.cpp: Create classes in three parts in C++ when there is a public constructor: Foo (JSDestructibleObject), FooPrototype (JSNonFinalObject), and FooConstructor (InternalFunction)
Define properties using HashTableValue arrays in C++ JavaScript class bindings
Add iso subspaces for C++ classes with fields in JavaScript class bindings
Cache structures in ZigGlobalObject for JavaScript class bindings

Files:

  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/stripANSI.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
**/*.zig

📄 CodeRabbit inference engine (CLAUDE.md)

In Zig code, be careful with allocators and use defer for cleanup

Files:

  • src/string/immutable/visible.zig
  • src/string/immutable/wrap_ansi.zig
src/**/*.zig

📄 CodeRabbit inference engine (src/CLAUDE.md)

src/**/*.zig: Use the # prefix for private fields in Zig structs, e.g., struct { #foo: u32 };
Use Decl literals in Zig, e.g., const decl: Decl = .{ .binding = 0, .value = 0 };
Place @import statements at the bottom of the file in Zig (auto formatter will handle positioning)
Never use @import() inline inside functions in Zig; always place imports at the bottom of the file or containing struct

Files:

  • src/string/immutable/visible.zig
  • src/string/immutable/wrap_ansi.zig
🧠 Learnings (63)
📓 Common learnings
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24212
File: src/cli/publish_command.zig:782-788
Timestamp: 2025-10-30T21:52:04.707Z
Learning: In the Bun codebase (oven-sh/bun), `enable_ansi_colors` flags are used to gate both ANSI color codes and Unicode box-drawing characters/emoji. This is the established pattern across the codebase.
📚 Learning: 2025-11-20T19:51:32.288Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 24880
File: packages/bun-vscode/package.json:382-385
Timestamp: 2025-11-20T19:51:32.288Z
Learning: In the Bun repository, dependencies may be explicitly added to package.json files (even when not directly imported in code) to force version upgrades on transitive dependencies, particularly as part of Aikido security scanner remediation to ensure vulnerable transitive dependencies resolve to patched versions.

Applied to files:

  • bench/package.json
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/test/v8/v8.test.ts : Add corresponding test cases to test/v8/v8.test.ts using checkSameOutput() function to compare Node.js and Bun output

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • bench/snippets/wrap-ansi.js
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-19T02:44:46.354Z
Learnt from: theshadow27
Repo: oven-sh/bun PR: 23798
File: packages/bun-otel/context-propagation.test.ts:1-1
Timestamp: 2025-10-19T02:44:46.354Z
Learning: In the Bun repository, standalone packages under packages/ (e.g., bun-vscode, bun-inspector-protocol, bun-plugin-yaml, bun-plugin-svelte, bun-debug-adapter-protocol, bun-otel) co-locate their tests with package source code using *.test.ts files. This follows standard npm/monorepo patterns. The test/ directory hierarchy (test/js/bun/, test/cli/, test/js/node/) is reserved for testing Bun's core runtime APIs and built-in functionality, not standalone packages.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun bd test <...test file>` to run tests with compiled code changes. Do not use `bun test` as it will not include your changes.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun:test` for files ending in `*.test.{ts,js,jsx,tsx,mjs,cjs}`. For test files without .test extension in test/js/node/test/{parallel,sequential}/*.js, use `bun bd <file>` instead of `bun bd test <file>` since they expect exit code 0.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2026-01-14T21:08:10.406Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/js/node/test/parallel/CLAUDE.md:0-0
Timestamp: 2026-01-14T21:08:10.406Z
Learning: These are Node.js compatibility tests not written by Bun and cannot be modified

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • bench/snippets/wrap-ansi.js
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Use `normalizeBunSnapshot` to normalize snapshot output of tests

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Verify tests fail with `USE_SYSTEM_BUN=1 bun test <file>` and pass with `bun bd test <file>` - tests are invalid if they pass with USE_SYSTEM_BUN=1

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-26T01:32:04.844Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 24082
File: test/cli/test/coverage.test.ts:60-112
Timestamp: 2025-10-26T01:32:04.844Z
Learning: In the Bun repository test files (test/cli/test/*.test.ts), when spawning Bun CLI commands with Bun.spawnSync for testing, prefer using stdio: ["inherit", "inherit", "inherit"] to inherit stdio streams rather than piping them.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Never use `bun test` directly - always use `bun bd test` to run tests with debug build changes

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Do not set a timeout on tests. Bun already has timeouts built-in.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-12-11T02:11:47.024Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 25462
File: src/ast/visitExpr.zig:1644-1695
Timestamp: 2025-12-11T02:11:47.024Z
Learning: In Bun's bundler feature flag implementation (src/ast/visitExpr.zig), the validation for feature() flag names intentionally only rejects UTF-16 strings (checking `is_utf16`) while allowing UTF-8 strings, even though the error message says "must be an ASCII string". This is the intended behavior and should not be changed to enforce strict ASCII validation.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • src/string/immutable/visible.zig
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-10-30T21:52:04.707Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24212
File: src/cli/publish_command.zig:782-788
Timestamp: 2025-10-30T21:52:04.707Z
Learning: In the Bun codebase (oven-sh/bun), `enable_ansi_colors` flags are used to gate both ANSI color codes and Unicode box-drawing characters/emoji. This is the established pattern across the codebase.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/ANSIHelpers.h
  • src/string/immutable/visible.zig
  • src/bun.js/bindings/stripANSI.cpp
  • test/js/bun/util/wrapAnsi.test.ts
  • src/bun.js/bindings/wrapAnsi.cpp
  • src/string/immutable/wrap_ansi.zig
📚 Learning: 2025-09-30T22:53:19.887Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 23117
File: src/bun.js/test/snapshot.zig:265-276
Timestamp: 2025-09-30T22:53:19.887Z
Learning: In Bun's snapshot testing (src/bun.js/test/snapshot.zig), multiple inline snapshots at the same line and column (same call position) must have identical values. However, multiple inline snapshots on the same line at different columns are allowed to have different values. The check is position-specific (line+col), not line-wide.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-09-24T05:48:59.872Z
Learnt from: nektro
Repo: oven-sh/bun PR: 22806
File: scripts/runner.node.mjs:687-689
Timestamp: 2025-09-24T05:48:59.872Z
Learning: In the Bun codebase, the `startGroup` utility function in scripts/runner.node.mjs automatically closes any previously open group when called, so `startGroup("End")` correctly closes the final test group and keeps subsequent output ungrouped.

Applied to files:

  • test/js/bun/util/wrapAnsi.npm.test.ts
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.h : Add BUN_EXPORT visibility attribute to all public V8 API functions to ensure proper symbol export across platforms

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/ANSIHelpers.h
  • src/string/immutable/visible.zig
  • src/bun.js/bindings/stripANSI.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.h : Create V8 class headers with .h extension following the pattern V8ClassName.h that include pragma once, v8.h, V8Local.h, V8Isolate.h, and declare classes extending from Data with BUN_EXPORT static methods

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/ANSIHelpers.h
  • src/bun.js/bindings/stripANSI.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Add iso subspaces for C++ classes with fields in JavaScript class bindings

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/ANSIHelpers.h
  • src/bun.js/bindings/stripANSI.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Create classes in three parts in C++ when there is a public constructor: Foo (JSDestructibleObject), FooPrototype (JSNonFinalObject), and FooConstructor (InternalFunction)

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Create V8 class implementations with .cpp extension following the pattern V8ClassName.cpp that include the header, v8_compatibility_assertions.h, use ASSERT_V8_TYPE_LAYOUT_MATCHES macro, and implement methods using isolate->currentHandleScope()->createLocal<T>() for handle creation

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/ANSIHelpers.h
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.dyn : Add symbol names with leading underscore and semicolons in braces to src/symbols.dyn for each new V8 API method

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:37:47.899Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/AGENTS.md:0-0
Timestamp: 2025-11-24T18:37:47.899Z
Learning: Applies to src/bun.js/bindings/v8/**/<UNKNOWN> : <UNKNOWN>

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/napi/napi.zig : For each new V8 C++ method, add both GCC/Clang and MSVC mangled symbol names to the V8API struct in src/napi/napi.zig using extern fn declarations

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/string/immutable/visible.zig
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-09-20T05:35:57.318Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 22534
File: src/bun.js/bindings/headers.h:729-731
Timestamp: 2025-09-20T05:35:57.318Z
Learning: symbols.txt in the Bun codebase is specifically for V8 API mangled symbols (without leading underscore), not for general Bun host functions declared with BUN_DECLARE_HOST_FUNCTION. Host functions are handled through different build mechanisms.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.txt : Add symbol names without leading underscore to src/symbols.txt for each new V8 API method

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Cache structures in ZigGlobalObject for JavaScript class bindings

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Use localToJSValue() to convert V8 handles to JSC values and perform JSC operations within V8 method implementations

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-10-15T20:19:37.256Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 23680
File: cmake/targets/BuildBun.cmake:822-822
Timestamp: 2025-10-15T20:19:37.256Z
Learning: In the Bun codebase, FFI (Foreign Function Interface) code is compiled separately using TinyCC (tcc), which barely supports C99. Headers like src/bun.js/api/FFI.h and src/bun.js/api/ffi-stdbool.h are only used for FFI compilation with tcc, not with the main compiler. Therefore, C standard changes to the main Bun target do not affect FFI code compilation.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/string/immutable/visible.zig
📚 Learning: 2025-12-23T06:50:31.577Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 25429
File: src/bun.js/bindings/helpers.h:422-422
Timestamp: 2025-12-23T06:50:31.577Z
Learning: In Bun's C++ bindings, when returning an empty JSC::Identifier and a VM is accessible, prefer using vm.propertyNames->emptyIdentifier over constructing with JSC::Identifier(JSC::Identifier::EmptyIdentifierFlag::EmptyIdentifier). The cached identifier from the VM's property names table is more efficient and consistent with WebKit upgrade patterns. Apply this guidance to src/bun.js/bindings/helpers.h and similar header files in the same bindings directory (i.e., any file that constructs an EmptyIdentifier).

Applied to files:

  • src/bun.js/bindings/wrapAnsi.h
  • src/bun.js/bindings/ANSIHelpers.h
📚 Learning: 2025-11-24T18:37:11.466Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/js/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:37:11.466Z
Learning: Write JS builtins for Bun's Node.js compatibility and APIs, and run `bun bd` after changes

Applied to files:

  • bench/snippets/wrap-ansi.js
  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/test/v8/v8-module/main.cpp : Register new V8 API test functions in the Init method using NODE_SET_METHOD with exports object

Applied to files:

  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-12-23T06:50:41.142Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 25429
File: src/bun.js/bindings/helpers.h:422-422
Timestamp: 2025-12-23T06:50:41.142Z
Learning: In Bun's C++ bindings (src/bun.js/bindings/helpers.h and similar files), when returning an empty JSC::Identifier and a VM is accessible, prefer using `vm.propertyNames->emptyIdentifier` over constructing with `JSC::Identifier(JSC::Identifier::EmptyIdentifierFlag::EmptyIdentifier)`. The cached identifier from the VM's property names table is more efficient and consistent with WebKit upgrade patterns.

Applied to files:

  • src/bun.js/bindings/BunObject.cpp
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Use V8_UNIMPLEMENTED() macro for functions not yet implemented in V8 compatibility classes

Applied to files:

  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/test/v8/v8-module/main.cpp : Create test functions in test/v8/v8-module/main.cpp that take FunctionCallbackInfo<Value> parameter, use the test V8 API, print results for comparison with Node.js, and return Undefined

Applied to files:

  • src/bun.js/bindings/BunObject.cpp
📚 Learning: 2025-11-14T16:07:01.064Z
Learnt from: RiskyMH
Repo: oven-sh/bun PR: 24719
File: docs/bundler/executables.mdx:527-560
Timestamp: 2025-11-14T16:07:01.064Z
Learning: In the Bun repository, certain bundler features like compile with code splitting (--compile --splitting) are CLI-only and not supported in the Bun.build() JavaScript API. Tests for CLI-only features use backend: "cli" flag (e.g., test/bundler/bundler_compile_splitting.test.ts). The CompileBuildConfig interface correctly restricts these with splitting?: never;. When documenting CLI-only bundler features, add a note clarifying they're not available via the programmatic API.

Applied to files:

  • packages/bun-types/bun.d.ts
  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-19T02:52:37.412Z
Learnt from: theshadow27
Repo: oven-sh/bun PR: 23798
File: packages/bun-otel/tsconfig.json:1-15
Timestamp: 2025-10-19T02:52:37.412Z
Learning: In the Bun repository, packages under packages/ (e.g., bun-otel) can follow a TypeScript-first pattern where package.json exports point directly to .ts files (not compiled .js files). Bun natively runs TypeScript, so consumers import .ts sources directly and receive full type information without needing compiled .d.ts declaration files. For such packages, adding "declaration": true or "outDir" in tsconfig.json is unnecessary and would break the export structure.
<!-- [remove_learning]
ceedde95-980e-4898-a2c6-40ff73913664

Applied to files:

  • packages/bun-types/bun.d.ts
📚 Learning: 2025-09-12T18:16:50.754Z
Learnt from: RiskyMH
Repo: oven-sh/bun PR: 22606
File: src/glob/GlobWalker.zig:449-452
Timestamp: 2025-09-12T18:16:50.754Z
Learning: For Bun codebase: prefer using `std.fs.path.sep` over manual platform separator detection, and use `bun.strings.lastIndexOfChar` instead of `std.mem.lastIndexOfScalar` for string operations.

Applied to files:

  • src/bun.js/bindings/ANSIHelpers.h
📚 Learning: 2025-10-24T10:43:09.398Z
Learnt from: fmguerreiro
Repo: oven-sh/bun PR: 23774
File: src/install/PackageManager/updatePackageJSONAndInstall.zig:548-548
Timestamp: 2025-10-24T10:43:09.398Z
Learning: In Bun's Zig codebase, the `as(usize, intCast(...))` cast pattern triggers a Zig compiler bug that causes compilation to hang indefinitely when used in complex control flow contexts (loops + short-circuit operators + optional unwrapping). Avoid this pattern and use simpler alternatives like just `intCast(...)` if type casting is necessary.

Applied to files:

  • src/string/immutable/visible.zig
  • src/bun.js/bindings/wrapAnsi.cpp
  • src/string/immutable/wrap_ansi.zig
📚 Learning: 2025-10-16T17:32:03.074Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 23710
File: src/install/PackageManager/PackageManagerOptions.zig:187-193
Timestamp: 2025-10-16T17:32:03.074Z
Learning: In Bun's codebase (particularly in files like src/install/PackageManager/PackageManagerOptions.zig), mixing bun.EnvVar.*.get() and bun.EnvVar.*.platformGet() for environment variable lookups is intentional and safe. The code is protected by compile-time platform checks (Environment.isWindows, etc.), and compilation will fail if the wrong function is used on the wrong platform. This pattern should not be flagged as a consistency issue.

Applied to files:

  • src/string/immutable/visible.zig
📚 Learning: 2025-11-12T04:11:52.293Z
Learnt from: cirospaciari
Repo: oven-sh/bun PR: 24622
File: src/deps/uws/us_socket_t.zig:112-113
Timestamp: 2025-11-12T04:11:52.293Z
Learning: In Bun's Zig codebase, when passing u32 values to C FFI functions that expect c_uint parameters, no explicit intCast is needed because c_uint is equivalent to u32 on Bun's target platforms and Zig allows implicit coercion between equivalent types. This pattern is used consistently throughout src/deps/uws/us_socket_t.zig in functions like setTimeout, setLongTimeout, and setKeepalive.

Applied to files:

  • src/string/immutable/visible.zig
📚 Learning: 2025-11-10T00:57:09.173Z
Learnt from: franciscop
Repo: oven-sh/bun PR: 24514
File: src/bun.js/api/crypto/PasswordObject.zig:86-101
Timestamp: 2025-11-10T00:57:09.173Z
Learning: In Bun's Zig codebase (PasswordObject.zig), when validating the parallelism parameter for Argon2, the upper limit is set to 65535 (2^16 - 1) rather than using `std.math.maxInt(u24)` because the latter triggers Zig's truncation limit checks. The value 65535 is a practical upper bound that avoids compiler issues while being sufficient for thread parallelism use cases.

Applied to files:

  • src/string/immutable/visible.zig
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-09-04T02:04:43.094Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 22278
File: src/ast/E.zig:980-1003
Timestamp: 2025-09-04T02:04:43.094Z
Learning: In Bun's Zig codebase, `as(i32, u8_or_u16_value)` is sufficient for casting u8/u16 to i32 in comparison operations. `intCast` is not required in this context, and the current casting approach compiles successfully.

Applied to files:

  • src/string/immutable/visible.zig
📚 Learning: 2025-10-16T21:24:52.779Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 23710
File: src/crash_handler.zig:1415-1423
Timestamp: 2025-10-16T21:24:52.779Z
Learning: When a boolean EnvVar in src/envvars.zig is defined with a default value (e.g., `.default = false`), the `get()` method returns `bool` instead of `?bool`. This means you cannot distinguish between "environment variable not set" and "environment variable explicitly set to the default value". For opt-out scenarios where detection of explicit setting is needed (like `BUN_ENABLE_CRASH_REPORTING` on platforms where crash reporting defaults to enabled), either: (1) don't provide a default value so `get()` returns `?bool`, or (2) use the returned boolean directly instead of only checking if it's true.

Applied to files:

  • src/string/immutable/visible.zig
📚 Learning: 2025-10-15T20:19:38.580Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 23680
File: cmake/targets/BuildBun.cmake:822-822
Timestamp: 2025-10-15T20:19:38.580Z
Learning: In the Bun codebase, FFI is compiled with tcc (TinyCC), which barely supports C99. The headers `src/bun.js/api/FFI.h` and `src/bun.js/api/ffi-stdbool.h` are only used for FFI compilation with tcc, not for the main Bun target. Therefore, C23 compatibility concerns (such as bool/true/false keyword conflicts) do not apply to these FFI headers.

Applied to files:

  • src/string/immutable/visible.zig
📚 Learning: 2026-01-05T16:32:07.551Z
Learnt from: alii
Repo: oven-sh/bun PR: 25474
File: src/bun.js/event_loop/Sigusr1Handler.zig:0-0
Timestamp: 2026-01-05T16:32:07.551Z
Learning: In Zig codebases (e.g., Bun), treat std.posix.sigaction as returning void and do not perform runtime error handling for its failure. The Zig standard library views sigaction failures as programmer errors (unreachable) because they only occur with invalid signals like SIGKILL/SIGSTOP. Apply this pattern across Zig files that call sigaction (e.g., crash_handler.zig, main.zig, filter_run.zig, process.zig) and ensure failures are not handled as recoverable errors; prefer reaching an explicit unreachable/compile-time assumption when such failures are detected.

Applied to files:

  • src/string/immutable/visible.zig
  • src/string/immutable/wrap_ansi.zig
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Never write tests that check for 'panic', 'uncaught exception', or similar strings in test output

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-08T13:48:02.430Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 23373
File: test/js/bun/tarball/extract.test.ts:107-111
Timestamp: 2025-10-08T13:48:02.430Z
Learning: In Bun's test runner, use `expect(async () => { await ... }).toThrow()` to assert async rejections. Unlike Jest/Vitest, Bun does not require `await expect(...).rejects.toThrow()` - the async function wrapper with `.toThrow()` is the correct pattern for async error assertions in Bun tests.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Ensure V8 API tests compare identical C++ code output between Node.js and Bun through the test suite validation process

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-09-20T00:58:38.042Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 22568
File: test/js/valkey/valkey.test.ts:561-564
Timestamp: 2025-09-20T00:58:38.042Z
Learning: For test/js/valkey/valkey.test.ts, do not comment on synchronous throw assertions for async Redis methods (like ctx.redis.set(), ctx.redis.unsubscribe(), etc.) - Bun's Redis client implementation differs from Node.js and can throw synchronously even for async methods. The maintainer has explicitly requested to stop looking at this error pattern.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.test.ts?(x) : Avoid shell commands like `find` or `grep` in tests - use Bun's Glob and built-in tools instead

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-18T05:23:24.403Z
Learnt from: theshadow27
Repo: oven-sh/bun PR: 23798
File: test/js/bun/telemetry-server.test.ts:91-100
Timestamp: 2025-10-18T05:23:24.403Z
Learning: In the Bun codebase, telemetry tests (test/js/bun/telemetry-*.test.ts) should focus on telemetry API behavior: configure/disable/isEnabled, callback signatures and invocation, request ID correlation, and error handling. HTTP protocol behaviors like status code normalization (e.g., 200 with empty body → 204) should be tested in HTTP server tests (test/js/bun/http/), not in telemetry tests. Keep separation of concerns: telemetry tests verify the telemetry API contract; HTTP tests verify HTTP semantics.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*-fixture.ts : Test files that spawn Bun processes should end in `*-fixture.ts` to identify them as test fixtures rather than tests themselves.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-09-20T03:39:41.770Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 22534
File: test/regression/issue/21830.fixture.ts:14-63
Timestamp: 2025-09-20T03:39:41.770Z
Learning: Bun's test runner supports async describe callbacks, unlike Jest/Vitest where describe callbacks must be synchronous. The syntax `describe("name", async () => { ... })` is valid in Bun.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-25T17:20:19.041Z
Learnt from: theshadow27
Repo: oven-sh/bun PR: 24063
File: test/js/bun/telemetry/server-header-injection.test.ts:5-20
Timestamp: 2025-10-25T17:20:19.041Z
Learning: In the Bun telemetry codebase, tests are organized into two distinct layers: (1) Internal API tests in test/js/bun/telemetry/ use numeric InstrumentKind enum values to test Zig↔JS injection points and low-level integration; (2) Public API tests in packages/bun-otel/test/ use string InstrumentKind values ("http", "fetch", etc.) to test the public-facing BunSDK and instrumentation APIs. This separation allows internal tests to use efficient numeric enums for refactoring flexibility while the public API maintains a developer-friendly string-based interface.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-01T21:59:54.571Z
Learnt from: taylordotfish
Repo: oven-sh/bun PR: 23169
File: src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h:47-74
Timestamp: 2025-10-01T21:59:54.571Z
Learning: In the new bindings generator (bindgenv2) for `src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h`, the context-aware enumeration conversion overloads intentionally use stricter validation (requiring `value.isString()` without ToString coercion), diverging from Web IDL semantics. This is a design decision documented in comments.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-03T20:43:06.996Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24273
File: src/bun.js/test/snapshot.zig:19-19
Timestamp: 2025-11-03T20:43:06.996Z
Learning: In Bun's Zig codebase, when storing JSValue objects in collections like ArrayList, use `jsc.Strong.Optional` (not raw JSValue). When adding values, wrap them with `jsc.Strong.Optional.create(value, globalThis)`. In cleanup code, iterate the collection calling `.deinit()` on each Strong.Optional item before calling `.deinit()` on the ArrayList itself. This pattern automatically handles GC protection. See examples in src/bun.js/test/ScopeFunctions.zig and src/bun.js/node/node_cluster_binding.zig.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:37:11.466Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/js/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:37:11.466Z
Learning: Applies to src/js/{builtins,node,bun,thirdparty,internal}/**/*.{ts,js} : Use JSC intrinsics (prefixed with `$`) such as `$Array.from()`, `$isCallable()`, and `$newArrayWithSize()` for performance-critical operations

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Use JSC::WriteBarrier for heap-allocated references in V8 objects and implement visitChildren() for custom heap objects to support garbage collection

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to **/*.zig : In Zig code, be careful with allocators and use defer for cleanup

Applied to files:

  • src/string/immutable/wrap_ansi.zig
📚 Learning: 2025-09-02T18:09:21.647Z
Learnt from: taylordotfish
Repo: oven-sh/bun PR: 22227
File: src/allocators/allocation_scope.zig:79-85
Timestamp: 2025-09-02T18:09:21.647Z
Learning: In Bun's Zig codebase, prefer simple, direct error handling patterns. When an error type has only one variant (like FreeError with only NotAllocated), use a switch statement rather than generic catch blocks to leverage compile-time knowledge and avoid unnecessary complexity. Avoid duplicating function calls across error paths when a cleaner structure is possible.

Applied to files:

  • src/string/immutable/wrap_ansi.zig
📚 Learning: 2025-09-02T18:27:23.279Z
Learnt from: taylordotfish
Repo: oven-sh/bun PR: 22227
File: src/collections/multi_array_list.zig:24-24
Timestamp: 2025-09-02T18:27:23.279Z
Learning: The `#allocator` syntax in bun's custom Zig implementation is valid and does not require quoting with @"#allocator". Private fields using the `#` prefix work correctly throughout the codebase without special quoting syntax.

Applied to files:

  • src/string/immutable/wrap_ansi.zig
📚 Learning: 2025-11-11T22:55:04.070Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24571
File: src/css/selectors/parser.zig:908-916
Timestamp: 2025-11-11T22:55:04.070Z
Learning: In oven-sh/bun, CSS serialization uses an arena allocator. In src/css/selectors/parser.zig, functions like PseudoClass.toCss and PseudoElement.toCss intentionally do not call deinit on std.Io.Writer.Allocating, scratch buffers, or css.Printer because dest.allocator is an arena and these temporaries are reclaimed when the CSS pass completes. Only debug-only paths (e.g., DeclarationBlock.DebugFmt in src/css/declaration.zig) may explicitly deinit.

Applied to files:

  • src/string/immutable/wrap_ansi.zig
🧬 Code graph analysis (3)
test/js/bun/util/wrapAnsi.npm.test.ts (1)
bench/snippets/wrap-ansi.js (3)
  • red (10-10)
  • green (11-11)
  • blue (12-12)
bench/snippets/wrap-ansi.js (1)
bench/runner.mjs (5)
  • summary (17-17)
  • summary (17-17)
  • bench (15-15)
  • bench (15-15)
  • run (7-13)
src/bun.js/bindings/wrapAnsi.cpp (1)
src/bun.js/api/BunObject.bind.ts (1)
  • stringWidth (30-36)
🪛 Biome (2.1.2)
test/js/bun/util/wrapAnsi.npm.test.ts

[error] 36-36: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 36-36: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 36-36: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 36-36: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)


[error] 37-37: Unexpected control character in a regular expression.

Control characters are unusual and potentially incorrect inputs, so they are disallowed.

(lint/suspicious/noControlCharactersInRegex)

🪛 Cppcheck (2.19.0)
src/bun.js/bindings/wrapAnsi.cpp

[information] 42-42: Include file

(missingInclude)


[information] 52-52: Include file

(missingIncludeSystem)


[information] 59-59: Include file

(missingIncludeSystem)


[error] 66-66: failed to evaluate #if condition, undefined function-like macro invocation

(syntaxError)

🔇 Additional comments (28)
bench/package.json (1)

21-21: LGTM!

The wrap-ansi dependency addition is appropriate for benchmarking the new Bun.wrapAnsi() implementation against the npm package.

src/bun.js/bindings/wrapAnsi.h (1)

1-9: LGTM!

The header follows the standard Bun binding pattern for host functions. The #pragma once guard, root.h include, and JSC_DECLARE_HOST_FUNCTION macro usage are correct.

bench/runner.mjs (1)

17-17: LGTM!

The summary export follows the existing pattern for re-exporting Mitata utilities and is used correctly by the new wrap-ansi benchmark.

src/bun.js/bindings/stripANSI.cpp (2)

3-3: Good refactoring to centralize ANSI handling.

Extracting the ANSI parsing utilities into ANSIHelpers.h promotes code reuse between stripANSI and the new wrapAnsi implementation.


25-49: LGTM!

The updated stripANSI function correctly delegates to the shared ANSI::findEscapeCharacter and ANSI::consumeANSI helpers while maintaining the same logic flow and safety invariants.

bench/snippets/wrap-ansi.js (4)

1-2: LGTM!

Imports are correct. The summary export from runner.mjs is properly used for grouping benchmark comparisons.


4-35: Well-designed test fixtures.

The fixtures provide comprehensive coverage including plain text of varying lengths, ANSI SGR color codes, full-width Japanese characters, emoji, and OSC 8 hyperlinks. This ensures the benchmark exercises all key code paths in the wrap-ansi implementation.


37-101: Comprehensive benchmark coverage.

The benchmark groups systematically compare npm and Bun implementations across all relevant scenarios: varying text lengths, ANSI colors, hard wrapping, full-width characters, emoji, hyperlinks, and trim options. The use of summary() groups enables clear side-by-side performance comparisons.


103-103: LGTM!

Correctly awaits the benchmark run.

src/bun.js/bindings/BunObject.cpp (2)

79-81: LGTM!

The declaration follows the established pattern used for similar host functions in this file (e.g., jsFunctionBunStripANSI on line 79).


805-806: LGTM!

The LUT entry is correctly added with arity 3 matching the API signature (string, columns, options?), placed appropriately near the related stripANSI entry.

test/js/bun/util/wrapAnsi.npm.test.ts (8)

1-23: LGTM!

Proper MIT license attribution for the ported tests from the wrap-ansi npm package.


28-33: LGTM!

ANSI color helpers are correctly implemented and consistent with the pattern used in the benchmark file.


49-73: LGTM!

Tests correctly verify both the exact output with ANSI codes preserved and the line length constraints.


103-128: LGTM!

Good coverage of hard wrapping behavior including word breaking and ANSI-only row handling.


164-171: Good documentation of implementation difference.

The comment clearly explains why Bun's output differs from the npm package while producing visually equivalent results. This is helpful for future maintainers.


173-182: LGTM!

Good coverage of Unicode handling including fullwidth characters and surrogate pairs.


225-243: LGTM!

Excellent coverage of OSC 8 hyperlink wrapping, including complex scenarios with nested colors and emoji.


250-255: LGTM!

Good coverage of CRLF normalization behavior.

src/string/immutable/wrap_ansi.zig (1)

59-70: Confirm whether space-only splitting is sufficient for Bun’s intended wrap semantics.

The implementation splits words only on ' ' (not tabs/Unicode whitespace/word boundaries). This matches the open question in the PR discussion; please confirm this is intended (and covered by the ported wrap-ansi tests for parity).

Also applies to: 98-156

src/bun.js/bindings/ANSIHelpers.h (4)

1-8: LGTM! Clean header structure.

The header uses #pragma once, includes necessary dependencies (root.h for common definitions, SIMDHelpers.h for SIMD operations), and properly namespaces the utilities under Bun::ANSI.


9-25: LGTM! Comprehensive escape character detection.

The function correctly identifies all ANSI escape sequence introducers: ESC (0x1b) and the C1 control codes (CSI, OSC, DCS, SOS, PM, APC) per ECMA-48 specification.


27-56: LGTM! Well-designed SIMD optimization with correct fallback.

The SIMD mask efficiently identifies candidate escape characters in the 0x10-0x1f and 0x90-0x9f ranges. The scalar fallback with isEscapeCharacter then precisely filters to actual escape introducers. This two-phase approach is a good performance trade-off.


58-187: LGTM! Robust ANSI sequence state machine.

The state machine correctly handles:

  • CSI sequences (ESC [ ... final byte 0x40-0x7E)
  • OSC sequences with BEL, ST (0x9c), or ESC \ terminators
  • DCS/SOS/PM/APC sequences requiring ST termination
  • Two-byte XTerm sequences (ESC + space/punctuation + char)
  • Consecutive escape sequences (via State::start loop-back)

The implementation properly returns end for unterminated sequences, which is safe behavior.

src/string/immutable/visible.zig (1)

1158-1173: LGTM! UTF-16, Latin-1, and codepoint width exports are correct.

  • UTF-16 export correctly passes through ambiguous_as_wide
  • Latin-1 export appropriately omits the ambiguous parameter since Latin-1 (0x00-0xFF) contains no East Asian ambiguous-width characters
  • Bun__codepointWidth is a clean wrapper for per-codepoint width evaluation
src/bun.js/bindings/wrapAnsi.cpp (3)

70-90: LGTM! UTF-16 surrogate pair decoding is correct.

The function properly handles high surrogates (0xD800-0xDBFF) followed by low surrogates (0xDC00-0xDFFF) and correctly computes the supplementary plane codepoint.


427-502: LGTM! ANSI style preservation logic is sound.

The function correctly:

  • Tracks the current SGR code and OSC 8 hyperlink URL
  • Closes styles before newlines and reopens them after
  • Uses getCloseCode for proper SGR reset sequences

The single-code tracking is a known limitation (see SGR parsing comment), but for common styling use cases this works well.


733-742: LGTM! Encoding-aware dispatch is correct.

The binding correctly dispatches to the appropriate implementation based on string encoding (is8Bit() for Latin1/UTF-8, otherwise UTF-16), preserving the original encoding characteristics through the wrapping process.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +647 to +688
/**
* Wrap a string to fit within the specified column width, preserving ANSI escape codes.
*
* This function is designed to be compatible with the popular "wrap-ansi" NPM package.
*
* Features:
* - Preserves ANSI escape codes (colors, styles) across line breaks
* - Supports SGR codes (colors, bold, italic, etc.) and OSC 8 hyperlinks
* - Respects Unicode display widths (full-width characters, emoji)
* - Word wrapping at word boundaries (configurable)
*
* @category Utilities
*
* @param input The string to wrap
* @param columns The maximum column width
* @param options Wrapping options
* @returns The wrapped string
*
* @example
* ```ts
* import { wrapAnsi } from "bun";
*
* console.log(wrapAnsi("hello world", 5));
* // Output:
* // hello
* // world
*
* // Preserves ANSI colors across line breaks
* console.log(wrapAnsi("\u001b[31mhello world\u001b[0m", 5));
* // Output:
* // \u001b[31mhello\u001b[0m
* // \u001b[31mworld\u001b[0m
*
* // Hard wrap long words
* console.log(wrapAnsi("abcdefghij", 3, { hard: true }));
* // Output:
* // abc
* // def
* // ghi
* // j
* ```
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Document CRLF normalization in wrapAnsi() JSDoc.

PR notes say \r\n is normalized to \n, but the API docs don’t mention it.

Proposed diff
    * Features:
    * - Preserves ANSI escape codes (colors, styles) across line breaks
    * - Supports SGR codes (colors, bold, italic, etc.) and OSC 8 hyperlinks
    * - Respects Unicode display widths (full-width characters, emoji)
+   * - Normalizes CRLF (`\r\n`) to LF (`\n`)
    * - Word wrapping at word boundaries (configurable)
🤖 Prompt for AI Agents
In `@packages/bun-types/bun.d.ts` around lines 647 - 688, Update the JSDoc for the
wrapAnsi function to explicitly state that CRLF line endings ("\r\n") are
normalized to LF ("\n") before wrapping; mention that this normalization occurs
on the input string (parameter: input) and that wrapped output will use "\n" for
line breaks, and note that this normalization is performed before any other
options (e.g., hard wrapping, ANSI preservation) are applied so callers can rely
on consistent line-ending behavior from wrapAnsi.

Comment on lines +352 to +371
template<typename Char>
static std::optional<uint32_t> parseSgrCode(const Char* start, const Char* end)
{
if (end - start < 3 || start[0] != 0x1b || start[1] != '[')
return std::nullopt;

uint32_t code = 0;
for (const Char* it = start + 2; it < end; ++it) {
Char c = *it;
if (c >= '0' && c <= '9') {
code = code * 10 + (c - '0');
} else if (c == 'm') {
return code;
} else {
break;
}
}

return std::nullopt;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cd src/bun.js/bindings && wc -l wrapAnsi.cpp

Repository: oven-sh/bun

Length of output: 71


🏁 Script executed:

cd src/bun.js/bindings && cat -n wrapAnsi.cpp | sed -n '340,380p'

Repository: oven-sh/bun

Length of output: 1596


🏁 Script executed:

cd src/bun.js/bindings && grep -n "parseSgrCode" wrapAnsi.cpp | head -20

Repository: oven-sh/bun

Length of output: 240


🏁 Script executed:

cd src/bun.js/bindings && cat -n wrapAnsi.cpp | sed -n '1,50p'

Repository: oven-sh/bun

Length of output: 1912


🏁 Script executed:

cd src/bun.js/bindings && cat -n wrapAnsi.cpp | sed -n '445,475p'

Repository: oven-sh/bun

Length of output: 1575


🏁 Script executed:

cd src/bun.js/bindings && cat -n wrapAnsi.cpp | sed -n '400,445p'

Repository: oven-sh/bun

Length of output: 1437


🏁 Script executed:

cd src/bun.js/bindings && cat -n wrapAnsi.cpp | sed -n '475,510p'

Repository: oven-sh/bun

Length of output: 1686


🏁 Script executed:

cd src/bun.js/bindings && grep -n "38;5\|38;2\|extended\|compound" wrapAnsi.cpp

Repository: oven-sh/bun

Length of output: 37


🏁 Script executed:

cd src/bun.js/bindings && cat -n wrapAnsi.cpp | sed -n '385,425p'

Repository: oven-sh/bun

Length of output: 1096


SGR parsing doesn't handle extended color codes.

parseSgrCode only parses simple numeric codes and doesn't support compound SGR sequences like 38;5;n (256-color) or 38;2;r;g;b (true color). Additionally, getCloseCode only maps standard color codes (30–37, 40–47, 90–97, 100–107) and text attributes (1–9), leaving extended colors unsupported. This means extended color styles won't be properly preserved across wrapped lines.

Consider whether this limitation is acceptable for the use case or if more complete SGR parsing is needed for full ANSI style preservation.

🤖 Prompt for AI Agents
In `@src/bun.js/bindings/wrapAnsi.cpp` around lines 352 - 371, parseSgrCode
currently only returns a single simple numeric SGR and thus misses
compound/extended sequences (e.g. "38;5;n" and "38;2;r;g;b"), and getCloseCode
only maps basic color codes so extended colors are lost across wraps; update
parseSgrCode to parse full semicolon-separated SGR parameter lists (returning
either a vector/list of uint32_t or an encoded representation) and update
getCloseCode to accept and handle extended color params (at least preserving
"38;5;n" and "38;2;r;g;b" sequences when computing closing codes), or
normalize/propagate unknown parameter sequences unchanged; modify callers that
expect a single uint32_t (e.g., places using parseSgrCode's return) to handle
the new representation so extended color styles are preserved across wrapped
lines.

Comment thread src/bun.js/bindings/wrapAnsi.cpp
Comment thread src/string/immutable/wrap_ansi.zig Outdated
Comment on lines +20 to +57
pub fn wrapAnsi(allocator: std.mem.Allocator, input: []const u8, columns: usize, options: WrapOptions) ![]u8 {
if (columns == 0) {
return allocator.dupe(u8, input);
}

// Normalize \r\n to \n
var normalized: std.ArrayListUnmanaged(u8) = .{};
defer normalized.deinit(allocator);

var i: usize = 0;
while (i < input.len) {
if (i + 1 < input.len and input[i] == '\r' and input[i + 1] == '\n') {
try normalized.append(allocator, '\n');
i += 2;
} else {
try normalized.append(allocator, input[i]);
i += 1;
}
}

var result: std.ArrayListUnmanaged(u8) = .{};
errdefer result.deinit(allocator);

// Split by newlines and process each line
var lines = std.mem.splitScalar(u8, normalized.items, '\n');
var first_line = true;

while (lines.next()) |line| {
if (!first_line) {
try result.append(allocator, '\n');
}
first_line = false;

try execLine(allocator, line, columns, options, &result);
}

return result.toOwnedSlice(allocator);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Avoid unconditional allocation/copy for CRLF normalization.

wrapAnsi() currently copies all input into normalized even when there’s no \r\n.

Proposed diff
 pub fn wrapAnsi(allocator: std.mem.Allocator, input: []const u8, columns: usize, options: WrapOptions) ![]u8 {
     if (columns == 0) {
         return allocator.dupe(u8, input);
     }

-    // Normalize \r\n to \n
-    var normalized: std.ArrayListUnmanaged(u8) = .{};
-    defer normalized.deinit(allocator);
-
-    var i: usize = 0;
-    while (i < input.len) {
-        if (i + 1 < input.len and input[i] == '\r' and input[i + 1] == '\n') {
-            try normalized.append(allocator, '\n');
-            i += 2;
-        } else {
-            try normalized.append(allocator, input[i]);
-            i += 1;
-        }
-    }
+    // Normalize \r\n to \n (only allocate if needed)
+    const needs_normalize = std.mem.indexOf(u8, input, "\r\n") != null;
+    var normalized: std.ArrayListUnmanaged(u8) = .{};
+    defer normalized.deinit(allocator);
+    const normalized_slice: []const u8 = blk: {
+        if (!needs_normalize) break :blk input;
+        var i: usize = 0;
+        while (i < input.len) {
+            if (i + 1 < input.len and input[i] == '\r' and input[i + 1] == '\n') {
+                try normalized.append(allocator, '\n');
+                i += 2;
+            } else {
+                try normalized.append(allocator, input[i]);
+                i += 1;
+            }
+        }
+        break :blk normalized.items;
+    };

     var result: std.ArrayListUnmanaged(u8) = .{};
     errdefer result.deinit(allocator);

     // Split by newlines and process each line
-    var lines = std.mem.splitScalar(u8, normalized.items, '\n');
+    var lines = std.mem.splitScalar(u8, normalized_slice, '\n');

Comment thread src/string/immutable/wrap_ansi.zig Outdated
Comment on lines +219 to +286
/// Wrap a word across multiple rows (character by character)
fn wrapWord(allocator: std.mem.Allocator, rows: *std.ArrayListUnmanaged(std.ArrayListUnmanaged(u8)), word: []const u8, columns: usize, options: WrapOptions) !void {
var is_inside_escape = false;
var is_inside_link_escape = false;

var vis = rowWidth(&rows.items[rows.items.len - 1], options.ambiguous_is_narrow);

var i: usize = 0;
while (i < word.len) {
var char_len: usize = 1;

if (word[i] == '\x1b') {
is_inside_escape = true;
// Check if it's a hyperlink escape
if (i + 4 < word.len and std.mem.startsWith(u8, word[i + 1 ..], "]8;;")) {
is_inside_link_escape = true;
}
}

const char_width = if (!is_inside_escape) blk: {
const result = getCharWidth(word[i..], !options.ambiguous_is_narrow);
char_len = result.bytes;
break :blk result.width;
} else 0;

if (!is_inside_escape and vis + char_width <= columns) {
try rows.items[rows.items.len - 1].appendSlice(allocator, word[i .. i + char_len]);
} else if (!is_inside_escape) {
try rows.append(allocator, .{});
try rows.items[rows.items.len - 1].appendSlice(allocator, word[i .. i + char_len]);
vis = 0;
} else {
// Inside escape, just append
try rows.items[rows.items.len - 1].append(allocator, word[i]);
}

if (is_inside_escape) {
if (is_inside_link_escape) {
if (word[i] == 0x07) { // BEL
is_inside_escape = false;
is_inside_link_escape = false;
}
} else if (word[i] == 'm') {
is_inside_escape = false;
}
i += 1;
continue;
}

vis += char_width;

if (vis == columns and i + char_len < word.len) {
try rows.append(allocator, .{});
vis = 0;
}

i += char_len;
}

// Handle edge case: last row is only ANSI escape codes
if (vis == 0 and rows.items[rows.items.len - 1].items.len > 0 and rows.items.len > 1) {
if (rows.pop()) |popped| {
var last_row = popped;
try rows.items[rows.items.len - 1].appendSlice(allocator, last_row.items);
last_row.deinit(allocator);
}
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

wc -l src/string/immutable/wrap_ansi.zig

Repository: oven-sh/bun

Length of output: 93


🏁 Script executed:

cat -n src/string/immutable/wrap_ansi.zig | head -100

Repository: oven-sh/bun

Length of output: 3971


🏁 Script executed:

# Find the functions mentioned in the review
rg -n "fn parseSgrCode|fn joinRowsWithAnsiPreservation|fn parseOsc8Url|fn stringWidth" src/string/immutable/wrap_ansi.zig

Repository: oven-sh/bun

Length of output: 395


🏁 Script executed:

sed -n '480,501p' src/string/immutable/wrap_ansi.zig

Repository: oven-sh/bun

Length of output: 538


🏁 Script executed:

sed -n '502,550p' src/string/immutable/wrap_ansi.zig

Repository: oven-sh/bun

Length of output: 1234


🏁 Script executed:

sed -n '366,403p' src/string/immutable/wrap_ansi.zig

Repository: oven-sh/bun

Length of output: 1153


🏁 Script executed:

sed -n '404,479p' src/string/immutable/wrap_ansi.zig

Repository: oven-sh/bun

Length of output: 2841


Multiple ANSI/OSC8 parsing issues limit real-world compatibility and can break wrap + style restoration.

The codebase has three interconnected problems:

  1. parseSgrCode() cannot parse parametric SGR codes: It breaks on the first non-digit character (line 494), so 38;2;255;0;0m (24-bit color) captures only 38, losing full color specification.

  2. Single escape_code state drops concurrent styles: joinRowsWithAnsiPreservation() tracks only one escape_code variable (line 418). When bold (1) and color (31) are both active, only the last applied code is stored. At line breaks, the earlier style is lost—only the final code is restored.

  3. OSC8 terminator handling is inconsistent and incomplete:

    • parseOsc8Url() stops at ESC (\x1b, line 508) but ST is ESC \ (two bytes), making the check incomplete
    • stringWidth() and wrapWord() check only BEL (0x07, lines 377 and 248) for OSC8 termination, missing ST entirely
    • This inconsistency means OSC8 sequences terminated by ST (the ANSI standard) may not close properly, affecting width calculation and wrapping

Recommend:

  1. Extend parseSgrCode() to track all semicolon-separated parameters, or restructure to store full SGR state
  2. Track all active SGR codes (not just the last one) in joinRowsWithAnsiPreservation() to replay full style context after line breaks
  3. Consistently handle both BEL and proper ST (\x1b\) across wrapWord(), stringWidth(), and parseOsc8Url()
🤖 Prompt for AI Agents
In `@src/string/immutable/wrap_ansi.zig` around lines 219 - 286, parseSgrCode,
joinRowsWithAnsiPreservation, parseOsc8Url, stringWidth, and wrapWord have three
related bugs: SGR parsing stops at first non-digit, only a single escape_code is
tracked losing concurrent styles, and OSC8 termination only checks BEL not the
ST sequence. Fix parseSgrCode to collect the full semicolon-separated parameter
list (e.g., parse until the final 'm' and return all params/bytes, not just the
first numeric token), change joinRowsWithAnsiPreservation to track active SGR
state as a collection (stack/set/list of codes) and replay the entire
active-code sequence when restoring styles after a line break, and make OSC8
handling consistent by recognizing both BEL (0x07) and the ST terminator (ESC
followed by '\\') in parseOsc8Url, stringWidth, and wrapWord so escapes
terminate correctly and width/wrapping and style restoration work for both BEL-
and ST-terminated OSC8 sequences.

Address review comment: Replace std::vector with WTF::Vector throughout
the wrapAnsi implementation for consistency with WebKit code style.

API changes applied:
- empty() -> isEmpty()
- push_back() -> append()
- data() -> span().data()
- reserve() -> reserveCapacity()
- back() -> last()
- pop_back() -> removeLast()
- resize() -> shrink()
- insert(end(), begin, end) -> appendVector() / append(span{})

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 3
Claude-Permission-Prompts: 2
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# wrapAnsi関数の実装計画

## 概要
BunにNPMの`wrap-ansi`ライブラリと互換性のある`Bun.wrapAnsi()`関数を追加する。

## APIシグネチャ
```typescript
Bun.wrapAnsi(string: string, columns: number, options?: WrapAnsiOptions): string

interface WrapAnsiOptions {
  hard?: boolean;            // default: false - 長い単語を強制分割
  wordWrap?: boolean;        // default: true - 単語境界で折り返し
  trim?: boolean;            // default: true - 行の先頭・末尾の空白を削除
  ambiguousIsNarrow?: boolean; // default: true - 曖昧幅文字をnarrow(幅1)として扱う
}
```

## 実装アプローチ
`stringWidth`と同様にZig + bindgenシステムを使用。既存のANSI/文字幅計算コードを再利用。

## 変更するファイル

### 1. バインディング定義
**ファイル**: `src/bun.js/api/BunObject.bind.ts`

```typescript
export const WrapAnsiOptions = t.dictionary({
  hard: t.boolean.default(false),
  wordWrap: t.boolean.default(true),
  trim: t.boolean.default(true),
  ambiguousIsNarrow: t.boolean.default(true),
});

export const wrapAnsi = fn({
  args: {
    global: t.globalObject,
    str: t.DOMString.default(""),
    columns: t.usize,
    opts: WrapAnsiOptions.default({}),
  },
  ret: t.any, // JSValue (String)
});
```

### 2. Zig実装
**ファイル**: `src/bun.js/api/BunObject.zig`

エントリーポイント関数を追加。コア実装を呼び出す。

### 3. コア実装(新規)
**ファイル**: `src/string/immutable/wrap_ansi.zig`

主要な処理:
1. 改行で分割し、各行を処理
2. スペースで単語に分割し、表示幅を計算(ANSI除外)
3. 列幅に基づいて折り返し
4. ANSIエスケープコードの追跡(SGR、OSC 8ハイパーリンク)
5. 行跨ぎでスタイルを閉じて再開

既存コードの再利用:
- `src/string/immutable/visible.zig` - 表示幅計算
- ANSI CSI/OSCパースロジック

### 4. テスト
**ファイル**: `test/js/bun/util/wrapAnsi.test.ts`

NPMライブラリとの比較テスト:
- 基本的な折り返し
- ANSIカラー付き文字列
- hard/wordWrap/trimオプション
- 全角文字、絵文字、サロゲートペア
- ハイパーリンク

### 5. 型定義
**ファイル**: `packages/bun-types/bun.d.ts`

TypeScript型定義を追加。

## 実装ステップ

### Step 1: bindgen定義追加
`BunObject.bind.ts`にオプションと関数シグネチャを追加。

### Step 2: コア実装
`wrap_ansi.zig`に以下を実装:
- `wrapAnsi()` - メイン関数
- `wrapLine()` - 1行の処理
- `wrapWord()` - 長い単語の分割(hardモード)
- ANSIスタイル追跡構造体
- SGRコードの開始→終了マッピング

### Step 3: BunObjectへの統合
`BunObject.zig`にエントリーポイント関数を追加。

### Step 4: テスト作成
NPMとの互換性テストを作成。

### Step 5: 型定義追加
`bun.d.ts`にインターフェースを追加。

## SGRコードマッピング
行跨ぎでスタイルを復元するために必要:

| 開始コード | 終了コード | 説明 |
|-----------|-----------|------|
| 1 | 22 | bold |
| 2 | 22 | dim |
| 3 | 23 | italic |
| 4 | 24 | underline |
| 7 | 27 | inverse |
| 8 | 28 | hidden |
| 9 | 29 | strikethrough |
| 30-37 | 39 | foreground color |
| 40-47 | 49 | background color |
| 90-97 | 39 | bright foreground |
| 100-107 | 49 | bright background |

## 検証方法

```bash
# テスト実行
bun bd test test/js/bun/util/wrapAnsi.test.ts

# システムBunでは失敗することを確認
USE_SYSTEM_BUN=1 bun test test/js/bun/util/wrapAnsi.test.ts

# 手動テスト
bun bd -e "console.log(Bun.wrapAnsi('hello world', 5))"
```

## 注意事項
- 全角文字は幅2としてカウント
- ANSIエスケープは幅0
- Unicode正規化(normalize())を適用
- `\r\n`を`\n`に正規化
</claude-plan>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@src/bun.js/bindings/wrapAnsi.cpp`:
- Around line 232-243: The code in wrapWord calls decodeUTF8 when
sizeof(Char)==1, which mis-decodes Latin1 bytes; implement a decodeLatin1(const
Latin1Character*, size_t, size_t&) that returns the byte value as a Unicode
codepoint and sets outLen=1, then change the branch in wrapWord to call
decodeLatin1 when Char is Latin1Character (otherwise keep decodeUTF8),
preserving charLen and passing the resulting codepoint to getVisibleWidth
(respecting options.ambiguousIsNarrow); ensure decodeLatin1 is used only for the
Latin1Character type and does not rely on available length.
♻️ Duplicate comments (5)
src/bun.js/bindings/wrapAnsi.cpp (5)

35-68: Missing continuation byte validation in UTF-8 decoding.

This issue was flagged previously and remains unaddressed. The decoding assembles multi-byte sequences without validating that continuation bytes have the correct 10xxxxxx pattern. Malformed UTF-8 input could produce incorrect codepoints instead of the replacement character.


150-204: Incomplete OSC sequence terminator handling in trimLeadingSpaces.

This was flagged previously. The escape sequence detection at lines 163-164 only handles BEL (0x07) and m terminators, but OSC sequences can also be terminated by ST (ESC \ or 0x9C). This affects multiple functions in the file that use the same pattern.


352-373: SGR parsing limitation: extended color codes not supported.

This limitation was noted in previous reviews. parseSgrCode only parses simple numeric codes and doesn't support compound SGR sequences like 38;5;n (256-color) or 38;2;r;g;b (true color). Extended color styles won't be properly preserved across wrapped lines.

This may be acceptable for the initial implementation given the complexity involved.


703-708: Handle negative column values.

This was flagged previously and remains unaddressed. toIntegerOrInfinity can return negative values or infinity, which when cast to size_t causes unsigned wraparound or undefined behavior.

Suggested fix
     // Get columns
     size_t columns = 0;
     if (!columnsValue.isUndefined()) {
-        columns = static_cast<size_t>(columnsValue.toIntegerOrInfinity(globalObject));
+        double colsDouble = columnsValue.toIntegerOrInfinity(globalObject);
         RETURN_IF_EXCEPTION(scope, {});
+        if (colsDouble > 0 && std::isfinite(colsDouble))
+            columns = static_cast<size_t>(colsDouble);
     }

710-735: Consider caching property identifiers for options parsing.

This was noted in previous reviews. Creating Identifier::fromString for each option lookup on every call is slightly inefficient. For a frequently-called API, consider using cached identifiers.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 96c3913 and af2b6d0.

📒 Files selected for processing (1)
  • src/bun.js/bindings/wrapAnsi.cpp
🧰 Additional context used
📓 Path-based instructions (1)
src/bun.js/bindings/**/*.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

src/bun.js/bindings/**/*.cpp: Create classes in three parts in C++ when there is a public constructor: Foo (JSDestructibleObject), FooPrototype (JSNonFinalObject), and FooConstructor (InternalFunction)
Define properties using HashTableValue arrays in C++ JavaScript class bindings
Add iso subspaces for C++ classes with fields in JavaScript class bindings
Cache structures in ZigGlobalObject for JavaScript class bindings

Files:

  • src/bun.js/bindings/wrapAnsi.cpp
🧠 Learnings (23)
📓 Common learnings
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24212
File: src/cli/publish_command.zig:782-788
Timestamp: 2025-10-30T21:52:04.707Z
Learning: In the Bun codebase (oven-sh/bun), `enable_ansi_colors` flags are used to gate both ANSI color codes and Unicode box-drawing characters/emoji. This is the established pattern across the codebase.
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Add iso subspaces for C++ classes with fields in JavaScript class bindings

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.h : Add BUN_EXPORT visibility attribute to all public V8 API functions to ensure proper symbol export across platforms

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:37:47.899Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/AGENTS.md:0-0
Timestamp: 2025-11-24T18:37:47.899Z
Learning: Applies to src/bun.js/bindings/v8/**/<UNKNOWN> : <UNKNOWN>

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/napi/napi.zig : For each new V8 C++ method, add both GCC/Clang and MSVC mangled symbol names to the V8API struct in src/napi/napi.zig using extern fn declarations

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Create classes in three parts in C++ when there is a public constructor: Foo (JSDestructibleObject), FooPrototype (JSNonFinalObject), and FooConstructor (InternalFunction)

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Create V8 class implementations with .cpp extension following the pattern V8ClassName.cpp that include the header, v8_compatibility_assertions.h, use ASSERT_V8_TYPE_LAYOUT_MATCHES macro, and implement methods using isolate->currentHandleScope()->createLocal<T>() for handle creation

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-09-20T05:35:57.318Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 22534
File: src/bun.js/bindings/headers.h:729-731
Timestamp: 2025-09-20T05:35:57.318Z
Learning: symbols.txt in the Bun codebase is specifically for V8 API mangled symbols (without leading underscore), not for general Bun host functions declared with BUN_DECLARE_HOST_FUNCTION. Host functions are handled through different build mechanisms.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-10-01T21:59:54.571Z
Learnt from: taylordotfish
Repo: oven-sh/bun PR: 23169
File: src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h:47-74
Timestamp: 2025-10-01T21:59:54.571Z
Learning: In the new bindings generator (bindgenv2) for `src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h`, the context-aware enumeration conversion overloads intentionally use stricter validation (requiring `value.isString()` without ToString coercion), diverging from Web IDL semantics. This is a design decision documented in comments.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.h : Create V8 class headers with .h extension following the pattern V8ClassName.h that include pragma once, v8.h, V8Local.h, V8Isolate.h, and declare classes extending from Data with BUN_EXPORT static methods

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.dyn : Add symbol names with leading underscore and semicolons in braces to src/symbols.dyn for each new V8 API method

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-10-24T10:43:09.398Z
Learnt from: fmguerreiro
Repo: oven-sh/bun PR: 23774
File: src/install/PackageManager/updatePackageJSONAndInstall.zig:548-548
Timestamp: 2025-10-24T10:43:09.398Z
Learning: In Bun's Zig codebase, the `as(usize, intCast(...))` cast pattern triggers a Zig compiler bug that causes compilation to hang indefinitely when used in complex control flow contexts (loops + short-circuit operators + optional unwrapping). Avoid this pattern and use simpler alternatives like just `intCast(...)` if type casting is necessary.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-10T00:57:09.173Z
Learnt from: franciscop
Repo: oven-sh/bun PR: 24514
File: src/bun.js/api/crypto/PasswordObject.zig:86-101
Timestamp: 2025-11-10T00:57:09.173Z
Learning: In Bun's Zig codebase (PasswordObject.zig), when validating the parallelism parameter for Argon2, the upper limit is set to 65535 (2^16 - 1) rather than using `std.math.maxInt(u24)` because the latter triggers Zig's truncation limit checks. The value 65535 is a practical upper bound that avoids compiler issues while being sufficient for thread parallelism use cases.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-12-11T02:11:47.024Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 25462
File: src/ast/visitExpr.zig:1644-1695
Timestamp: 2025-12-11T02:11:47.024Z
Learning: In Bun's bundler feature flag implementation (src/ast/visitExpr.zig), the validation for feature() flag names intentionally only rejects UTF-16 strings (checking `is_utf16`) while allowing UTF-8 strings, even though the error message says "must be an ASCII string". This is the intended behavior and should not be changed to enforce strict ASCII validation.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-10-30T21:52:04.707Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24212
File: src/cli/publish_command.zig:782-788
Timestamp: 2025-10-30T21:52:04.707Z
Learning: In the Bun codebase (oven-sh/bun), `enable_ansi_colors` flags are used to gate both ANSI color codes and Unicode box-drawing characters/emoji. This is the established pattern across the codebase.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-12-23T06:50:41.142Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 25429
File: src/bun.js/bindings/helpers.h:422-422
Timestamp: 2025-12-23T06:50:41.142Z
Learning: In Bun's C++ bindings (src/bun.js/bindings/helpers.h and similar files), when returning an empty JSC::Identifier and a VM is accessible, prefer using `vm.propertyNames->emptyIdentifier` over constructing with `JSC::Identifier(JSC::Identifier::EmptyIdentifierFlag::EmptyIdentifier)`. The cached identifier from the VM's property names table is more efficient and consistent with WebKit upgrade patterns.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-12-16T00:21:32.179Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2025-12-16T00:21:32.179Z
Learning: Applies to src/bun.js/bindings/**/*.cpp : Cache structures in ZigGlobalObject for JavaScript class bindings

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-03T20:43:06.996Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24273
File: src/bun.js/test/snapshot.zig:19-19
Timestamp: 2025-11-03T20:43:06.996Z
Learning: In Bun's Zig codebase, when storing JSValue objects in collections like ArrayList, use `jsc.Strong.Optional` (not raw JSValue). When adding values, wrap them with `jsc.Strong.Optional.create(value, globalThis)`. In cleanup code, iterate the collection calling `.deinit()` on each Strong.Optional item before calling `.deinit()` on the ArrayList itself. This pattern automatically handles GC protection. See examples in src/bun.js/test/ScopeFunctions.zig and src/bun.js/node/node_cluster_binding.zig.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:37:11.466Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/js/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:37:11.466Z
Learning: Applies to src/js/{builtins,node,bun,thirdparty,internal}/**/*.{ts,js} : Use JSC intrinsics (prefixed with `$`) such as `$Array.from()`, `$isCallable()`, and `$newArrayWithSize()` for performance-critical operations

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Use JSC::WriteBarrier for heap-allocated references in V8 objects and implement visitChildren() for custom heap objects to support garbage collection

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-09-07T08:20:47.215Z
Learnt from: RiskyMH
Repo: oven-sh/bun PR: 22258
File: src/cli/test_command.zig:1258-1281
Timestamp: 2025-09-07T08:20:47.215Z
Learning: For Bun's test line filtering feature, the parseFileLineArg function should only handle the specific cases of "file:line" and "file:line:col" formats. It should not try to be overly tolerant of other patterns, as components like ":col" or other non-numeric segments could legitimately be part of filenames. The current conservative approach that checks for numeric segments in expected positions is appropriate.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-09-12T18:16:50.754Z
Learnt from: RiskyMH
Repo: oven-sh/bun PR: 22606
File: src/glob/GlobWalker.zig:449-452
Timestamp: 2025-09-12T18:16:50.754Z
Learning: For Bun codebase: prefer using `std.fs.path.sep` over manual platform separator detection, and use `bun.strings.lastIndexOfChar` instead of `std.mem.lastIndexOfScalar` for string operations.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Use localToJSValue() to convert V8 handles to JSC values and perform JSC operations within V8 method implementations

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
🧬 Code graph analysis (1)
src/bun.js/bindings/wrapAnsi.cpp (1)
src/bun.js/api/BunObject.bind.ts (1)
  • stringWidth (30-36)
🪛 Cppcheck (2.19.0)
src/bun.js/bindings/wrapAnsi.cpp

[information] 42-42: Include file

(missingInclude)


[information] 52-52: Include file

(missingIncludeSystem)


[information] 59-59: Include file

(missingIncludeSystem)


[error] 66-66: failed to evaluate #if condition, undefined function-like macro invocation

(syntaxError)

🔇 Additional comments (7)
src/bun.js/bindings/wrapAnsi.cpp (7)

1-14: LGTM - Includes and extern declarations are well-organized.

The header includes and extern declarations for the Zig visible-width functions are appropriate.


70-90: LGTM - UTF-16 decoding correctly handles surrogate pairs.

The surrogate pair detection and codepoint calculation are correct.


286-346: LGTM - Trailing space trimming with ANSI preservation.

The logic correctly identifies the last visible content and preserves ANSI codes. The same OSC terminator limitation mentioned earlier applies here as well.


429-505: LGTM - ANSI style preservation across line breaks.

The logic correctly closes SGR styles and hyperlinks before newlines and reopens them after, ensuring terminal compatibility. The order of operations is correct.


511-615: LGTM - Line processing logic handles various wrap modes correctly.

The implementation properly handles trim, hard wrap, and word wrap modes with appropriate row management.


621-677: LGTM - Main implementation with proper normalization and line processing.

The CRLF normalization and per-line processing with ANSI preservation are implemented correctly. Uses WTF::Vector as recommended.


739-743: Encoding dispatch needs to account for Latin1 vs UTF-8.

When view->is8Bit() is true, the data is Latin1-encoded. The current implementation routes this through wrapAnsiImpl<Latin1Character>, but internally calls UTF-8 decoding/width functions. Ensure the Latin1 encoding issues noted earlier are addressed before this dispatch is correct.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment thread src/bun.js/bindings/wrapAnsi.cpp
Comment thread src/bun.js/bindings/wrapAnsi.cpp
@Jarred-Sumner

Copy link
Copy Markdown
Collaborator

This shouldn't be doing UTF-8 decoding.

sosukesuzuki and others added 3 commits January 15, 2026 12:04
JSC 8-bit strings are Latin1-encoded, not UTF-8. The code was incorrectly:
1. Calling Bun__visibleWidthExcludeANSI_utf8 for Latin1 data
2. Using UTF-8 multi-byte decoding on Latin1 characters

Fix by:
- Using Bun__visibleWidthExcludeANSI_latin1 for 8-bit strings
- Handling Latin1 inline (each byte is one char, direct 1:1 mapping to U+0000-U+00FF)
- Removing unused UTF-8 decoding utilities

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 0
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# wrapAnsi関数の実装計画

## 概要
BunにNPMの`wrap-ansi`ライブラリと互換性のある`Bun.wrapAnsi()`関数を追加する。

## APIシグネチャ
```typescript
Bun.wrapAnsi(string: string, columns: number, options?: WrapAnsiOptions): string

interface WrapAnsiOptions {
  hard?: boolean;            // default: false - 長い単語を強制分割
  wordWrap?: boolean;        // default: true - 単語境界で折り返し
  trim?: boolean;            // default: true - 行の先頭・末尾の空白を削除
  ambiguousIsNarrow?: boolean; // default: true - 曖昧幅文字をnarrow(幅1)として扱う
}
```

## 実装アプローチ
`stringWidth`と同様にZig + bindgenシステムを使用。既存のANSI/文字幅計算コードを再利用。

## 変更するファイル

### 1. バインディング定義
**ファイル**: `src/bun.js/api/BunObject.bind.ts`

```typescript
export const WrapAnsiOptions = t.dictionary({
  hard: t.boolean.default(false),
  wordWrap: t.boolean.default(true),
  trim: t.boolean.default(true),
  ambiguousIsNarrow: t.boolean.default(true),
});

export const wrapAnsi = fn({
  args: {
    global: t.globalObject,
    str: t.DOMString.default(""),
    columns: t.usize,
    opts: WrapAnsiOptions.default({}),
  },
  ret: t.any, // JSValue (String)
});
```

### 2. Zig実装
**ファイル**: `src/bun.js/api/BunObject.zig`

エントリーポイント関数を追加。コア実装を呼び出す。

### 3. コア実装(新規)
**ファイル**: `src/string/immutable/wrap_ansi.zig`

主要な処理:
1. 改行で分割し、各行を処理
2. スペースで単語に分割し、表示幅を計算(ANSI除外)
3. 列幅に基づいて折り返し
4. ANSIエスケープコードの追跡(SGR、OSC 8ハイパーリンク)
5. 行跨ぎでスタイルを閉じて再開

既存コードの再利用:
- `src/string/immutable/visible.zig` - 表示幅計算
- ANSI CSI/OSCパースロジック

### 4. テスト
**ファイル**: `test/js/bun/util/wrapAnsi.test.ts`

NPMライブラリとの比較テスト:
- 基本的な折り返し
- ANSIカラー付き文字列
- hard/wordWrap/trimオプション
- 全角文字、絵文字、サロゲートペア
- ハイパーリンク

### 5. 型定義
**ファイル**: `packages/bun-types/bun.d.ts`

TypeScript型定義を追加。

## 実装ステップ

### Step 1: bindgen定義追加
`BunObject.bind.ts`にオプションと関数シグネチャを追加。

### Step 2: コア実装
`wrap_ansi.zig`に以下を実装:
- `wrapAnsi()` - メイン関数
- `wrapLine()` - 1行の処理
- `wrapWord()` - 長い単語の分割(hardモード)
- ANSIスタイル追跡構造体
- SGRコードの開始→終了マッピング

### Step 3: BunObjectへの統合
`BunObject.zig`にエントリーポイント関数を追加。

### Step 4: テスト作成
NPMとの互換性テストを作成。

### Step 5: 型定義追加
`bun.d.ts`にインターフェースを追加。

## SGRコードマッピング
行跨ぎでスタイルを復元するために必要:

| 開始コード | 終了コード | 説明 |
|-----------|-----------|------|
| 1 | 22 | bold |
| 2 | 22 | dim |
| 3 | 23 | italic |
| 4 | 24 | underline |
| 7 | 27 | inverse |
| 8 | 28 | hidden |
| 9 | 29 | strikethrough |
| 30-37 | 39 | foreground color |
| 40-47 | 49 | background color |
| 90-97 | 39 | bright foreground |
| 100-107 | 49 | bright background |

## 検証方法

```bash
# テスト実行
bun bd test test/js/bun/util/wrapAnsi.test.ts

# システムBunでは失敗することを確認
USE_SYSTEM_BUN=1 bun test test/js/bun/util/wrapAnsi.test.ts

# 手動テスト
bun bd -e "console.log(Bun.wrapAnsi('hello world', 5))"
```

## 注意事項
- 全角文字は幅2としてカウント
- ANSIエスケープは幅0
- Unicode正規化(normalize())を適用
- `\r\n`を`\n`に正規化
</claude-plan>
@sosukesuzuki

Copy link
Copy Markdown
Member Author

ah sorry tests were not successful

CSI sequences (like cursor movement ESC[1D) end with bytes in 0x40-0x7E
range, not just 'm'. Added proper detection for all CSI terminators
in both wrapWord and trimRowTrailingSpaces functions.

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 1
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# wrapAnsi関数の実装計画

## 概要
BunにNPMの`wrap-ansi`ライブラリと互換性のある`Bun.wrapAnsi()`関数を追加する。

## APIシグネチャ
```typescript
Bun.wrapAnsi(string: string, columns: number, options?: WrapAnsiOptions): string

interface WrapAnsiOptions {
  hard?: boolean;            // default: false - 長い単語を強制分割
  wordWrap?: boolean;        // default: true - 単語境界で折り返し
  trim?: boolean;            // default: true - 行の先頭・末尾の空白を削除
  ambiguousIsNarrow?: boolean; // default: true - 曖昧幅文字をnarrow(幅1)として扱う
}
```

## 実装アプローチ
`stringWidth`と同様にZig + bindgenシステムを使用。既存のANSI/文字幅計算コードを再利用。

## 変更するファイル

### 1. バインディング定義
**ファイル**: `src/bun.js/api/BunObject.bind.ts`

```typescript
export const WrapAnsiOptions = t.dictionary({
  hard: t.boolean.default(false),
  wordWrap: t.boolean.default(true),
  trim: t.boolean.default(true),
  ambiguousIsNarrow: t.boolean.default(true),
});

export const wrapAnsi = fn({
  args: {
    global: t.globalObject,
    str: t.DOMString.default(""),
    columns: t.usize,
    opts: WrapAnsiOptions.default({}),
  },
  ret: t.any, // JSValue (String)
});
```

### 2. Zig実装
**ファイル**: `src/bun.js/api/BunObject.zig`

エントリーポイント関数を追加。コア実装を呼び出す。

### 3. コア実装(新規)
**ファイル**: `src/string/immutable/wrap_ansi.zig`

主要な処理:
1. 改行で分割し、各行を処理
2. スペースで単語に分割し、表示幅を計算(ANSI除外)
3. 列幅に基づいて折り返し
4. ANSIエスケープコードの追跡(SGR、OSC 8ハイパーリンク)
5. 行跨ぎでスタイルを閉じて再開

既存コードの再利用:
- `src/string/immutable/visible.zig` - 表示幅計算
- ANSI CSI/OSCパースロジック

### 4. テスト
**ファイル**: `test/js/bun/util/wrapAnsi.test.ts`

NPMライブラリとの比較テスト:
- 基本的な折り返し
- ANSIカラー付き文字列
- hard/wordWrap/trimオプション
- 全角文字、絵文字、サロゲートペア
- ハイパーリンク

### 5. 型定義
**ファイル**: `packages/bun-types/bun.d.ts`

TypeScript型定義を追加。

## 実装ステップ

### Step 1: bindgen定義追加
`BunObject.bind.ts`にオプションと関数シグネチャを追加。

### Step 2: コア実装
`wrap_ansi.zig`に以下を実装:
- `wrapAnsi()` - メイン関数
- `wrapLine()` - 1行の処理
- `wrapWord()` - 長い単語の分割(hardモード)
- ANSIスタイル追跡構造体
- SGRコードの開始→終了マッピング

### Step 3: BunObjectへの統合
`BunObject.zig`にエントリーポイント関数を追加。

### Step 4: テスト作成
NPMとの互換性テストを作成。

### Step 5: 型定義追加
`bun.d.ts`にインターフェースを追加。

## SGRコードマッピング
行跨ぎでスタイルを復元するために必要:

| 開始コード | 終了コード | 説明 |
|-----------|-----------|------|
| 1 | 22 | bold |
| 2 | 22 | dim |
| 3 | 23 | italic |
| 4 | 24 | underline |
| 7 | 27 | inverse |
| 8 | 28 | hidden |
| 9 | 29 | strikethrough |
| 30-37 | 39 | foreground color |
| 40-47 | 49 | background color |
| 90-97 | 39 | bright foreground |
| 100-107 | 49 | bright background |

## 検証方法

```bash
# テスト実行
bun bd test test/js/bun/util/wrapAnsi.test.ts

# システムBunでは失敗することを確認
USE_SYSTEM_BUN=1 bun test test/js/bun/util/wrapAnsi.test.ts

# 手動テスト
bun bd -e "console.log(Bun.wrapAnsi('hello world', 5))"
```

## 注意事項
- 全角文字は幅2としてカウント
- ANSIエスケープは幅0
- Unicode正規化(normalize())を適用
- `\r\n`を`\n`に正規化
</claude-plan>
Vector<Char> normalized;
normalized.reserveCapacity(input.size());

for (size_t i = 0; i < input.size(); ++i) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

this should really use WTF::find to find these newlines

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This should also use WTF::find but could be done as a follow-up.

Comment thread src/string/immutable/wrap_ansi.zig Outdated
@@ -0,0 +1,606 @@
/// wrap-ansi compatible text wrapping with ANSI escape code preservation.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this file still used?

@Jarred-Sumner Jarred-Sumner left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

  • I think probably can delete src/string/immutable/wrap_ansi.zig
  • The CodeRabbit comments about SGR parsing and width tracking, and integer validation looks real

The C++ implementation is now used instead.
Negative values and Infinity would cause incorrect behavior due to
unsigned integer wraparound. Now they are treated as 0 (return input
unchanged).

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 2
Claude-Permission-Prompts: 1
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# wrapAnsi関数の実装計画

## 概要
BunにNPMの`wrap-ansi`ライブラリと互換性のある`Bun.wrapAnsi()`関数を追加する。

## APIシグネチャ
```typescript
Bun.wrapAnsi(string: string, columns: number, options?: WrapAnsiOptions): string

interface WrapAnsiOptions {
  hard?: boolean;            // default: false - 長い単語を強制分割
  wordWrap?: boolean;        // default: true - 単語境界で折り返し
  trim?: boolean;            // default: true - 行の先頭・末尾の空白を削除
  ambiguousIsNarrow?: boolean; // default: true - 曖昧幅文字をnarrow(幅1)として扱う
}
```

## 実装アプローチ
`stringWidth`と同様にZig + bindgenシステムを使用。既存のANSI/文字幅計算コードを再利用。

## 変更するファイル

### 1. バインディング定義
**ファイル**: `src/bun.js/api/BunObject.bind.ts`

```typescript
export const WrapAnsiOptions = t.dictionary({
  hard: t.boolean.default(false),
  wordWrap: t.boolean.default(true),
  trim: t.boolean.default(true),
  ambiguousIsNarrow: t.boolean.default(true),
});

export const wrapAnsi = fn({
  args: {
    global: t.globalObject,
    str: t.DOMString.default(""),
    columns: t.usize,
    opts: WrapAnsiOptions.default({}),
  },
  ret: t.any, // JSValue (String)
});
```

### 2. Zig実装
**ファイル**: `src/bun.js/api/BunObject.zig`

エントリーポイント関数を追加。コア実装を呼び出す。

### 3. コア実装(新規)
**ファイル**: `src/string/immutable/wrap_ansi.zig`

主要な処理:
1. 改行で分割し、各行を処理
2. スペースで単語に分割し、表示幅を計算(ANSI除外)
3. 列幅に基づいて折り返し
4. ANSIエスケープコードの追跡(SGR、OSC 8ハイパーリンク)
5. 行跨ぎでスタイルを閉じて再開

既存コードの再利用:
- `src/string/immutable/visible.zig` - 表示幅計算
- ANSI CSI/OSCパースロジック

### 4. テスト
**ファイル**: `test/js/bun/util/wrapAnsi.test.ts`

NPMライブラリとの比較テスト:
- 基本的な折り返し
- ANSIカラー付き文字列
- hard/wordWrap/trimオプション
- 全角文字、絵文字、サロゲートペア
- ハイパーリンク

### 5. 型定義
**ファイル**: `packages/bun-types/bun.d.ts`

TypeScript型定義を追加。

## 実装ステップ

### Step 1: bindgen定義追加
`BunObject.bind.ts`にオプションと関数シグネチャを追加。

### Step 2: コア実装
`wrap_ansi.zig`に以下を実装:
- `wrapAnsi()` - メイン関数
- `wrapLine()` - 1行の処理
- `wrapWord()` - 長い単語の分割(hardモード)
- ANSIスタイル追跡構造体
- SGRコードの開始→終了マッピング

### Step 3: BunObjectへの統合
`BunObject.zig`にエントリーポイント関数を追加。

### Step 4: テスト作成
NPMとの互換性テストを作成。

### Step 5: 型定義追加
`bun.d.ts`にインターフェースを追加。

## SGRコードマッピング
行跨ぎでスタイルを復元するために必要:

| 開始コード | 終了コード | 説明 |
|-----------|-----------|------|
| 1 | 22 | bold |
| 2 | 22 | dim |
| 3 | 23 | italic |
| 4 | 24 | underline |
| 7 | 27 | inverse |
| 8 | 28 | hidden |
| 9 | 29 | strikethrough |
| 30-37 | 39 | foreground color |
| 40-47 | 49 | background color |
| 90-97 | 39 | bright foreground |
| 100-107 | 49 | bright background |

## 検証方法

```bash
# テスト実行
bun bd test test/js/bun/util/wrapAnsi.test.ts

# システムBunでは失敗することを確認
USE_SYSTEM_BUN=1 bun test test/js/bun/util/wrapAnsi.test.ts

# 手動テスト
bun bd -e "console.log(Bun.wrapAnsi('hello world', 5))"
```

## 注意事項
- 全角文字は幅2としてカウント
- ANSIエスケープは幅0
- Unicode正規化(normalize())を適用
- `\r\n`を`\n`に正規化
</claude-plan>
When a character starts a new line, the visible width counter was reset
to 0 instead of the character's width. This could cause incorrect
wrapping for subsequent characters.

Also restructured the code to avoid double-counting width by moving
the vis += charWidth into the appropriate branch.

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 0
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# wrapAnsi関数の実装計画

## 概要
BunにNPMの`wrap-ansi`ライブラリと互換性のある`Bun.wrapAnsi()`関数を追加する。

## APIシグネチャ
```typescript
Bun.wrapAnsi(string: string, columns: number, options?: WrapAnsiOptions): string

interface WrapAnsiOptions {
  hard?: boolean;            // default: false - 長い単語を強制分割
  wordWrap?: boolean;        // default: true - 単語境界で折り返し
  trim?: boolean;            // default: true - 行の先頭・末尾の空白を削除
  ambiguousIsNarrow?: boolean; // default: true - 曖昧幅文字をnarrow(幅1)として扱う
}
```

## 実装アプローチ
`stringWidth`と同様にZig + bindgenシステムを使用。既存のANSI/文字幅計算コードを再利用。

## 変更するファイル

### 1. バインディング定義
**ファイル**: `src/bun.js/api/BunObject.bind.ts`

```typescript
export const WrapAnsiOptions = t.dictionary({
  hard: t.boolean.default(false),
  wordWrap: t.boolean.default(true),
  trim: t.boolean.default(true),
  ambiguousIsNarrow: t.boolean.default(true),
});

export const wrapAnsi = fn({
  args: {
    global: t.globalObject,
    str: t.DOMString.default(""),
    columns: t.usize,
    opts: WrapAnsiOptions.default({}),
  },
  ret: t.any, // JSValue (String)
});
```

### 2. Zig実装
**ファイル**: `src/bun.js/api/BunObject.zig`

エントリーポイント関数を追加。コア実装を呼び出す。

### 3. コア実装(新規)
**ファイル**: `src/string/immutable/wrap_ansi.zig`

主要な処理:
1. 改行で分割し、各行を処理
2. スペースで単語に分割し、表示幅を計算(ANSI除外)
3. 列幅に基づいて折り返し
4. ANSIエスケープコードの追跡(SGR、OSC 8ハイパーリンク)
5. 行跨ぎでスタイルを閉じて再開

既存コードの再利用:
- `src/string/immutable/visible.zig` - 表示幅計算
- ANSI CSI/OSCパースロジック

### 4. テスト
**ファイル**: `test/js/bun/util/wrapAnsi.test.ts`

NPMライブラリとの比較テスト:
- 基本的な折り返し
- ANSIカラー付き文字列
- hard/wordWrap/trimオプション
- 全角文字、絵文字、サロゲートペア
- ハイパーリンク

### 5. 型定義
**ファイル**: `packages/bun-types/bun.d.ts`

TypeScript型定義を追加。

## 実装ステップ

### Step 1: bindgen定義追加
`BunObject.bind.ts`にオプションと関数シグネチャを追加。

### Step 2: コア実装
`wrap_ansi.zig`に以下を実装:
- `wrapAnsi()` - メイン関数
- `wrapLine()` - 1行の処理
- `wrapWord()` - 長い単語の分割(hardモード)
- ANSIスタイル追跡構造体
- SGRコードの開始→終了マッピング

### Step 3: BunObjectへの統合
`BunObject.zig`にエントリーポイント関数を追加。

### Step 4: テスト作成
NPMとの互換性テストを作成。

### Step 5: 型定義追加
`bun.d.ts`にインターフェースを追加。

## SGRコードマッピング
行跨ぎでスタイルを復元するために必要:

| 開始コード | 終了コード | 説明 |
|-----------|-----------|------|
| 1 | 22 | bold |
| 2 | 22 | dim |
| 3 | 23 | italic |
| 4 | 24 | underline |
| 7 | 27 | inverse |
| 8 | 28 | hidden |
| 9 | 29 | strikethrough |
| 30-37 | 39 | foreground color |
| 40-47 | 49 | background color |
| 90-97 | 39 | bright foreground |
| 100-107 | 49 | bright background |

## 検証方法

```bash
# テスト実行
bun bd test test/js/bun/util/wrapAnsi.test.ts

# システムBunでは失敗することを確認
USE_SYSTEM_BUN=1 bun test test/js/bun/util/wrapAnsi.test.ts

# 手動テスト
bun bd -e "console.log(Bun.wrapAnsi('hello world', 5))"
```

## 注意事項
- 全角文字は幅2としてカウント
- ANSIエスケープは幅0
- Unicode正規化(normalize())を適用
- `\r\n`を`\n`に正規化
</claude-plan>
Per review feedback, use WebKit's utility function instead of manual
iteration for better consistency with codebase patterns.

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 0
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# wrapAnsi関数の実装計画

## 概要
BunにNPMの`wrap-ansi`ライブラリと互換性のある`Bun.wrapAnsi()`関数を追加する。

## APIシグネチャ
```typescript
Bun.wrapAnsi(string: string, columns: number, options?: WrapAnsiOptions): string

interface WrapAnsiOptions {
  hard?: boolean;            // default: false - 長い単語を強制分割
  wordWrap?: boolean;        // default: true - 単語境界で折り返し
  trim?: boolean;            // default: true - 行の先頭・末尾の空白を削除
  ambiguousIsNarrow?: boolean; // default: true - 曖昧幅文字をnarrow(幅1)として扱う
}
```

## 実装アプローチ
`stringWidth`と同様にZig + bindgenシステムを使用。既存のANSI/文字幅計算コードを再利用。

## 変更するファイル

### 1. バインディング定義
**ファイル**: `src/bun.js/api/BunObject.bind.ts`

```typescript
export const WrapAnsiOptions = t.dictionary({
  hard: t.boolean.default(false),
  wordWrap: t.boolean.default(true),
  trim: t.boolean.default(true),
  ambiguousIsNarrow: t.boolean.default(true),
});

export const wrapAnsi = fn({
  args: {
    global: t.globalObject,
    str: t.DOMString.default(""),
    columns: t.usize,
    opts: WrapAnsiOptions.default({}),
  },
  ret: t.any, // JSValue (String)
});
```

### 2. Zig実装
**ファイル**: `src/bun.js/api/BunObject.zig`

エントリーポイント関数を追加。コア実装を呼び出す。

### 3. コア実装(新規)
**ファイル**: `src/string/immutable/wrap_ansi.zig`

主要な処理:
1. 改行で分割し、各行を処理
2. スペースで単語に分割し、表示幅を計算(ANSI除外)
3. 列幅に基づいて折り返し
4. ANSIエスケープコードの追跡(SGR、OSC 8ハイパーリンク)
5. 行跨ぎでスタイルを閉じて再開

既存コードの再利用:
- `src/string/immutable/visible.zig` - 表示幅計算
- ANSI CSI/OSCパースロジック

### 4. テスト
**ファイル**: `test/js/bun/util/wrapAnsi.test.ts`

NPMライブラリとの比較テスト:
- 基本的な折り返し
- ANSIカラー付き文字列
- hard/wordWrap/trimオプション
- 全角文字、絵文字、サロゲートペア
- ハイパーリンク

### 5. 型定義
**ファイル**: `packages/bun-types/bun.d.ts`

TypeScript型定義を追加。

## 実装ステップ

### Step 1: bindgen定義追加
`BunObject.bind.ts`にオプションと関数シグネチャを追加。

### Step 2: コア実装
`wrap_ansi.zig`に以下を実装:
- `wrapAnsi()` - メイン関数
- `wrapLine()` - 1行の処理
- `wrapWord()` - 長い単語の分割(hardモード)
- ANSIスタイル追跡構造体
- SGRコードの開始→終了マッピング

### Step 3: BunObjectへの統合
`BunObject.zig`にエントリーポイント関数を追加。

### Step 4: テスト作成
NPMとの互換性テストを作成。

### Step 5: 型定義追加
`bun.d.ts`にインターフェースを追加。

## SGRコードマッピング
行跨ぎでスタイルを復元するために必要:

| 開始コード | 終了コード | 説明 |
|-----------|-----------|------|
| 1 | 22 | bold |
| 2 | 22 | dim |
| 3 | 23 | italic |
| 4 | 24 | underline |
| 7 | 27 | inverse |
| 8 | 28 | hidden |
| 9 | 29 | strikethrough |
| 30-37 | 39 | foreground color |
| 40-47 | 49 | background color |
| 90-97 | 39 | bright foreground |
| 100-107 | 49 | bright background |

## 検証方法

```bash
# テスト実行
bun bd test test/js/bun/util/wrapAnsi.test.ts

# システムBunでは失敗することを確認
USE_SYSTEM_BUN=1 bun test test/js/bun/util/wrapAnsi.test.ts

# 手動テスト
bun bd -e "console.log(Bun.wrapAnsi('hello world', 5))"
```

## 注意事項
- 全角文字は幅2としてカウント
- ANSIエスケープは幅0
- Unicode正規化(normalize())を適用
- `\r\n`を`\n`に正規化
</claude-plan>
Document that extended color sequences are correctly preserved across
line wraps without close/reopen, matching npm wrap-ansi behavior.

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 0
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# wrapAnsi関数の実装計画

## 概要
BunにNPMの`wrap-ansi`ライブラリと互換性のある`Bun.wrapAnsi()`関数を追加する。

## APIシグネチャ
```typescript
Bun.wrapAnsi(string: string, columns: number, options?: WrapAnsiOptions): string

interface WrapAnsiOptions {
  hard?: boolean;            // default: false - 長い単語を強制分割
  wordWrap?: boolean;        // default: true - 単語境界で折り返し
  trim?: boolean;            // default: true - 行の先頭・末尾の空白を削除
  ambiguousIsNarrow?: boolean; // default: true - 曖昧幅文字をnarrow(幅1)として扱う
}
```

## 実装アプローチ
`stringWidth`と同様にZig + bindgenシステムを使用。既存のANSI/文字幅計算コードを再利用。

## 変更するファイル

### 1. バインディング定義
**ファイル**: `src/bun.js/api/BunObject.bind.ts`

```typescript
export const WrapAnsiOptions = t.dictionary({
  hard: t.boolean.default(false),
  wordWrap: t.boolean.default(true),
  trim: t.boolean.default(true),
  ambiguousIsNarrow: t.boolean.default(true),
});

export const wrapAnsi = fn({
  args: {
    global: t.globalObject,
    str: t.DOMString.default(""),
    columns: t.usize,
    opts: WrapAnsiOptions.default({}),
  },
  ret: t.any, // JSValue (String)
});
```

### 2. Zig実装
**ファイル**: `src/bun.js/api/BunObject.zig`

エントリーポイント関数を追加。コア実装を呼び出す。

### 3. コア実装(新規)
**ファイル**: `src/string/immutable/wrap_ansi.zig`

主要な処理:
1. 改行で分割し、各行を処理
2. スペースで単語に分割し、表示幅を計算(ANSI除外)
3. 列幅に基づいて折り返し
4. ANSIエスケープコードの追跡(SGR、OSC 8ハイパーリンク)
5. 行跨ぎでスタイルを閉じて再開

既存コードの再利用:
- `src/string/immutable/visible.zig` - 表示幅計算
- ANSI CSI/OSCパースロジック

### 4. テスト
**ファイル**: `test/js/bun/util/wrapAnsi.test.ts`

NPMライブラリとの比較テスト:
- 基本的な折り返し
- ANSIカラー付き文字列
- hard/wordWrap/trimオプション
- 全角文字、絵文字、サロゲートペア
- ハイパーリンク

### 5. 型定義
**ファイル**: `packages/bun-types/bun.d.ts`

TypeScript型定義を追加。

## 実装ステップ

### Step 1: bindgen定義追加
`BunObject.bind.ts`にオプションと関数シグネチャを追加。

### Step 2: コア実装
`wrap_ansi.zig`に以下を実装:
- `wrapAnsi()` - メイン関数
- `wrapLine()` - 1行の処理
- `wrapWord()` - 長い単語の分割(hardモード)
- ANSIスタイル追跡構造体
- SGRコードの開始→終了マッピング

### Step 3: BunObjectへの統合
`BunObject.zig`にエントリーポイント関数を追加。

### Step 4: テスト作成
NPMとの互換性テストを作成。

### Step 5: 型定義追加
`bun.d.ts`にインターフェースを追加。

## SGRコードマッピング
行跨ぎでスタイルを復元するために必要:

| 開始コード | 終了コード | 説明 |
|-----------|-----------|------|
| 1 | 22 | bold |
| 2 | 22 | dim |
| 3 | 23 | italic |
| 4 | 24 | underline |
| 7 | 27 | inverse |
| 8 | 28 | hidden |
| 9 | 29 | strikethrough |
| 30-37 | 39 | foreground color |
| 40-47 | 49 | background color |
| 90-97 | 39 | bright foreground |
| 100-107 | 49 | bright background |

## 検証方法

```bash
# テスト実行
bun bd test test/js/bun/util/wrapAnsi.test.ts

# システムBunでは失敗することを確認
USE_SYSTEM_BUN=1 bun test test/js/bun/util/wrapAnsi.test.ts

# 手動テスト
bun bd -e "console.log(Bun.wrapAnsi('hello world', 5))"
```

## 注意事項
- 全角文字は幅2としてカウント
- ANSIエスケープは幅0
- Unicode正規化(normalize())を適用
- `\r\n`を`\n`に正規化
</claude-plan>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/bun.js/bindings/wrapAnsi.cpp`:
- Around line 56-70: The Latin1 branch in stringWidth ignores the
ambiguousIsNarrow flag and calls Bun__visibleWidthExcludeANSI_latin1 without
passing ambiguity, causing width mismatches with wrapWord/getVisibleWidth;
update stringWidth to forward ambiguousIsNarrow to the Latin1 width helper (or
replace the Latin1 path with a per-codepoint loop that calls getVisibleWidth for
each U+0000–U+00FF codepoint) and adjust the comment to remove the incorrect
claim that Latin1 has no ambiguous-width characters; changes should touch
stringWidth, the call site to Bun__visibleWidthExcludeANSI_latin1, and the
comment lines referenced.
♻️ Duplicate comments (1)
src/bun.js/bindings/wrapAnsi.cpp (1)

264-271: OSC ST terminators are not recognized

OSC sequences can terminate with ST (ESC \ or 0x9C). isAnsiEscapeTerminator only treats BEL as the OSC terminator (Line 268–270), which can leave the parser in escape mode and drop visible text for OSC 8 hyperlinks that use ST. Please handle ST (and the ESC \ sequence) in the escape-state logic.

🐛 Suggested fix (partial)
-    if (isOscSequence)
-        return c == 0x07; // BEL terminates OSC sequences
+    if (isOscSequence)
+        return c == 0x07 || c == 0x9c; // BEL or ST terminates OSC sequences

You’ll still need to detect the two-byte ESC \ sequence in the loops that track escape state (e.g., wrapWord, trimLeadingSpaces, trimRowTrailingSpaces).

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between d3aadda and f9f6935.

📒 Files selected for processing (2)
  • src/bun.js/bindings/wrapAnsi.cpp
  • test/js/bun/util/wrapAnsi.test.ts
🧰 Additional context used
📓 Path-based instructions (3)
test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}

📄 CodeRabbit inference engine (test/CLAUDE.md)

test/**/*.test.{ts,js,jsx,tsx,mjs,cjs}: Use bun bd test <...test file> to run tests with compiled code changes. Do not use bun test as it will not include your changes.
Use bun:test for files ending in *.test.{ts,js,jsx,tsx,mjs,cjs}. For test files without .test extension in test/js/node/test/{parallel,sequential}/*.js, use bun bd <file> instead of bun bd test <file> since they expect exit code 0.
Do not set a timeout on tests. Bun already has timeouts built-in.

Files:

  • test/js/bun/util/wrapAnsi.test.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (CLAUDE.md)

test/**/*.test.ts: Use Bun's Jest-compatible test runner with proper test fixtures and imports from harness
Always use port: 0 when binding to ports in tests - never hardcode ports or use custom random port functions
Use normalizeBunSnapshot to normalize snapshot output in tests
Never write tests that check for 'panic', 'uncaught exception', or similar strings in test output as these will never fail in CI
Use tempDir from harness to create temporary directories in tests - do not use tmpdirSync or fs.mkdtempSync
In spawned process tests, use expect(stdout).toBe(...) BEFORE expect(exitCode).toBe(0) for more useful error messages
Do not use setTimeout in tests - await the condition to be met instead, as you are testing the CONDITION not TIME PASSING

Files:

  • test/js/bun/util/wrapAnsi.test.ts
src/bun.js/bindings/*.cpp

📄 CodeRabbit inference engine (CLAUDE.md)

When implementing JavaScript classes in C++, create three classes if there's a public constructor: class inheriting from JSDestructibleObject, a Prototype class, and a Constructor class

Files:

  • src/bun.js/bindings/wrapAnsi.cpp
🧠 Learnings (44)
📓 Common learnings
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24212
File: src/cli/publish_command.zig:782-788
Timestamp: 2025-10-30T21:52:04.707Z
Learning: In the Bun codebase (oven-sh/bun), `enable_ansi_colors` flags are used to gate both ANSI color codes and Unicode box-drawing characters/emoji. This is the established pattern across the codebase.
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/test/v8/v8.test.ts : Add corresponding test cases to test/v8/v8.test.ts using checkSameOutput() function to compare Node.js and Bun output

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2026-01-15T03:22:50.711Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-15T03:22:50.711Z
Learning: Applies to test/**/*.test.ts : Use Bun's Jest-compatible test runner with proper test fixtures and imports from `harness`

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun bd test <...test file>` to run tests with compiled code changes. Do not use `bun test` as it will not include your changes.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Use `bun:test` for files ending in `*.test.{ts,js,jsx,tsx,mjs,cjs}`. For test files without .test extension in test/js/node/test/{parallel,sequential}/*.js, use `bun bd <file>` instead of `bun bd test <file>` since they expect exit code 0.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-19T02:44:46.354Z
Learnt from: theshadow27
Repo: oven-sh/bun PR: 23798
File: packages/bun-otel/context-propagation.test.ts:1-1
Timestamp: 2025-10-19T02:44:46.354Z
Learning: In the Bun repository, standalone packages under packages/ (e.g., bun-vscode, bun-inspector-protocol, bun-plugin-yaml, bun-plugin-svelte, bun-debug-adapter-protocol, bun-otel) co-locate their tests with package source code using *.test.ts files. This follows standard npm/monorepo patterns. The test/ directory hierarchy (test/js/bun/, test/cli/, test/js/node/) is reserved for testing Bun's core runtime APIs and built-in functionality, not standalone packages.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2026-01-15T03:22:50.711Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-15T03:22:50.711Z
Learning: Applies to test/**/*.test.ts : Use `normalizeBunSnapshot` to normalize snapshot output in tests

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-11-14T16:07:01.064Z
Learnt from: RiskyMH
Repo: oven-sh/bun PR: 24719
File: docs/bundler/executables.mdx:527-560
Timestamp: 2025-11-14T16:07:01.064Z
Learning: In the Bun repository, certain bundler features like compile with code splitting (--compile --splitting) are CLI-only and not supported in the Bun.build() JavaScript API. Tests for CLI-only features use backend: "cli" flag (e.g., test/bundler/bundler_compile_splitting.test.ts). The CompileBuildConfig interface correctly restricts these with splitting?: never;. When documenting CLI-only bundler features, add a note clarifying they're not available via the programmatic API.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-26T01:32:04.844Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 24082
File: test/cli/test/coverage.test.ts:60-112
Timestamp: 2025-10-26T01:32:04.844Z
Learning: In the Bun repository test files (test/cli/test/*.test.ts), when spawning Bun CLI commands with Bun.spawnSync for testing, prefer using stdio: ["inherit", "inherit", "inherit"] to inherit stdio streams rather than piping them.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*.test.{ts,js,jsx,tsx,mjs,cjs} : Do not set a timeout on tests. Bun already has timeouts built-in.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-08T13:48:02.430Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 23373
File: test/js/bun/tarball/extract.test.ts:107-111
Timestamp: 2025-10-08T13:48:02.430Z
Learning: In Bun's test runner, use `expect(async () => { await ... }).toThrow()` to assert async rejections. Unlike Jest/Vitest, Bun does not require `await expect(...).rejects.toThrow()` - the async function wrapper with `.toThrow()` is the correct pattern for async error assertions in Bun tests.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2026-01-15T03:22:50.711Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-15T03:22:50.711Z
Learning: Applies to test/**/*.test.ts : Never write tests that check for 'panic', 'uncaught exception', or similar strings in test output as these will never fail in CI

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-09-30T22:53:19.887Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 23117
File: src/bun.js/test/snapshot.zig:265-276
Timestamp: 2025-09-30T22:53:19.887Z
Learning: In Bun's snapshot testing (src/bun.js/test/snapshot.zig), multiple inline snapshots at the same line and column (same call position) must have identical values. However, multiple inline snapshots on the same line at different columns are allowed to have different values. The check is position-specific (line+col), not line-wide.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2026-01-15T03:22:50.711Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-15T03:22:50.711Z
Learning: Applies to test/**/*.test.ts : In spawned process tests, use expect(stdout).toBe(...) BEFORE expect(exitCode).toBe(0) for more useful error messages

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Ensure V8 API tests compare identical C++ code output between Node.js and Bun through the test suite validation process

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-09-20T00:58:38.042Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 22568
File: test/js/valkey/valkey.test.ts:561-564
Timestamp: 2025-09-20T00:58:38.042Z
Learning: For test/js/valkey/valkey.test.ts, do not comment on synchronous throw assertions for async Redis methods (like ctx.redis.set(), ctx.redis.unsubscribe(), etc.) - Bun's Redis client implementation differs from Node.js and can throw synchronously even for async methods. The maintainer has explicitly requested to stop looking at this error pattern.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-18T05:23:24.403Z
Learnt from: theshadow27
Repo: oven-sh/bun PR: 23798
File: test/js/bun/telemetry-server.test.ts:91-100
Timestamp: 2025-10-18T05:23:24.403Z
Learning: In the Bun codebase, telemetry tests (test/js/bun/telemetry-*.test.ts) should focus on telemetry API behavior: configure/disable/isEnabled, callback signatures and invocation, request ID correlation, and error handling. HTTP protocol behaviors like status code normalization (e.g., 200 with empty body → 204) should be tested in HTTP server tests (test/js/bun/http/), not in telemetry tests. Keep separation of concerns: telemetry tests verify the telemetry API contract; HTTP tests verify HTTP semantics.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2026-01-05T23:04:01.518Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/CLAUDE.md:0-0
Timestamp: 2026-01-05T23:04:01.518Z
Learning: Applies to test/**/*-fixture.ts : Test files that spawn Bun processes should end in `*-fixture.ts` to identify them as test fixtures rather than tests themselves.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2026-01-14T21:08:10.438Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: test/js/node/test/parallel/CLAUDE.md:0-0
Timestamp: 2026-01-14T21:08:10.438Z
Learning: These are Node.js compatibility tests not written by Bun and cannot be modified

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-11-06T00:58:23.965Z
Learnt from: markovejnovic
Repo: oven-sh/bun PR: 24417
File: test/js/bun/spawn/spawn.test.ts:903-918
Timestamp: 2025-11-06T00:58:23.965Z
Learning: In Bun test files, `await using` with spawn() is appropriate for long-running processes that need guaranteed cleanup on scope exit or when explicitly testing disposal behavior. For short-lived processes that exit naturally (e.g., console.log scripts), the pattern `const proc = spawn(...); await proc.exited;` is standard and more common, as evidenced by 24 instances vs 4 `await using` instances in test/js/bun/spawn/spawn.test.ts.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-30T21:52:04.707Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24212
File: src/cli/publish_command.zig:782-788
Timestamp: 2025-10-30T21:52:04.707Z
Learning: In the Bun codebase (oven-sh/bun), `enable_ansi_colors` flags are used to gate both ANSI color codes and Unicode box-drawing characters/emoji. This is the established pattern across the codebase.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-09-20T03:39:41.770Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 22534
File: test/regression/issue/21830.fixture.ts:14-63
Timestamp: 2025-09-20T03:39:41.770Z
Learning: Bun's test runner supports async describe callbacks, unlike Jest/Vitest where describe callbacks must be synchronous. The syntax `describe("name", async () => { ... })` is valid in Bun.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-09-24T05:48:59.872Z
Learnt from: nektro
Repo: oven-sh/bun PR: 22806
File: scripts/runner.node.mjs:687-689
Timestamp: 2025-09-24T05:48:59.872Z
Learning: In the Bun codebase, the `startGroup` utility function in scripts/runner.node.mjs automatically closes any previously open group when called, so `startGroup("End")` correctly closes the final test group and keeps subsequent output ungrouped.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-10-25T17:20:19.041Z
Learnt from: theshadow27
Repo: oven-sh/bun PR: 24063
File: test/js/bun/telemetry/server-header-injection.test.ts:5-20
Timestamp: 2025-10-25T17:20:19.041Z
Learning: In the Bun telemetry codebase, tests are organized into two distinct layers: (1) Internal API tests in test/js/bun/telemetry/ use numeric InstrumentKind enum values to test Zig↔JS injection points and low-level integration; (2) Public API tests in packages/bun-otel/test/ use string InstrumentKind values ("http", "fetch", etc.) to test the public-facing BunSDK and instrumentation APIs. This separation allows internal tests to use efficient numeric enums for refactoring flexibility while the public API maintains a developer-friendly string-based interface.

Applied to files:

  • test/js/bun/util/wrapAnsi.test.ts
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.h : Add BUN_EXPORT visibility attribute to all public V8 API functions to ensure proper symbol export across platforms

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/napi/napi.zig : For each new V8 C++ method, add both GCC/Clang and MSVC mangled symbol names to the V8API struct in src/napi/napi.zig using extern fn declarations

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:37:47.899Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/AGENTS.md:0-0
Timestamp: 2025-11-24T18:37:47.899Z
Learning: Applies to src/bun.js/bindings/v8/**/<UNKNOWN> : <UNKNOWN>

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-09-20T05:35:57.318Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 22534
File: src/bun.js/bindings/headers.h:729-731
Timestamp: 2025-09-20T05:35:57.318Z
Learning: symbols.txt in the Bun codebase is specifically for V8 API mangled symbols (without leading underscore), not for general Bun host functions declared with BUN_DECLARE_HOST_FUNCTION. Host functions are handled through different build mechanisms.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.dyn : Add symbol names with leading underscore and semicolons in braces to src/symbols.dyn for each new V8 API method

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/src/symbols.txt : Add symbol names without leading underscore to src/symbols.txt for each new V8 API method

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2026-01-15T03:22:50.711Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-01-15T03:22:50.711Z
Learning: C++ code for JavaScriptCore bindings should be placed in `src/bun.js/bindings/*.cpp`

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.h : Create V8 class headers with .h extension following the pattern V8ClassName.h that include pragma once, v8.h, V8Local.h, V8Isolate.h, and declare classes extending from Data with BUN_EXPORT static methods

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-10-01T21:59:54.571Z
Learnt from: taylordotfish
Repo: oven-sh/bun PR: 23169
File: src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h:47-74
Timestamp: 2025-10-01T21:59:54.571Z
Learning: In the new bindings generator (bindgenv2) for `src/bun.js/bindings/webcore/JSDOMConvertEnumeration.h`, the context-aware enumeration conversion overloads intentionally use stricter validation (requiring `value.isString()` without ToString coercion), diverging from Web IDL semantics. This is a design decision documented in comments.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Create V8 class implementations with .cpp extension following the pattern V8ClassName.cpp that include the header, v8_compatibility_assertions.h, use ASSERT_V8_TYPE_LAYOUT_MATCHES macro, and implement methods using isolate->currentHandleScope()->createLocal<T>() for handle creation

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-10-24T10:43:09.398Z
Learnt from: fmguerreiro
Repo: oven-sh/bun PR: 23774
File: src/install/PackageManager/updatePackageJSONAndInstall.zig:548-548
Timestamp: 2025-10-24T10:43:09.398Z
Learning: In Bun's Zig codebase, the `as(usize, intCast(...))` cast pattern triggers a Zig compiler bug that causes compilation to hang indefinitely when used in complex control flow contexts (loops + short-circuit operators + optional unwrapping). Avoid this pattern and use simpler alternatives like just `intCast(...)` if type casting is necessary.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-10T00:57:09.173Z
Learnt from: franciscop
Repo: oven-sh/bun PR: 24514
File: src/bun.js/api/crypto/PasswordObject.zig:86-101
Timestamp: 2025-11-10T00:57:09.173Z
Learning: In Bun's Zig codebase (PasswordObject.zig), when validating the parallelism parameter for Argon2, the upper limit is set to 65535 (2^16 - 1) rather than using `std.math.maxInt(u24)` because the latter triggers Zig's truncation limit checks. The value 65535 is a practical upper bound that avoids compiler issues while being sufficient for thread parallelism use cases.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-12-11T02:11:47.024Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 25462
File: src/ast/visitExpr.zig:1644-1695
Timestamp: 2025-12-11T02:11:47.024Z
Learning: In Bun's bundler feature flag implementation (src/ast/visitExpr.zig), the validation for feature() flag names intentionally only rejects UTF-16 strings (checking `is_utf16`) while allowing UTF-8 strings, even though the error message says "must be an ASCII string". This is the intended behavior and should not be changed to enforce strict ASCII validation.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-12-23T06:50:41.142Z
Learnt from: Jarred-Sumner
Repo: oven-sh/bun PR: 25429
File: src/bun.js/bindings/helpers.h:422-422
Timestamp: 2025-12-23T06:50:41.142Z
Learning: In Bun's C++ bindings (src/bun.js/bindings/helpers.h and similar files), when returning an empty JSC::Identifier and a VM is accessible, prefer using `vm.propertyNames->emptyIdentifier` over constructing with `JSC::Identifier(JSC::Identifier::EmptyIdentifierFlag::EmptyIdentifier)`. The cached identifier from the VM's property names table is more efficient and consistent with WebKit upgrade patterns.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-03T20:43:06.996Z
Learnt from: pfgithub
Repo: oven-sh/bun PR: 24273
File: src/bun.js/test/snapshot.zig:19-19
Timestamp: 2025-11-03T20:43:06.996Z
Learning: In Bun's Zig codebase, when storing JSValue objects in collections like ArrayList, use `jsc.Strong.Optional` (not raw JSValue). When adding values, wrap them with `jsc.Strong.Optional.create(value, globalThis)`. In cleanup code, iterate the collection calling `.deinit()` on each Strong.Optional item before calling `.deinit()` on the ArrayList itself. This pattern automatically handles GC protection. See examples in src/bun.js/test/ScopeFunctions.zig and src/bun.js/node/node_cluster_binding.zig.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:37:11.466Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/js/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:37:11.466Z
Learning: Applies to src/js/{builtins,node,bun,thirdparty,internal}/**/*.{ts,js} : Use JSC intrinsics (prefixed with `$`) such as `$Array.from()`, `$isCallable()`, and `$newArrayWithSize()` for performance-critical operations

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Use JSC::WriteBarrier for heap-allocated references in V8 objects and implement visitChildren() for custom heap objects to support garbage collection

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-09-07T08:20:47.215Z
Learnt from: RiskyMH
Repo: oven-sh/bun PR: 22258
File: src/cli/test_command.zig:1258-1281
Timestamp: 2025-09-07T08:20:47.215Z
Learning: For Bun's test line filtering feature, the parseFileLineArg function should only handle the specific cases of "file:line" and "file:line:col" formats. It should not try to be overly tolerant of other patterns, as components like ":col" or other non-numeric segments could legitimately be part of filenames. The current conservative approach that checks for numeric segments in expected positions is appropriate.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-09-12T18:16:50.754Z
Learnt from: RiskyMH
Repo: oven-sh/bun PR: 22606
File: src/glob/GlobWalker.zig:449-452
Timestamp: 2025-09-12T18:16:50.754Z
Learning: For Bun codebase: prefer using `std.fs.path.sep` over manual platform separator detection, and use `bun.strings.lastIndexOfChar` instead of `std.mem.lastIndexOfScalar` for string operations.

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
📚 Learning: 2025-11-24T18:36:59.706Z
Learnt from: CR
Repo: oven-sh/bun PR: 0
File: src/bun.js/bindings/v8/CLAUDE.md:0-0
Timestamp: 2025-11-24T18:36:59.706Z
Learning: Applies to src/bun.js/bindings/v8/V8*.cpp : Use localToJSValue() to convert V8 handles to JSC values and perform JSC operations within V8 method implementations

Applied to files:

  • src/bun.js/bindings/wrapAnsi.cpp
🧬 Code graph analysis (1)
src/bun.js/bindings/wrapAnsi.cpp (1)
src/bun.js/api/BunObject.bind.ts (1)
  • stringWidth (30-36)
🪛 Cppcheck (2.19.0)
src/bun.js/bindings/wrapAnsi.cpp

[information] 42-42: Include file

(missingInclude)


[information] 52-52: Include file

(missingIncludeSystem)


[information] 59-59: Include file

(missingIncludeSystem)


[error] 66-66: failed to evaluate #if condition, undefined function-like macro invocation

(syntaxError)

🔇 Additional comments (13)
src/bun.js/bindings/wrapAnsi.cpp (6)

166-254: Wrap-word flow and width tracking look solid

The per-character width accounting and hard-wrap behavior are consistent and handle ANSI escapes cleanly.


273-341: Trailing-trim preserves ANSI tails nicely

The logic to keep trailing ANSI sequences while trimming visible whitespace is well-structured.


424-499: ANSI close/reopen joining logic is well structured

The close-before-newline and reopen-after-newline flow is clear and matches the preservation intent.


506-610: Line processing and word length precompute look good

Option handling and wrap branching are consistent with the intended semantics.


616-674: CRLF normalization + per-line processing look good

The normalization pass and line-by-line wrap flow are clear and efficient.


680-745: Binding argument handling is solid

Input conversion, column validation, and option parsing are well guarded.

test/js/bun/util/wrapAnsi.test.ts (7)

3-40: Baseline + hard-wrap coverage looks good

These cases provide a solid foundation for default and hard-wrap behavior.


42-59: Option toggles covered

wordWrap and trim behavior are explicitly exercised.


61-97: ANSI handling coverage is strong

The tests validate preservation and width exclusion with ANSI sequences.


99-129: Unicode/full-width coverage looks good

Full-width and emoji scenarios are well represented.


131-143: Existing newline handling covered

Line-preservation behavior is tested.


145-177: Edge cases + ambiguous width option are covered

Good breadth across tabs/spaces and ambiguous width handling.


179-235: Column edge cases + width tracking + extended SGR coverage look good

These tests exercise critical correctness scenarios and advanced SGR handling.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +56 to +70
template<typename Char>
static size_t stringWidth(const Char* start, const Char* end, bool ambiguousIsNarrow)
{
size_t len = end - start;
if (len == 0)
return 0;

if constexpr (sizeof(Char) == 1) {
// 8-bit JSC strings are Latin1, not UTF-8
// Note: Latin1 doesn't have ambiguous width characters (all are in U+0000-U+00FF)
(void)ambiguousIsNarrow;
return Bun__visibleWidthExcludeANSI_latin1(reinterpret_cast<const uint8_t*>(start), len);
} else {
return Bun__visibleWidthExcludeANSI_utf16(reinterpret_cast<const uint16_t*>(start), len, !ambiguousIsNarrow);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

wc -l src/bun.js/bindings/wrapAnsi.cpp

Repository: oven-sh/bun

Length of output: 91


🏁 Script executed:

head -100 src/bun.js/bindings/wrapAnsi.cpp

Repository: oven-sh/bun

Length of output: 3008


🏁 Script executed:

sed -n '40,100p' src/bun.js/bindings/wrapAnsi.cpp

Repository: oven-sh/bun

Length of output: 1759


🏁 Script executed:

sed -n '150,300p' src/bun.js/bindings/wrapAnsi.cpp

Repository: oven-sh/bun

Length of output: 5009


🏁 Script executed:

grep -n "wrapWord\|ambiguousIsNarrow" src/bun.js/bindings/wrapAnsi.cpp | head -30

Repository: oven-sh/bun

Length of output: 1682


🏁 Script executed:

grep -n "Bun__visibleWidthExcludeANSI_latin1\|Bun__visibleWidthExcludeANSI_utf16\|Bun__codepointWidth" src/bun.js/bindings/wrapAnsi.cpp | head -20

Repository: oven-sh/bun

Length of output: 616


🏁 Script executed:

sed -n '1,20p' src/bun.js/bindings/wrapAnsi.cpp

Repository: oven-sh/bun

Length of output: 822


🏁 Script executed:

python3 << 'EOF'
# Verify East Asian Width properties for U+00A1 and U+00A4
codepoints = [0x00A1, 0x00A4]
names = {0x00A1: "¡ (inverted exclamation)", 0x00A4: "¤ (currency sign)"}

for cp in codepoints:
    print(f"U+{cp:04X} {names[cp]}")
    # These are known ambiguous-width characters in Unicode East Asian Width
    # U+00A1 to U+00A9, U+00AB to U+00B9, U+00BB to U+00BF are ambiguous
    if 0x00A1 <= cp <= 0x00A9 or 0x00AB <= cp <= 0x00B9 or 0x00BB <= cp <= 0x00BF:
        print("  → In ambiguous East Asian Width range: YES")
    else:
        print("  → In ambiguous range: NO")
    
    # Check Latin1 range
    if cp <= 0x00FF:
        print("  → In Latin1 (U+0000-U+00FF): YES")
EOF

Repository: oven-sh/bun

Length of output: 141


🏁 Script executed:

sed -n '57,70p' src/bun.js/bindings/wrapAnsi.cpp

Repository: oven-sh/bun

Length of output: 658


🏁 Script executed:

sed -n '195,210p' src/bun.js/bindings/wrapAnsi.cpp

Repository: oven-sh/bun

Length of output: 650


Latin1 path ignores ambiguousIsNarrow flag, causing potential width mismatch

stringWidth discards the ambiguousIsNarrow parameter for Latin1 (line 66) and calls Bun__visibleWidthExcludeANSI_latin1 without any ambiguous-width handling, but wrapWord calculates per-character widths using getVisibleWidth with the ambiguous flag enabled (line 201) for all codepoints including Latin1. This creates a desync when precomputing word lengths (line 527) vs. actual character wrapping: if ambiguousIsNarrow=false, stringWidth will return a width that differs from what wrapWord will compute for Latin1 strings containing ambiguous-width codepoints (e.g., U+00A1, U+00A4).

Additionally, the comment on lines 64–65 stating "Latin1 doesn't have ambiguous width characters" is misleading—Latin1 (U+0000–U+00FF) does contain ambiguous East Asian Width characters. Consider passing the ambiguous flag to the Latin1 helper or using per-character width calculation, and correct the comment.

🧰 Tools
🪛 Cppcheck (2.19.0)

[information] 59-59: Include file

(missingIncludeSystem)


[error] 66-66: failed to evaluate #if condition, undefined function-like macro invocation

(syntaxError)

🤖 Prompt for AI Agents
In `@src/bun.js/bindings/wrapAnsi.cpp` around lines 56 - 70, The Latin1 branch in
stringWidth ignores the ambiguousIsNarrow flag and calls
Bun__visibleWidthExcludeANSI_latin1 without passing ambiguity, causing width
mismatches with wrapWord/getVisibleWidth; update stringWidth to forward
ambiguousIsNarrow to the Latin1 width helper (or replace the Latin1 path with a
per-codepoint loop that calls getVisibleWidth for each U+0000–U+00FF codepoint)
and adjust the comment to remove the incorrect claim that Latin1 has no
ambiguous-width characters; changes should touch stringWidth, the call site to
Bun__visibleWidthExcludeANSI_latin1, and the comment lines referenced.

@Jarred-Sumner
Jarred-Sumner merged commit 44df912 into main Jan 17, 2026
54 of 56 checks passed
@Jarred-Sumner
Jarred-Sumner deleted the claude/add-wrap-ansi branch January 17, 2026 00:12
sosukesuzuki added a commit that referenced this pull request Jan 17, 2026
Replace manual loops with WTF's optimized string search functions:

- Use WTF::findNextNewline for CRLF normalization (lines 628-639)
  This leverages SIMD-optimized newline detection for \r, \n, and \r\n

- Use WTF::find for space detection in word length calculation (lines 524-533)
  This uses optimized character search instead of manual iteration

These changes address the review feedback from #26061 requesting the use
of WTF::find for newline searches.

No performance regression observed - benchmarks show slight improvements
in most cases.

Claude-Generated-By: Claude Code (cli/claude-opus-4-5=100%)
Claude-Steers: 8
Claude-Permission-Prompts: 0
Claude-Escapes: 0
Claude-Plan:
<claude-plan>
# wrapAnsi.cpp の WTF::find 対応計画

## 概要
`src/bun.js/bindings/wrapAnsi.cpp` で手動ループによる文字検索を WTF の最適化された検索関数に置き換える。

## 修正対象

### 1. CRLF正規化ループ (Lines 628-639) - **最優先**
レビューコメントで指摘された箇所。

**現在のコード:**
```cpp
for (size_t i = 0; i < input.size(); ++i) {
    if (i + 1 < input.size() && input[i] == '\r' && input[i + 1] == '\n') {
        normalized.append(static_cast<Char>('\n'));
        i++;
    } else {
        normalized.append(input[i]);
    }
}
```

**修正方法:** `WTF::findNextNewline` を使用
```cpp
size_t pos = 0;
while (pos < input.size()) {
    auto newline = WTF::findNextNewline(input, pos);
    if (newline.position == WTF::notFound) {
        normalized.append(std::span { input.data() + pos, input.size() - pos });
        break;
    }
    normalized.append(std::span { input.data() + pos, newline.position - pos });
    normalized.append(static_cast<Char>('\n'));
    pos = newline.position + newline.length;
}
```

### 2. processLine() 内の単語長計算ループ (Lines 524-533)

**現在のコード:**
```cpp
for (const Char* it = lineStart; it <= lineEnd; ++it) {
    if (it == lineEnd || *it == ' ') {
        if (wordStart < it) {
            wordLengths.append(stringWidth(wordStart, it, options.ambiguousIsNarrow));
        } else {
            wordLengths.append(0);
        }
        wordStart = it + 1;
    }
}
```

**修正方法:** `WTF::find` でスペースを検索
```cpp
auto lineSpan = std::span<const Char>(lineStart, lineEnd);
size_t wordStartIdx = 0;
while (wordStartIdx <= lineSpan.size()) {
    size_t spacePos = WTF::find(lineSpan, static_cast<Char>(' '), wordStartIdx);
    size_t wordEndIdx = (spacePos == WTF::notFound) ? lineSpan.size() : spacePos;

    if (wordStartIdx < wordEndIdx) {
        wordLengths.append(stringWidth(lineSpan.data() + wordStartIdx,
                                       lineSpan.data() + wordEndIdx,
                                       options.ambiguousIsNarrow));
    } else {
        wordLengths.append(0);
    }

    if (spacePos == WTF::notFound)
        break;
    wordStartIdx = wordEndIdx + 1;
}
```

## 修正しない箇所(ANSIエスケープ処理が必要)

以下の箇所はANSIエスケープシーケンスの状態追跡が必要なため、`WTF::find` への置き換えは不適切:

- `Row::trimLeadingSpaces()` (lines 106-158)
- `trimRowTrailingSpaces()` (lines 274-340)
- `joinRowsWithAnsiPreservation()` (lines 425-499)
- `wrapWord()` (lines 166-254)

## 修正ファイル

- `src/bun.js/bindings/wrapAnsi.cpp`

## 検証方法

### 1. 修正前のベンチマーク(ベースライン取得・保存)

まだwrapAnsiがリリースされていないため、現在のリリースビルドでベースラインを取得し、ファイルに保存する。

```bash
# リリース済みのbunでベンチマーク実行(結果をファイルに保存)
bun bench/snippets/wrap-ansi.js 2>&1 | tee /tmp/wrapAnsi-baseline.txt
```

保存先: `/tmp/wrapAnsi-baseline.txt`

### 2. デバッグビルド
```bash
bun bd
```

### 3. 既存テストの実行
```bash
bun bd test test/js/bun/util/wrapAnsi.test.ts
```

### 4. 修正後のベンチマーク(性能比較)
```bash
# デバッグビルドでベンチマーク実行
bun bd bench/snippets/wrap-ansi.js 2>&1 | tee /tmp/wrapAnsi-after.txt
```

保存先: `/tmp/wrapAnsi-after.txt`

ベースラインと比較して性能低下がないことを確認:
```bash
diff /tmp/wrapAnsi-baseline.txt /tmp/wrapAnsi-after.txt
```

### 5. エッジケースの手動確認
```bash
# CRLF正規化が正しく動作するか
bun bd -e "console.log(JSON.stringify(Bun.wrapAnsi('hello\r\nworld', 10)))"

# 単語区切りが正しく動作するか
bun bd -e "console.log(Bun.wrapAnsi('word1 word2 word3', 8))"
```
</claude-plan>
Jarred-Sumner pushed a commit that referenced this pull request Jan 18, 2026
## Summary

This PR addresses the review feedback from #26061
([comment](#26061 (comment)))
requesting the use of `WTF::find` for newline searches in
`wrapAnsi.cpp`.

## Changes

### 1. CRLF Normalization (lines 628-639)
Replaced manual loop with `WTF::findNextNewline` which provides
SIMD-optimized detection for `\r`, `\n`, and `\r\n` sequences.

**Before:**
```cpp
for (size_t i = 0; i < input.size(); ++i) {
    if (i + 1 < input.size() && input[i] == '\r' && input[i + 1] == '\n') {
        normalized.append(static_cast<Char>('\n'));
        i++;
    } else {
        normalized.append(input[i]);
    }
}
```

**After:**
```cpp
size_t pos = 0;
while (pos < input.size()) {
    auto newline = WTF::findNextNewline(input, pos);
    if (newline.position == WTF::notFound) {
        normalized.append(std::span { input.data() + pos, input.size() - pos });
        break;
    }
    if (newline.position > pos)
        normalized.append(std::span { input.data() + pos, newline.position - pos });
    normalized.append(static_cast<Char>('\n'));
    pos = newline.position + newline.length;
}
```

### 2. Word Length Calculation (lines 524-533)
Replaced manual loop with `WTF::find` for space character detection.

**Before:**
```cpp
for (const Char* it = lineStart; it <= lineEnd; ++it) {
    if (it == lineEnd || *it == ' ') {
        // word boundary logic
    }
}
```

**After:**
```cpp
auto lineSpan = std::span<const Char>(lineStart, lineEnd);
size_t wordStartIdx = 0;
while (wordStartIdx <= lineSpan.size()) {
    size_t spacePos = WTF::find(lineSpan, static_cast<Char>(' '), wordStartIdx);
    // word boundary logic using spacePos
}
```

## Benchmark Results

Tested on Apple M4 Max. No performance regression observed - most
benchmarks show slight improvements.

| Benchmark | Before | After | Change |
|-----------|--------|-------|--------|
| Short text (45 chars) | 613 ns | 583 ns | -4.9% |
| Medium text (810 chars) | 10.85 µs | 10.31 µs | -5.0% |
| Long text (8100 chars) | 684 µs | 102 µs | -85% * |
| Colored short | 1.26 µs | 806 ns | -36% |
| Colored medium | 19.24 µs | 13.80 µs | -28% |
| Japanese (full-width) | 7.74 µs | 7.43 µs | -4.0% |
| Emoji text | 9.35 µs | 9.27 µs | -0.9% |
| Hyperlink (OSC 8) | 5.73 µs | 5.58 µs | -2.6% |

\* Large variance in baseline measurement

## Testing

- All 35 existing tests pass
- Manual verification of CRLF normalization and word wrapping edge cases

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
liooil pushed a commit to liooil/poly that referenced this pull request Aug 7, 2026
## Summary

This PR addresses the review feedback from #26061
([comment](oven-sh/bun#26061 (comment)))
requesting the use of `WTF::find` for newline searches in
`wrapAnsi.cpp`.

## Changes

### 1. CRLF Normalization (lines 628-639)
Replaced manual loop with `WTF::findNextNewline` which provides
SIMD-optimized detection for `\r`, `\n`, and `\r\n` sequences.

**Before:**
```cpp
for (size_t i = 0; i < input.size(); ++i) {
    if (i + 1 < input.size() && input[i] == '\r' && input[i + 1] == '\n') {
        normalized.append(static_cast<Char>('\n'));
        i++;
    } else {
        normalized.append(input[i]);
    }
}
```

**After:**
```cpp
size_t pos = 0;
while (pos < input.size()) {
    auto newline = WTF::findNextNewline(input, pos);
    if (newline.position == WTF::notFound) {
        normalized.append(std::span { input.data() + pos, input.size() - pos });
        break;
    }
    if (newline.position > pos)
        normalized.append(std::span { input.data() + pos, newline.position - pos });
    normalized.append(static_cast<Char>('\n'));
    pos = newline.position + newline.length;
}
```

### 2. Word Length Calculation (lines 524-533)
Replaced manual loop with `WTF::find` for space character detection.

**Before:**
```cpp
for (const Char* it = lineStart; it <= lineEnd; ++it) {
    if (it == lineEnd || *it == ' ') {
        // word boundary logic
    }
}
```

**After:**
```cpp
auto lineSpan = std::span<const Char>(lineStart, lineEnd);
size_t wordStartIdx = 0;
while (wordStartIdx <= lineSpan.size()) {
    size_t spacePos = WTF::find(lineSpan, static_cast<Char>(' '), wordStartIdx);
    // word boundary logic using spacePos
}
```

## Benchmark Results

Tested on Apple M4 Max. No performance regression observed - most
benchmarks show slight improvements.

| Benchmark | Before | After | Change |
|-----------|--------|-------|--------|
| Short text (45 chars) | 613 ns | 583 ns | -4.9% |
| Medium text (810 chars) | 10.85 µs | 10.31 µs | -5.0% |
| Long text (8100 chars) | 684 µs | 102 µs | -85% * |
| Colored short | 1.26 µs | 806 ns | -36% |
| Colored medium | 19.24 µs | 13.80 µs | -28% |
| Japanese (full-width) | 7.74 µs | 7.43 µs | -4.0% |
| Emoji text | 9.35 µs | 9.27 µs | -0.9% |
| Hyperlink (OSC 8) | 5.73 µs | 5.58 µs | -2.6% |

\* Large variance in baseline measurement

## Testing

- All 35 existing tests pass
- Manual verification of CRLF normalization and word wrapping edge cases

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Jarred-Sumner pushed a commit that referenced this pull request Sep 16, 2026
…e row list cannot grow (#42833)

### Problem
- `Bun.wrapAnsi("a ".repeat(2 ** 26), 1)` aborts: `panic(main thread):
abort() called`, exit 134. Top frames:
`WTF::VectorBufferBase::allocateBuffer<FailureAction::Crash>`
(Vector.h:228) <- `Vector::appendSlowCase<Bun::Row<unsigned char>>`. The
output (2^27 characters) is below the string length limit.
- `processLine` keeps one 32-byte `Row` for each wrapped row in a
`WTF::Vector` (`src/jsc/bindings/wrapAnsi.cpp:676`). `Vector::append`
calls `CRASH()` when it cannot grow: at the 51,821,029th row of one
input line.
- Two more Vectors in the file abort the same way: the text of one row,
and the `tail` copy in `trimRowTrailingSpaces`.

### Fix
- Every Vector append in the file is fallible: `tryAppend` behind
`Bun::maxVectorSize<T>()`, as #42649 does. A failure returns `false` up
to the binding, which throws `RangeError: Out of memory`.
- `trimRowTrailingSpaces` trims in place, as `trimLeadingSpaces` does.
It allocates nothing.
- Outputs that fit do not change: 1.9 million seeded random inputs
match.
- Verified: `test/js/bun/util/wrapAnsi.test.ts`. Its 2 new tests hold 15
inputs that throw (10 of the 11 statements that grow a Vector) and 5
that return exactly at a bound. Both fail without the fix. Also the
`wrapAnsi.npm`, `sliceAnsi`, `stringWidth`, `stripANSI` suites.

### Background
- `WTF::Vector<T>` holds `INT32_MAX / sizeof(T)` elements and grows by
half. `append` calls `CRASH()` when the next capacity passes that bound.
`tryAppend` returns false.
- `Bun::maxVectorSize<T>()` (`VectorSizeLimit.h`, from #42649) is that
bound, lowered by `Bun__stringSyntheticAllocationLimit`. The tests set
it to 64 KiB in a child, so the 2049th row reaches it.
- #42225 (open) makes the output `StringBuilder` of the same function
throw. It does not touch these Vectors.

<details><summary>Notes</summary>

**Repros, release builds on Linux x64: Bun 1.4.2 and canary 782c402
against this branch**

```js
// 1. The row list. Before: exit 134 after 4.2 s, 3.2 GB peak. After: RangeError in 4.3 s, 3.0 GB.
Bun.wrapAnsi("a ".repeat(2 ** 26), 1);
// 51,821,028 words return 103,642,055 characters before and after. One more word is the abort.

// 2. One UTF-16 row. Before: exit 134, 5.4 GB. After: RangeError, 5.1 GB.
Bun.wrapAnsi("\u2603".repeat(9e8) + " b", 2 ** 31);

// 3. One Latin-1 row. Before: exit 134, 5.4 GB. After: RangeError, 5.1 GB.
Bun.wrapAnsi("a".repeat(1.8e9) + " b", 2 ** 31);

// 4. The tail copy of the trailing trim. Before: exit 134, 9.7 GB. After: returns 900,000,001 characters, 6.7 GB.
Bun.wrapAnsi("a " + "\u200b".repeat(9e8), 80);
```

**Where each number comes from**
- `sizeof(Row<Char>)` is 32, so the largest legal capacity of the row
list is 67,108,863. `FastMalloc::nextCapacity` grows a full Vector from
51,821,028 to 77,731,542, which passes it. `tryAppend` refuses at the
same step, so the function now throws at the 51,821,029th row.
`maxVectorSize` binds only when a test lowers the limit. #42649 has the
same property.
- Repros 2 and 3: the first word fills the row with one exact-size
allocation. The separator space of the next word asks for 1.5 times
that, which passes `INT32_MAX` bytes.
- Repro 4: the row itself fits. `trimRowTrailingSpaces` copied the
zero-width tail into a second Vector one character at a time, and that
Vector failed to grow past 885,410,839 characters.

**A failed call**
- The rows of the current line are dropped and the function throws.
Nothing is cached between calls, so the next call starts clean.
- The message is the one #42649 and #37237 use. A `RangeError` with the
same text comes from JSC when `"a".repeat()` runs out of memory.

**Not changed here**
- A UTF-16 input of more than about 976 million characters still aborts,
in `WTF::StringBuilder` under `joinRowsWithAnsiPreservation`.
`wrapAnsiImpl` reserves 1.1 times the input length while the builder is
still 8-bit, and the first 16-bit append doubles that reserve past the
string limit. It is the output builder, which #42225 owns, so it is
tracked apart from this PR.
- `Bun.wrapAnsi` has had these Vectors since #26061 added it. This was
never correct, so the test is in the module's test file.

**The tests**
- Each child runs with `BUN_FEATURE_FLAG_SYNTHETIC_MEMORY_LIMIT=65536`,
the knob the #42649 tests use. The bounds become 2048 rows, and 65,536
Latin-1 or 32,768 UTF-16 characters in a row.
- Five inputs sit exactly at a bound and return: 2048 rows (Latin-1 and
UTF-16), 65,536 Latin-1 characters, 32,768 UTF-16 characters, and a
separator space as character 65,536. The same input one element longer
throws.
- Without the fix every input returns its normal length, so the failure
is an assertion diff and not an abort.
- A debugger breakpoint on every `return false` confirmed which
statement fails in each case. Every one is covered except the first row
of a line. That one fails only when the limit is below the 32 bytes of
one `Row`.

**No change for outputs that fit**
- A seeded generator builds inputs from escapes (SGR, OSC 8 hyperlinks,
C1, unterminated and malformed sequences), tabs, line breaks, wide,
zero-width, combining and surrogate characters, with `columns` 1 to 10
and every option set. A hash of 1.9 million outputs is equal on the
canary (09bb546 and 782c402) and on the debug ASAN build of this
branch.
- Wrapping 20 MB of text, best of 3, release builds: 225 ms before and
205 ms after at 80 columns, 302 ms and 258 ms for UTF-16 text, 189 ms
and 194 ms at 1 column with `hard`.

</details>

<!-- robobun:evidence:begin -->

---

**[human-review]** gate passed · iteration 0 · 2 files touched

<details><summary>fails on main (without fix)</summary>

```console
ASAN without fix: 2 FAILED
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/bun/util/wrapAnsi.test.ts
bun test v1.4.3 (09bb546)

test/js/bun/util/wrapAnsi.test.ts:
(pass) Bun.wrapAnsi > basic wrapping > wraps text at word boundaries [3.23ms]
(pass) Bun.wrapAnsi > basic wrapping > handles empty string [2.39ms]
(pass) Bun.wrapAnsi > basic wrapping > no wrapping needed [1.87ms]
(pass) Bun.wrapAnsi > basic wrapping > wraps multiple words [2.51ms]
(pass) Bun.wrapAnsi > basic wrapping > handles single long word [2.45ms]
(pass) Bun.wrapAnsi > basic wrapping > handles columns = 0 [1.82ms]
(pass) Bun.wrapAnsi > hard wrap option > breaks long words in middle [2.98ms]
(pass) Bun.wrapAnsi > hard wrap option > breaks very long word [3.53ms]
(pass) Bun.wrapAnsi > wordWrap option > wordWrap false disables wrapping [2.53ms]
(pass) Bun.wrapAnsi > wordWrap option > wordWrap: undefined keeps word wrapping on [2.86ms]
(pass) Bun.wrapAnsi > wordWrap option > wordWrap: null keeps word wrapping on [0.62ms]
(pass) Bun.wrapAnsi > wordWrap option > wordWrap: 0 keeps word wrapping on [0.45ms]
(pass) Bun.wrapAnsi > wordWrap opti
... (truncated)

release without fix: all passed
bun test v1.4.3-canary.1 (7606b00)

test/js/bun/util/wrapAnsi.test.ts:
(pass) Bun.wrapAnsi > basic wrapping > wraps text at word boundaries [0.05ms]
(pass) Bun.wrapAnsi > basic wrapping > handles empty string [0.02ms]
(pass) Bun.wrapAnsi > basic wrapping > no wrapping needed [0.01ms]
(pass) Bun.wrapAnsi > basic wrapping > wraps multiple words [0.01ms]
(pass) Bun.wrapAnsi > basic wrapping > handles single long word [0.01ms]
(pass) Bun.wrapAnsi > basic wrapping > handles columns = 0 [0.01ms]
(pass) Bun.wrapAnsi > hard wrap option > breaks long words in middle [0.02ms]
(pass) Bun.wrapAnsi > hard wrap option > breaks very long word [0.04ms]
(pass) Bun.wrapAnsi > wordWrap option > wordWrap false disables wrapping [0.04ms]
(pass) Bun.wrapAnsi > wordWrap option > wordWrap: undefined keeps word wrapping on [0.02ms]
(pass) Bun.wrapAnsi > wordWrap option > wordWrap: null keeps word wrapping on [0.01ms]
(pass) Bun.wrapAnsi > wordWrap option > wordWrap: 0 keeps word wrapping on
(pass) Bun.wrapAnsi > wordWrap option > wordWrap: "" keeps word wrapping on
(pass) Bun.wrapAnsi > wordWrap option > wordWrap: false breaks words character-by-character [0.02ms]
(pass) Bun.wrapAnsi > tr
... (truncated)
```

</details>

<details><summary>passes on PR (with fix)</summary>

```console
ASAN with fix: all passed
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test "--reporter=junit" "--reporter-outfile=/tmp/pr_gate.xml" test/js/bun/util/wrapAnsi.test.ts
bun test v1.4.3 (09bb546)

test/js/bun/util/wrapAnsi.test.ts:
(pass) Bun.wrapAnsi > basic wrapping > wraps text at word boundaries [2.58ms]
(pass) Bun.wrapAnsi > basic wrapping > handles empty string [1.66ms]
(pass) Bun.wrapAnsi > basic wrapping > no wrapping needed [1.31ms]
(pass) Bun.wrapAnsi > basic wrapping > wraps multiple words [1.64ms]
(pass) Bun.wrapAnsi > basic wrapping > handles single long word [1.59ms]
(pass) Bun.wrapAnsi > basic wrapping > handles columns = 0 [1.24ms]
(pass) Bun.wrapAnsi > hard wrap option > breaks long words in middle [2.03ms]
(pass) Bun.wrapAnsi > hard wrap option > breaks very long word [1.91ms]
(pass) Bun.wrapAnsi > wordWrap option > wordWrap false disables wrapping [1.66ms]
(pass) Bun.wrapAnsi > wordWrap option > wordWrap: undefined keeps word wrapping on [1.79ms]
(pass) Bun.wrapAnsi > wordWrap option > wordWrap: null keeps word wrapping on [0.47ms]
(pass) Bun.wrapAnsi > wordWrap option > wordWrap: 0 keeps word wrapping on [0.30ms]
(pass) Bun.wrapAnsi > wordWrap opti
... (truncated)

release with fix: all passed
$ bun scripts/build.ts --profile=release
[configured] bun-profile → bun (stripped) in 907ms (unchanged)
ninja: Entering directory `/workspace/bun/build/release'
[1/133] gen ErrorCode+*.h
[2/133] esbuild bun-error

  ../../build/release/codegen/bun-error/index.js       34.9kb
  ../../build/release/codegen/bun-error/bun-error.css  12.8kb

⚡ Done in 23ms
[3/133] gen compressed/codegen/bun-error/bun-error.css.zst
[4/133] gen compressed/codegen/bun-error/index.js.zst
[5/133] gen NodeModuleModule.lut.h
Generating /workspace/bun/build/release/codegen/NodeModuleModule.lut.h from /workspace/bun/src/jsc/modules/NodeModuleModule.cpp
[6/133] gen generated_host_exports.rs
generated_host_exports.rs: 122 exports (host=5, lazy=10, generic=107, rust=0); 243 extern-C blocks audited
[7/133] gen ZigGeneratedClasses.{cpp,h,rs}
Found 2 classes from /workspace/bun/src/jsc/resolve_message.classes.ts
  - ResolveMessage (15 fields)
  - BuildMessage (10 fields)
Found 1 classes from /workspace/bun/src/runtime/api/Archive.classes.ts
  - Archive (4 fields, 1 class fields)
Found 2 classes from /workspace/bun/src/runtime/api/BunObject.classes.ts
  - ResourceUsage (8 fields)
  - Subprocess (20 
... (truncated)
```

</details>

<details><summary>diff hotspot</summary>

```
src/jsc/bindings/wrapAnsi.cpp     | 115 +++++++++++++++++++++++++-------------
 test/js/bun/util/wrapAnsi.test.ts | 106 +++++++++++++++++++++++++++++++++++
 2 files changed, 181 insertions(+), 40 deletions(-)
```

</details>

**gate history** · 2 passed · 0 rejected · iteration 0

<details><summary>evidence per changed file</summary>

```
file                               reads  edits  tests
src/jsc/bindings/wrapAnsi.cpp          4     10     15
test/js/bun/util/wrapAnsi.test.ts      7      4     15
```

</details>

<!-- robobun:evidence:end -->
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.

3 participants