( logicalLines: string[], logicalCursor: [number, number], viewportWidth: number, )
| 673 | |
| 674 | // Helper to calculate visual lines and map cursor positions |
| 675 | function calculateVisualLayout( |
| 676 | logicalLines: string[], |
| 677 | logicalCursor: [number, number], |
| 678 | viewportWidth: number, |
| 679 | ): { |
| 680 | visualLines: string[]; |
| 681 | visualCursor: [number, number]; |
| 682 | logicalToVisualMap: Array<Array<[number, number]>>; // For each logical line, an array of [visualLineIndex, startColInLogical] |
| 683 | visualToLogicalMap: Array<[number, number]>; // For each visual line, its [logicalLineIndex, startColInLogical] |
| 684 | } { |
| 685 | const visualLines: string[] = []; |
| 686 | const logicalToVisualMap: Array<Array<[number, number]>> = []; |
| 687 | const visualToLogicalMap: Array<[number, number]> = []; |
| 688 | let currentVisualCursor: [number, number] = [0, 0]; |
| 689 | |
| 690 | logicalLines.forEach((logLine, logIndex) => { |
| 691 | logicalToVisualMap[logIndex] = []; |
| 692 | if (logLine.length === 0) { |
| 693 | // Handle empty logical line |
| 694 | logicalToVisualMap[logIndex].push([visualLines.length, 0]); |
| 695 | visualToLogicalMap.push([logIndex, 0]); |
| 696 | visualLines.push(''); |
| 697 | if (logIndex === logicalCursor[0] && logicalCursor[1] === 0) { |
| 698 | currentVisualCursor = [visualLines.length - 1, 0]; |
| 699 | } |
| 700 | } else { |
| 701 | // Non-empty logical line |
| 702 | let currentPosInLogLine = 0; // Tracks position within the current logical line (code point index) |
| 703 | const codePointsInLogLine = toCodePoints(logLine); |
| 704 | |
| 705 | while (currentPosInLogLine < codePointsInLogLine.length) { |
| 706 | let currentChunk = ''; |
| 707 | let currentChunkVisualWidth = 0; |
| 708 | let numCodePointsInChunk = 0; |
| 709 | let lastWordBreakPoint = -1; // Index in codePointsInLogLine for word break |
| 710 | let numCodePointsAtLastWordBreak = 0; |
| 711 | |
| 712 | // Iterate through code points to build the current visual line (chunk) |
| 713 | for (let i = currentPosInLogLine; i < codePointsInLogLine.length; i++) { |
| 714 | const char = codePointsInLogLine[i]; |
| 715 | const charVisualWidth = stringWidth(char); |
| 716 | |
| 717 | if (currentChunkVisualWidth + charVisualWidth > viewportWidth) { |
| 718 | // Character would exceed viewport width |
| 719 | if ( |
| 720 | lastWordBreakPoint !== -1 && |
| 721 | numCodePointsAtLastWordBreak > 0 && |
| 722 | currentPosInLogLine + numCodePointsAtLastWordBreak < i |
| 723 | ) { |
| 724 | // We have a valid word break point to use, and it's not the start of the current segment |
| 725 | currentChunk = codePointsInLogLine |
| 726 | .slice( |
| 727 | currentPosInLogLine, |
| 728 | currentPosInLogLine + numCodePointsAtLastWordBreak, |
| 729 | ) |
| 730 | .join(''); |
| 731 | numCodePointsInChunk = numCodePointsAtLastWordBreak; |
| 732 | } else { |
no test coverage detected