Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 49 additions & 10 deletions packages/core/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import cliWidth from 'cli-width';
import wrapAnsi from 'wrap-ansi';
import { readline } from './hook-engine.ts';

/**
* Force line returns at specific width. This function is ANSI code friendly and it'll
* ignore invisible codes during width calculation.
Expand All @@ -10,14 +8,55 @@ import { readline } from './hook-engine.ts';
* @return {string}
*/
export function breakLines(content: string, width: number): string {
Copy link
Owner

Choose a reason for hiding this comment

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

One thing here that isn't clear anymore about this approach;

We were talking of how the bug reported seemed to be an issue with how different terminal auto-wrap content and how that conflicted with the cursor position and the way wrap-ansi would do the wrapping.

Given that, it's unclear why a different wrapping algorithm fix this issue. This would fix it for terminals doing simple wraps, but won't it now break for those who're maybe implementing word wrapping instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

By implementing character-based wrapping directly in the code rather than relying on wrap-ansi we Make wrapping deterministic, so the exact same wrapping happens on all terminals. So since we control the wrapping, we can accurately calculate cursor positions.

Now a terminal's auto-wrap behavior becomes irrelevant because we've already wrapped the content at exactly width characters, and the terminal receives pre-wrapped lines that fit within its display width. So even if a terminal has word-wrapping enabled, it only applies to content that exceeds the width, which ours won't

Example

Terminal A (word-wrap): "This is a long message"
Terminal B (char-wrap): "This is a long message"

using wrap-ansi:
- Terminal A might wrap: "This is a long" / "message"
- Terminal B might wrap: "This is a lo" / "ng message"
- Cursor calculation broken because we don't know which one we're on!

character-based:
- All terminals receive: "This is a lo" / "ng message"
- Both terminals see it's already wrapped
- Cursor calculation is accurate because we control the wrapping!

return content
.split('\n')
.flatMap((line) =>
wrapAnsi(line, width, { trim: false, hard: true })
.split('\n')
.map((str) => str.trimEnd()),
)
.join('\n');
const lines = content.split('\n');
const result: string[] = [];

for (const line of lines) {
let currentLine = '';
let visibleLength = 0;
let escapeSequence = '';
let inEscape = false;

for (const char of line) {
// Detect start of ANSI escape code
if (char === '\x1b') {
inEscape = true;
escapeSequence += char;
continue;
}

// If inside an escape sequence, accumulate it but don't count width
if (inEscape) {
escapeSequence += char;

// ANSI escape sequences always end with a letter (eg., 'm' in '\x1b[31m')
// This marks the end of the sequence so we can append it without counting its width
if (/[a-zA-Z]/.test(char)) {
inEscape = false;
currentLine += escapeSequence;
escapeSequence = '';
}
continue;
}

// Normal character: Add to line and increment visual width
currentLine += char;
visibleLength++;

// Hard Wrap: If we reached the width limit
if (visibleLength === width) {
result.push(currentLine);
currentLine = '';
visibleLength = 0;
}
}

if (currentLine.length > 0 || result.length === 0) {
result.push(currentLine.trimEnd());
}
}

return result.join('\n');
}

/**
Expand Down