| 41 | // Wrap a long word across multiple rows |
| 42 | // Ansi escape codes do not count towards length |
| 43 | function wrapWord(rows: string[], word: string, columns: number, indent: string) { |
| 44 | const characters = [...word] |
| 45 | |
| 46 | let isInsideEscape = false |
| 47 | let isInsideLinkEscape = false |
| 48 | let visible = stringWidth(stripAnsi(rows.at(-1)!)) |
| 49 | |
| 50 | const indentLength = stringWidth(indent) |
| 51 | |
| 52 | for (const [index, character] of characters.entries()) { |
| 53 | const characterLength = stringWidth(character) |
| 54 | |
| 55 | if (visible + characterLength <= columns) { |
| 56 | rows[rows.length - 1] += character |
| 57 | } else { |
| 58 | rows.push(`${indent}${character}`) |
| 59 | visible = indentLength |
| 60 | } |
| 61 | |
| 62 | if (ESCAPES.has(character)) { |
| 63 | isInsideEscape = true |
| 64 | |
| 65 | const ansiEscapeLinkCandidate = characters |
| 66 | .slice(index + 1, index + 1 + ANSI_ESCAPE_LINK.length) |
| 67 | .join('') |
| 68 | isInsideLinkEscape = ansiEscapeLinkCandidate === ANSI_ESCAPE_LINK |
| 69 | } |
| 70 | |
| 71 | if (isInsideEscape) { |
| 72 | if (isInsideLinkEscape) { |
| 73 | if (character === ANSI_ESCAPE_BELL) { |
| 74 | isInsideEscape = false |
| 75 | isInsideLinkEscape = false |
| 76 | } |
| 77 | } else if (character === ANSI_SGR_TERMINATOR) { |
| 78 | isInsideEscape = false |
| 79 | } |
| 80 | |
| 81 | continue |
| 82 | } |
| 83 | |
| 84 | visible += characterLength |
| 85 | |
| 86 | if (visible === columns && index < characters.length - 1) { |
| 87 | rows.push(indent) |
| 88 | visible = indentLength |
| 89 | } |
| 90 | } |
| 91 | |
| 92 | // It's possible that the last row we copy over is only |
| 93 | // ansi escape characters, handle this edge-case |
| 94 | if (!visible && rows.at(-1)!.length > 0 && rows.length > 1) { |
| 95 | rows[rows.length - 2] += rows.pop() |
| 96 | } |
| 97 | } |
| 98 | |
| 99 | // Trims spaces from a string ignoring invisible sequences |
| 100 | function stringVisibleTrimSpacesRight(string: string) { |