* Write a single line's characters into the screen buffer. * Extracted from Output.get() so JSC can optimize this tight, * monomorphic loop independently — better register allocation, * setCellAt inlining, and type feedback than when buried inside * a 300-line dispatch function. * * Returns th
( screen: Screen, line: string, x: number, y: number, screenWidth: number, stylePool: StylePool, charCache: Map<string, ClusteredChar[]>, )
| 631 | * line via stringWidth(). Caller computes the debug cell-count as end-x. |
| 632 | */ |
| 633 | function writeLineToScreen( |
| 634 | screen: Screen, |
| 635 | line: string, |
| 636 | x: number, |
| 637 | y: number, |
| 638 | screenWidth: number, |
| 639 | stylePool: StylePool, |
| 640 | charCache: Map<string, ClusteredChar[]>, |
| 641 | ): number { |
| 642 | let characters = charCache.get(line) |
| 643 | if (!characters) { |
| 644 | characters = reorderBidi( |
| 645 | styledCharsWithGraphemeClustering( |
| 646 | styledCharsFromTokens(tokenize(line)), |
| 647 | stylePool, |
| 648 | ), |
| 649 | ) |
| 650 | charCache.set(line, characters) |
| 651 | } |
| 652 | |
| 653 | let offsetX = x |
| 654 | |
| 655 | for (let charIdx = 0; charIdx < characters.length; charIdx++) { |
| 656 | const character = characters[charIdx]! |
| 657 | const codePoint = character.value.codePointAt(0) |
| 658 | |
| 659 | // Handle C0 control characters (0x00-0x1F) that cause cursor movement |
| 660 | // mismatches. stringWidth treats these as width 0, but terminals may |
| 661 | // move the cursor differently. |
| 662 | if (codePoint !== undefined && codePoint <= 0x1f) { |
| 663 | // Tab (0x09): expand to spaces to reach next tab stop |
| 664 | if (codePoint === 0x09) { |
| 665 | const tabWidth = 8 |
| 666 | const spacesToNextStop = tabWidth - (offsetX % tabWidth) |
| 667 | for (let i = 0; i < spacesToNextStop && offsetX < screenWidth; i++) { |
| 668 | setCellAt(screen, offsetX, y, { |
| 669 | char: ' ', |
| 670 | styleId: stylePool.none, |
| 671 | width: CellWidth.Narrow, |
| 672 | hyperlink: undefined, |
| 673 | }) |
| 674 | offsetX++ |
| 675 | } |
| 676 | } |
| 677 | // ESC (0x1B): skip incomplete escape sequences that ansi-tokenize |
| 678 | // didn't recognize. ansi-tokenize only parses SGR sequences (ESC[...m) |
| 679 | // and OSC 8 hyperlinks (ESC]8;;url BEL). Other sequences like cursor |
| 680 | // movement, screen clearing, or terminal title become individual char |
| 681 | // tokens that we need to skip here. |
| 682 | else if (codePoint === 0x1b) { |
| 683 | const nextChar = characters[charIdx + 1]?.value |
| 684 | const nextCode = nextChar?.codePointAt(0) |
| 685 | if ( |
| 686 | nextChar === '(' || |
| 687 | nextChar === ')' || |
| 688 | nextChar === '*' || |
| 689 | nextChar === '+' |
| 690 | ) { |
no test coverage detected