Something went wrong. Try again.
source dump of claude code forked from oppi.li/claude-code
Something went wrong. Try again.
26 kB · 797 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798import { type AnsiCode, type StyledChar, styledCharsFromTokens, tokenize,} from '@alcalzone/ansi-tokenize'import { logForDebugging } from '../utils/debug.js'import { getGraphemeSegmenter } from '../utils/intl.js'import sliceAnsi from '../utils/sliceAnsi.js'import { reorderBidi } from './bidi.js'import { type Rectangle, unionRect } from './layout/geometry.js'import { blitRegion, CellWidth, extractHyperlinkFromStyles, filterOutHyperlinkStyles, markNoSelectRegion, OSC8_PREFIX, resetScreen, type Screen, type StylePool, setCellAt, shiftRows,} from './screen.js'import { stringWidth } from './stringWidth.js'import { widestLine } from './widest-line.js'
/** * A grapheme cluster with precomputed terminal width, styleId, and hyperlink. * Built once per unique line (cached via charCache), so the per-char hot loop * is just property reads + setCellAt — no stringWidth, no style interning, * no hyperlink extraction per frame. * * styleId is safe to cache: StylePool is session-lived (never reset). * hyperlink is stored as a string (not interned ID) since hyperlinkPool * resets every 5 min; setCellAt interns it per-frame (cheap Map.get). */type ClusteredChar = { value: string width: number styleId: number hyperlink: string | undefined}
/** * Collects write/blit/clear/clip operations from the render tree, then * applies them to a Screen buffer in `get()`. The Screen is what gets * diffed against the previous frame to produce terminal updates. */
type Options = { width: number height: number stylePool: StylePool /** * Screen to render into. Will be reset before use. * For double-buffering, pass a reusable screen. Otherwise create a new one. */ screen: Screen}
export type Operation = | WriteOperation | ClipOperation | UnclipOperation | BlitOperation | ClearOperation | NoSelectOperation | ShiftOperation
type WriteOperation = { type: 'write' x: number y: number text: string /** * Per-line soft-wrap flags, parallel to text.split('\n'). softWrap[i]=true * means line i is a continuation of line i-1 (the `\n` before it was * inserted by word-wrap, not in the source). Index 0 is always false. * Undefined means the producer didn't track wrapping (e.g. fills, * raw-ansi) — the screen's per-row bitmap is left untouched. */ softWrap?: boolean[]}
type ClipOperation = { type: 'clip' clip: Clip}
export type Clip = { x1: number | undefined x2: number | undefined y1: number | undefined y2: number | undefined}
/** * Intersect two clips. `undefined` on an axis means unbounded; the other * clip's bound wins. If both are bounded, take the tighter constraint * (max of mins, min of maxes). If the resulting region is empty * (x1 >= x2 or y1 >= y2), writes clipped by it will be dropped. */function intersectClip(parent: Clip | undefined, child: Clip): Clip { if (!parent) return child return { x1: maxDefined(parent.x1, child.x1), x2: minDefined(parent.x2, child.x2), y1: maxDefined(parent.y1, child.y1), y2: minDefined(parent.y2, child.y2), }}
function maxDefined( a: number | undefined, b: number | undefined,): number | undefined { if (a === undefined) return b if (b === undefined) return a return Math.max(a, b)}
function minDefined( a: number | undefined, b: number | undefined,): number | undefined { if (a === undefined) return b if (b === undefined) return a return Math.min(a, b)}
type UnclipOperation = { type: 'unclip'}
type BlitOperation = { type: 'blit' src: Screen x: number y: number width: number height: number}
type ShiftOperation = { type: 'shift' top: number bottom: number n: number}
type ClearOperation = { type: 'clear' region: Rectangle /** * Set when the clear is for an absolute-positioned node's old bounds. * Absolute nodes overlay normal-flow siblings, so their stale paint is * what an earlier sibling's clean-subtree blit wrongly restores from * prevScreen. Normal-flow siblings' clears don't have this problem — * their old position can't have been painted on top of a sibling. */ fromAbsolute?: boolean}
type NoSelectOperation = { type: 'noSelect' region: Rectangle}
export default class Output { width: number height: number private readonly stylePool: StylePool private screen: Screen
private readonly operations: Operation[] = []
private charCache: Map<string, ClusteredChar[]> = new Map()
constructor(options: Options) { const { width, height, stylePool, screen } = options
this.width = width this.height = height this.stylePool = stylePool this.screen = screen
resetScreen(screen, width, height) }
/** * Reuse this Output for a new frame. Zeroes the screen buffer, clears * the operation list (backing storage is retained), and caps charCache * growth. Preserving charCache across frames is the main win — most * lines don't change between renders, so tokenize + grapheme clustering * becomes a cache hit. */ reset(width: number, height: number, screen: Screen): void { this.width = width this.height = height this.screen = screen this.operations.length = 0 resetScreen(screen, width, height) if (this.charCache.size > 16384) this.charCache.clear() }
/** * Copy cells from a source screen region (blit = block image transfer). */ blit(src: Screen, x: number, y: number, width: number, height: number): void { this.operations.push({ type: 'blit', src, x, y, width, height }) }
/** * Shift full-width rows within [top, bottom] by n. n > 0 = up. Mirrors * what DECSTBM + SU/SD does to the terminal. Paired with blit() to reuse * prevScreen content during pure scroll, avoiding full child re-render. */ shift(top: number, bottom: number, n: number): void { this.operations.push({ type: 'shift', top, bottom, n }) }
/** * Clear a region by writing empty cells. Used when a node shrinks to * ensure stale content from the previous frame is removed. */ clear(region: Rectangle, fromAbsolute?: boolean): void { this.operations.push({ type: 'clear', region, fromAbsolute }) }
/** * Mark a region as non-selectable (excluded from fullscreen text * selection copy + highlight). Used by <NoSelect> to fence off * gutters (line numbers, diff sigils). Applied AFTER blit/write so * the mark wins regardless of what's blitted into the region. */ noSelect(region: Rectangle): void { this.operations.push({ type: 'noSelect', region }) }
write(x: number, y: number, text: string, softWrap?: boolean[]): void { if (!text) { return }
this.operations.push({ type: 'write', x, y, text, softWrap, }) }
clip(clip: Clip) { this.operations.push({ type: 'clip', clip, }) }
unclip() { this.operations.push({ type: 'unclip', }) }
get(): Screen { const screen = this.screen const screenWidth = this.width const screenHeight = this.height
// Track blit vs write cell counts for debugging let blitCells = 0 let writeCells = 0
// Pass 1: expand damage to cover clear regions. The buffer is freshly // zeroed by resetScreen, so this pass only marks damage so diff() // checks these regions against the previous frame. // // Also collect clears from absolute-positioned nodes. An absolute // node overlays normal-flow siblings; when it shrinks, its clear is // pushed AFTER those siblings' clean-subtree blits (DOM order). The // blit copies the absolute node's own stale paint from prevScreen, // and since clear is damage-only, the ghost survives diff. Normal- // flow clears don't need this — a normal-flow node's old position // can't have been painted on top of a sibling's current position. const absoluteClears: Rectangle[] = [] for (const operation of this.operations) { if (operation.type !== 'clear') continue const { x, y, width, height } = operation.region const startX = Math.max(0, x) const startY = Math.max(0, y) const maxX = Math.min(x + width, screenWidth) const maxY = Math.min(y + height, screenHeight) if (startX >= maxX || startY >= maxY) continue const rect = { x: startX, y: startY, width: maxX - startX, height: maxY - startY, } screen.damage = screen.damage ? unionRect(screen.damage, rect) : rect if (operation.fromAbsolute) absoluteClears.push(rect) }
const clips: Clip[] = []
for (const operation of this.operations) { switch (operation.type) { case 'clear': // handled in pass 1 continue
case 'clip': // Intersect with the parent clip (if any) so nested // overflow:hidden boxes can't write outside their ancestor's // clip region. Without this, a message with overflow:hidden at // the bottom of a scrollbox pushes its OWN clip (based on its // layout bounds, already translated by -scrollTop) which can // extend below the scrollbox viewport — writes escape into // the sibling bottom section's rows. clips.push(intersectClip(clips.at(-1), operation.clip)) continue
case 'unclip': clips.pop() continue
case 'blit': { // Bulk-copy cells from source screen region using TypedArray.set(). // Tracking damage ensures diff() checks blitted cells for stale content // when a parent blits an area that previously contained child content. const { src, x: regionX, y: regionY, width: regionWidth, height: regionHeight, } = operation // Intersect with active clip — a child's clean-blit passes its full // cached rect, but the parent ScrollBox may have shrunk (pill mount). // Without this, the blit writes past the ScrollBox's new bottom edge // into the pill's row. const clip = clips.at(-1) const startX = Math.max(regionX, clip?.x1 ?? 0) const startY = Math.max(regionY, clip?.y1 ?? 0) const maxY = Math.min( regionY + regionHeight, screenHeight, src.height, clip?.y2 ?? Infinity, ) const maxX = Math.min( regionX + regionWidth, screenWidth, src.width, clip?.x2 ?? Infinity, ) if (startX >= maxX || startY >= maxY) continue // Skip rows covered by an absolute-positioned node's clear. // Absolute nodes overlay normal-flow siblings, so prevScreen in // that region holds the absolute node's stale paint — blitting // it back would ghost. See absoluteClears collection above. if (absoluteClears.length === 0) { blitRegion(screen, src, startX, startY, maxX, maxY) blitCells += (maxY - startY) * (maxX - startX) continue } let rowStart = startY for (let row = startY; row <= maxY; row++) { const excluded = row < maxY && absoluteClears.some( r => row >= r.y && row < r.y + r.height && startX >= r.x && maxX <= r.x + r.width, ) if (excluded || row === maxY) { if (row > rowStart) { blitRegion(screen, src, startX, rowStart, maxX, row) blitCells += (row - rowStart) * (maxX - startX) } rowStart = row + 1 } } continue }
case 'shift': { shiftRows(screen, operation.top, operation.bottom, operation.n) continue }
case 'write': { const { text, softWrap } = operation let { x, y } = operation let lines = text.split('\n') let swFrom = 0 let prevContentEnd = 0
const clip = clips.at(-1)
if (clip) { const clipHorizontally = typeof clip?.x1 === 'number' && typeof clip?.x2 === 'number'
const clipVertically = typeof clip?.y1 === 'number' && typeof clip?.y2 === 'number'
// If text is positioned outside of clipping area altogether, // skip to the next operation to avoid unnecessary calculations if (clipHorizontally) { const width = widestLine(text)
if (x + width <= clip.x1! || x >= clip.x2!) { continue } }
if (clipVertically) { const height = lines.length
if (y + height <= clip.y1! || y >= clip.y2!) { continue } }
if (clipHorizontally) { lines = lines.map(line => { const from = x < clip.x1! ? clip.x1! - x : 0 const width = stringWidth(line) const to = x + width > clip.x2! ? clip.x2! - x : width let sliced = sliceAnsi(line, from, to) // Wide chars (CJK, emoji) occupy 2 cells. When `to` lands // on the first cell of a wide char, sliceAnsi includes the // entire glyph and the result overflows clip.x2 by one cell, // writing a SpacerTail into the adjacent sibling. Re-slice // one cell earlier; wide chars are exactly 2 cells, so a // single retry always fits. if (stringWidth(sliced) > to - from) { sliced = sliceAnsi(line, from, to - 1) } return sliced })
if (x < clip.x1!) { x = clip.x1! } }
if (clipVertically) { const from = y < clip.y1! ? clip.y1! - y : 0 const height = lines.length const to = y + height > clip.y2! ? clip.y2! - y : height
// If the first visible line is a soft-wrap continuation, we // need the clipped previous line's content end so // screen.softWrap[lineY] correctly records the join point // even though that line's cells were never written. if (softWrap && from > 0 && softWrap[from] === true) { prevContentEnd = x + stringWidth(lines[from - 1]!) }
lines = lines.slice(from, to) swFrom = from
if (y < clip.y1!) { y = clip.y1! } } }
const swBits = screen.softWrap let offsetY = 0
for (const line of lines) { const lineY = y + offsetY // Line can be outside screen if `text` is taller than screen height if (lineY >= screenHeight) { break } const contentEnd = writeLineToScreen( screen, line, x, lineY, screenWidth, this.stylePool, this.charCache, ) writeCells += contentEnd - x // See Screen.softWrap docstring for the encoding. contentEnd // from writeLineToScreen is tab-expansion-aware, unlike // x+stringWidth(line) which treats tabs as width 0. if (softWrap) { const isSW = softWrap[swFrom + offsetY] === true swBits[lineY] = isSW ? prevContentEnd : 0 prevContentEnd = contentEnd } offsetY++ } continue } } }
// noSelect ops go LAST so they win over blits (which copy noSelect // from prevScreen) and writes (which don't touch noSelect). This way // a <NoSelect> box correctly fences its region even when the parent // blits, and moving a <NoSelect> between frames correctly clears the // old region (resetScreen already zeroed the bitmap). for (const operation of this.operations) { if (operation.type === 'noSelect') { const { x, y, width, height } = operation.region markNoSelectRegion(screen, x, y, width, height) } }
// Log blit/write ratio for debugging - high write count suggests blitting isn't working const totalCells = blitCells + writeCells if (totalCells > 1000 && writeCells > blitCells) { logForDebugging( `High write ratio: blit=${blitCells}, write=${writeCells} (${((writeCells / totalCells) * 100).toFixed(1)}% writes), screen=${screenHeight}x${screenWidth}`, ) }
return screen }}
function stylesEqual(a: AnsiCode[], b: AnsiCode[]): boolean { if (a === b) return true // Reference equality fast path const len = a.length if (len !== b.length) return false if (len === 0) return true // Both empty for (let i = 0; i < len; i++) { if (a[i]!.code !== b[i]!.code) return false } return true}
/** * Convert a string with ANSI codes into styled characters with proper grapheme * clustering. Fixes ansi-tokenize splitting grapheme clusters (like family * emojis) into individual code points. * * Also precomputes styleId + hyperlink per style run (not per char) — an * 80-char line with 3 style runs does 3 intern calls instead of 80. */function styledCharsWithGraphemeClustering( chars: StyledChar[], stylePool: StylePool,): ClusteredChar[] { const charCount = chars.length if (charCount === 0) return []
const result: ClusteredChar[] = [] const bufferChars: string[] = [] let bufferStyles: AnsiCode[] = chars[0]!.styles
for (let i = 0; i < charCount; i++) { const char = chars[i]! const styles = char.styles
// Different styles means we need to flush and start new buffer if (bufferChars.length > 0 && !stylesEqual(styles, bufferStyles)) { flushBuffer(bufferChars.join(''), bufferStyles, stylePool, result) bufferChars.length = 0 }
bufferChars.push(char.value) bufferStyles = styles }
// Final flush if (bufferChars.length > 0) { flushBuffer(bufferChars.join(''), bufferStyles, stylePool, result) }
return result}
function flushBuffer( buffer: string, styles: AnsiCode[], stylePool: StylePool, out: ClusteredChar[],): void { // Compute styleId + hyperlink ONCE for the whole style run. // Every grapheme in this buffer shares the same styles. // // Extract and track hyperlinks separately, filter from styles. // Always check for OSC 8 codes to filter, not just when a URL is // extracted. The tokenizer treats OSC 8 close codes (empty URL) as // active styles, so they must be filtered even when no hyperlink // URL is present. const hyperlink = extractHyperlinkFromStyles(styles) ?? undefined const hasOsc8Styles = hyperlink !== undefined || styles.some( s => s.code.length >= OSC8_PREFIX.length && s.code.startsWith(OSC8_PREFIX), ) const filteredStyles = hasOsc8Styles ? filterOutHyperlinkStyles(styles) : styles const styleId = stylePool.intern(filteredStyles)
for (const { segment: grapheme } of getGraphemeSegmenter().segment(buffer)) { out.push({ value: grapheme, width: stringWidth(grapheme), styleId, hyperlink, }) }}
/** * 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 the end column (x + visual width, including tab expansion) so * the caller can record it in screen.softWrap without re-walking the * line via stringWidth(). Caller computes the debug cell-count as end-x. */function writeLineToScreen( screen: Screen, line: string, x: number, y: number, screenWidth: number, stylePool: StylePool, charCache: Map<string, ClusteredChar[]>,): number { let characters = charCache.get(line) if (!characters) { characters = reorderBidi( styledCharsWithGraphemeClustering( styledCharsFromTokens(tokenize(line)), stylePool, ), ) charCache.set(line, characters) }
let offsetX = x
for (let charIdx = 0; charIdx < characters.length; charIdx++) { const character = characters[charIdx]! const codePoint = character.value.codePointAt(0)
// Handle C0 control characters (0x00-0x1F) that cause cursor movement // mismatches. stringWidth treats these as width 0, but terminals may // move the cursor differently. if (codePoint !== undefined && codePoint <= 0x1f) { // Tab (0x09): expand to spaces to reach next tab stop if (codePoint === 0x09) { const tabWidth = 8 const spacesToNextStop = tabWidth - (offsetX % tabWidth) for (let i = 0; i < spacesToNextStop && offsetX < screenWidth; i++) { setCellAt(screen, offsetX, y, { char: ' ', styleId: stylePool.none, width: CellWidth.Narrow, hyperlink: undefined, }) offsetX++ } } // ESC (0x1B): skip incomplete escape sequences that ansi-tokenize // didn't recognize. ansi-tokenize only parses SGR sequences (ESC[...m) // and OSC 8 hyperlinks (ESC]8;;url BEL). Other sequences like cursor // movement, screen clearing, or terminal title become individual char // tokens that we need to skip here. else if (codePoint === 0x1b) { const nextChar = characters[charIdx + 1]?.value const nextCode = nextChar?.codePointAt(0) if ( nextChar === '(' || nextChar === ')' || nextChar === '*' || nextChar === '+' ) { // Charset selection: ESC ( X, ESC ) X, etc. // Skip the intermediate char and the charset designator charIdx += 2 } else if (nextChar === '[') { // CSI sequence: ESC [ ... final-byte // Final byte is in range 0x40-0x7E (@, A-Z, [\]^_`, a-z, {|}~) // Examples: ESC[2J (clear), ESC[?25l (cursor hide), ESC[H (home) charIdx++ // skip the [ while (charIdx < characters.length - 1) { charIdx++ const c = characters[charIdx]?.value.codePointAt(0) // Final byte terminates the sequence if (c !== undefined && c >= 0x40 && c <= 0x7e) { break } } } else if ( nextChar === ']' || nextChar === 'P' || nextChar === '_' || nextChar === '^' || nextChar === 'X' ) { // String-based sequences terminated by BEL (0x07) or ST (ESC \): // - OSC: ESC ] ... (Operating System Command) // - DCS: ESC P ... (Device Control String) // - APC: ESC _ ... (Application Program Command) // - PM: ESC ^ ... (Privacy Message) // - SOS: ESC X ... (Start of String) charIdx++ // skip the introducer char while (charIdx < characters.length - 1) { charIdx++ const c = characters[charIdx]?.value // BEL (0x07) terminates the sequence if (c === '\x07') { break } // ST (String Terminator) is ESC \ // When we see ESC, check if next char is backslash if (c === '\x1b') { const nextC = characters[charIdx + 1]?.value if (nextC === '\\') { charIdx++ // skip the backslash too break } } } } else if ( nextCode !== undefined && nextCode >= 0x30 && nextCode <= 0x7e ) { // Single-character escape sequences: ESC followed by 0x30-0x7E // (excluding the multi-char introducers already handled above) // - Fp range (0x30-0x3F): ESC 7 (save cursor), ESC 8 (restore) // - Fe range (0x40-0x5F): ESC D (index), ESC M (reverse index) // - Fs range (0x60-0x7E): ESC c (reset) charIdx++ // skip the command char } } // Carriage return (0x0D): would move cursor to column 0, skip it // Backspace (0x08): would move cursor left, skip it // Bell (0x07), vertical tab (0x0B), form feed (0x0C): skip // All other control chars (0x00-0x06, 0x0E-0x1F): skip // Note: newline (0x0A) is already handled by line splitting continue }
// Zero-width characters (combining marks, ZWNJ, ZWS, etc.) // don't occupy terminal cells — storing them as Narrow cells // desyncs the virtual cursor from the real terminal cursor. // Width was computed once during clustering (cached via charCache). const charWidth = character.width if (charWidth === 0) { continue }
const isWideCharacter = charWidth >= 2
// Wide char at last column can't fit — terminal would wrap it to // the next line, desyncing our cursor model. Place a SpacerHead // to mark the blank column, matching terminal behavior. if (isWideCharacter && offsetX + 2 > screenWidth) { setCellAt(screen, offsetX, y, { char: ' ', styleId: stylePool.none, width: CellWidth.SpacerHead, hyperlink: undefined, }) offsetX++ continue }
// styleId + hyperlink were precomputed during clustering (once per // style run, cached via charCache). Hot loop is now just property // reads — no intern, no extract, no filter per frame. setCellAt(screen, offsetX, y, { char: character.value, styleId: character.styleId, width: isWideCharacter ? CellWidth.Wide : CellWidth.Narrow, hyperlink: character.hyperlink, }) offsetX += isWideCharacter ? 2 : 1 }
return offsetX}