From 8afea421b8fec9964eaab47e95e0dcd4afa7b2f8 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Fri, 13 Nov 2026 17:54:09 -0800 Subject: [PATCH] Share terminal editor hooks and allocate sparse state on demand Keep one verified method set per emulator and one resource-local attachment per row. Preserve actual mutation footprints and snapshots while avoiding per-row closures and empty component maps. Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com> --- .../headless-content-origin-lines.ts | 48 +++- .../emulation/headless-content-origins.ts | 241 ++++++++++-------- .../headless-origin-cell-snapshots.ts | 41 +++ .../emulation/headless-origin-line-edits.ts | 239 +++++++++-------- .../emulation/headless-origin-line-hooks.ts | 102 ++++++++ .../emulation/headless-origin-print-hooks.ts | 48 ---- .../testing/headless-emulator.test.ts | 2 +- 7 files changed, 449 insertions(+), 272 deletions(-) create mode 100644 src/main/daemon/emulation/headless-origin-cell-snapshots.ts create mode 100644 src/main/daemon/emulation/headless-origin-line-hooks.ts delete mode 100644 src/main/daemon/emulation/headless-origin-print-hooks.ts diff --git a/src/main/daemon/emulation/headless-content-origin-lines.ts b/src/main/daemon/emulation/headless-content-origin-lines.ts index 0d884e7eec2..83c454f699c 100644 --- a/src/main/daemon/emulation/headless-content-origin-lines.ts +++ b/src/main/daemon/emulation/headless-content-origin-lines.ts @@ -68,14 +68,47 @@ export function originTerminalCore(terminal: Terminal): OriginTerminalCore | und export type OriginLineState = { runs: TerminalOriginRuns - components: Map + components?: Map +} + +export type OriginLineMethods = Pick< + OriginBufferLine, + | 'loadCell' + | 'setCell' + | 'setCellFromCodepoint' + | 'addCodepointToCell' + | 'set' + | 'insertCells' + | 'deleteCells' + | 'replaceCells' + | 'fill' + | 'resize' + | 'copyFrom' + | 'clone' + | 'copyCellsFrom' +> +export type OriginLineAttachment = OriginLineState & { + owner: object + originals: OriginLineMethods + editing: boolean + recordSets: boolean + erased?: { start: number; end: number }[] +} +const ORIGIN_LINE_ATTACHMENT = Symbol('terminalContentOrigin') + +export function readOriginLineAttachment(line: OriginBufferLine): OriginLineAttachment | undefined { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only this module owns the private editor attachment symbol. + return (line as OriginBufferLine & { [ORIGIN_LINE_ATTACHMENT]?: OriginLineAttachment })[ + ORIGIN_LINE_ATTACHMENT + ] +} + +export function attachOriginLineState(line: OriginBufferLine, state: OriginLineAttachment): void { + Object.defineProperty(line, ORIGIN_LINE_ATTACHMENT, { value: state, configurable: true }) } /** Build coordinates from xterm's own translation, including wide and combined glyphs. */ -export function readOriginScreenLines( - terminal: Terminal, - states: WeakMap -): string[] { +export function readOriginScreenLines(terminal: Terminal, owner: object): string[] { const core = originTerminalCore(terminal) const buffer = terminal.buffer.active const lines: string[] = [] @@ -84,7 +117,8 @@ export function readOriginScreenLines( const line = core?.buffers.active.lines.get(index) const columns: number[] = [] const text = line?.translateToString(true, 0, undefined, columns) ?? '' - const state = line ? states.get(line) : undefined + const attached = line ? readOriginLineAttachment(line) : undefined + const state = attached?.owner === owner ? attached : undefined const runs: TerminalOriginRun[] = [] const append = (start: number, end: number, origin: TerminalContentOrigin): void => { const previous = runs.at(-1) @@ -106,7 +140,7 @@ export function readOriginScreenLines( while (end < text.length && columns[end] === column) { end++ } - const component = state?.components.get(column) + const component = state?.components?.get(column) if (component) { for (const run of component) { append(at + run.start, Math.min(end, at + run.end), run.origin) diff --git a/src/main/daemon/emulation/headless-content-origins.ts b/src/main/daemon/emulation/headless-content-origins.ts index 7263abb7fbd..40120d58f31 100644 --- a/src/main/daemon/emulation/headless-content-origins.ts +++ b/src/main/daemon/emulation/headless-content-origins.ts @@ -1,6 +1,7 @@ import type { Terminal } from '@xterm/headless' +import { HeadlessOriginCellSnapshots } from './headless-origin-cell-snapshots' import { HeadlessOriginBuffers } from './headless-origin-buffers' -import { HeadlessOriginPrintHooks } from './headless-origin-print-hooks' +import { captureOriginLineMethods, createOriginLineHooks } from './headless-origin-line-hooks' import { HeadlessOriginLineEdits, replaceOriginCells } from './headless-origin-line-edits' import { terminalOriginAt, @@ -11,6 +12,10 @@ import { } from '../../../shared/terminal/output/terminal-content-origin' import { readOriginScreenLines, + readOriginLineAttachment, + attachOriginLineState, + type OriginLineAttachment, + type OriginLineMethods, type OriginBufferLine, type OriginCell, type OriginLineState, @@ -19,19 +24,39 @@ import { /** Certificates follow the editor's actual line identities; no second screen or global patch. */ export class HeadlessContentOrigins { - private readonly lines = new WeakMap() - private readonly cells = new WeakMap< - OriginCell, - { - runs: TerminalOriginRuns - index: number - component?: TerminalOriginRuns - } - >() - private readonly printHooks = new HeadlessOriginPrintHooks( - (line, index, width, code) => this.printed(line, index, width, code), - (line, index, code, width, combine) => this.combined(line, index, code, width, combine) + private readonly cells = new HeadlessOriginCellSnapshots((line) => this.state(line)) + private sharedOriginals?: OriginLineMethods + private readonly edits = new HeadlessOriginLineEdits( + (line) => this.state(line), + (line, state) => this.assign(line, state) ) + private readonly hooks = createOriginLineHooks({ + loadCell: (line, index, cell) => this.cells.load(line, index, cell), + setCell: (line, index, cell) => this.setCell(line, index, cell), + setCellFromCodepoint: (line, index, code, width, attrs) => { + const state = readOriginLineAttachment(line)! + state.originals.setCellFromCodepoint.call(line, index, code, width, attrs) + if (!this.edits.collect(state, index, true)) { + this.printed(line, index, width, code) + } + }, + addCodepointToCell: (line, index, code, width) => this.combined(line, index, code, width), + set: (line, index, cell) => { + readOriginLineAttachment(line)!.originals.set.call(line, index, cell) + this.writeCell(line, index) + }, + insertCells: (line, position, count, fill) => this.edits.insert(line, position, count, fill), + deleteCells: (line, position, count, fill) => this.edits.remove(line, position, count, fill), + replaceCells: (line, start, end, fill, protect) => + this.edits.replace(line, start, end, fill, protect), + fill: (line, cell, protect) => this.edits.fill(line, cell, protect), + resize: (line, cols, cell) => this.edits.resize(line, cols, cell), + copyFrom: (line, source, blank) => this.copyFrom(line, source, blank), + clone: (line, blank) => this.clone(line, blank), + copyCellsFrom: (line, source, from, to, count, reverse) => + this.copyCells(line, source, from, to, count, reverse) + }) + private readonly requiredHooks = (({ set: _set, ...hooks }) => hooks)(this.hooks) private readonly attachments: HeadlessOriginBuffers private pending?: { line: OriginBufferLine @@ -72,7 +97,7 @@ export class HeadlessContentOrigins { ) { return cached.lines } - const lines = readOriginScreenLines(this.terminal, this.lines) + const lines = readOriginScreenLines(this.terminal, this) this.cached = { revision: this.revision, scope, lines } return lines } @@ -82,14 +107,16 @@ export class HeadlessContentOrigins { this.attachments.attach() } - private state(line: OriginBufferLine): OriginLineState { + private state(line: OriginBufferLine): OriginLineAttachment { this.flush() this.attachLine(line) - return this.lines.get(line)! + return readOriginLineAttachment(line)! } private assign(line: OriginBufferLine, state: OriginLineState): void { - this.lines.set(line, state) + const attached = readOriginLineAttachment(line)! + attached.runs = state.runs + attached.components = state.components?.size ? state.components : undefined this.revision++ } @@ -99,9 +126,9 @@ export class HeadlessContentOrigins { return } this.pending = undefined - const state = this.lines.get(pending.line)! + const state = readOriginLineAttachment(pending.line)! const components = state.components - if (components.size) { + if (components?.size) { for (let index = pending.start; index < pending.end; index++) { components.delete(index) } @@ -131,13 +158,8 @@ export class HeadlessContentOrigins { } } - private combined( - line: OriginBufferLine, - index: number, - code: number, - width: number, - combine: OriginBufferLine['addCodepointToCell'] - ): void { + private combined(line: OriginBufferLine, index: number, code: number, width: number): void { + const combine = readOriginLineAttachment(line)!.originals.addCodepointToCell const pending = this.pending if ( pending?.line === line && @@ -151,7 +173,7 @@ export class HeadlessContentOrigins { const state = this.state(line) const baseOrigin = terminalOriginAt(state.runs, index) if ( - !state.components.has(index) && + !state.components?.has(index) && (baseOrigin === this.origin || (baseOrigin && this.origin && @@ -164,10 +186,10 @@ export class HeadlessContentOrigins { } const before = line.getString(index) const base = - state.components.get(index) ?? + state.components?.get(index) ?? terminalOriginSpan(before.length, terminalOriginAt(state.runs, index)) combine.call(line, index, code, width) - const components = state.components + let components = state.components const after = line.getString(index) const joined = writeTerminalOrigins( base, @@ -179,109 +201,106 @@ export class HeadlessContentOrigins { ? joined.length === 1 && joined[0]!.start === 0 && joined[0]!.end === after.length : joined.length === 0 if (uniform) { - components.delete(index) + components?.delete(index) } else { + components ??= new Map() components.set(index, joined) } this.assign(line, { runs: state.runs, components }) } - private writeCell(line: OriginBufferLine, index: number, state?: OriginLineState): void { + private writeCell( + line: OriginBufferLine, + index: number, + runs?: TerminalOriginRuns, + component?: TerminalOriginRuns + ): void { const previous = this.state(line) - const components = previous.components - const copied = state?.components.get(0) - components.delete(index) - if (copied) { - components.set(index, copied) + let components = previous.components + components?.delete(index) + if (component) { + components ??= new Map() + components.set(index, component) } this.assign(line, { runs: writeTerminalOrigins( previous.runs, index, index + 1, - state?.runs ?? terminalOriginSpan(1, line.getString(index) ? this.origin : undefined) + runs ?? terminalOriginSpan(1, line.getString(index) ? this.origin : undefined) ), components }) } + private setCell(line: OriginBufferLine, index: number, cell: OriginCell): void { + const state = readOriginLineAttachment(line)! + const origin = this.cells.get(cell) + state.originals.setCell.call(line, index, cell) + if (state.editing) { + if (state.recordSets) { + this.edits.erase(state, index) + } + } else { + this.writeCell( + line, + index, + origin ? terminalOriginSpan(1, terminalOriginAt(origin.runs, origin.index)) : [], + origin?.component + ) + } + } + + private copyFrom(line: OriginBufferLine, source: OriginBufferLine, blank?: boolean): void { + const state = this.state(source) + readOriginLineAttachment(line)!.originals.copyFrom.call(line, source, blank) + this.assign(line, { + runs: state.runs, + components: !blank && state.components?.size ? new Map(state.components) : undefined + }) + } + + private clone(line: OriginBufferLine, blank?: boolean): OriginBufferLine { + const result = this.attachLine( + readOriginLineAttachment(line)!.originals.clone.call(line, blank) + ) + const state = this.state(line) + this.assign(result, { + runs: state.runs, + components: !blank && state.components?.size ? new Map(state.components) : undefined + }) + return result + } + + private copyCells( + line: OriginBufferLine, + source: OriginBufferLine, + from: number, + to: number, + count: number, + reverse: boolean + ): void { + const original = this.state(source) + const target = this.state(line) + const next = replaceOriginCells(target, to, to + count, original, from) + target.originals.copyCellsFrom.call(line, source, from, to, count, reverse) + this.assign(line, next) + } + private attachLine(line: OriginBufferLine): OriginBufferLine { - if (this.lines.has(line)) { + if (readOriginLineAttachment(line)?.owner === this) { return line } - this.lines.set(line, { runs: [], components: new Map() }) - const edits = new HeadlessOriginLineEdits( - () => this.state(line), - (state) => this.assign(line, state) - ) - const load = line.loadCell - line.loadCell = (index, cell) => { - const result = load.call(line, index, cell) - if (edits.active) { - return result - } - const state = this.state(line) - const snapshot = this.cells.get(result) - if (snapshot) { - snapshot.runs = state.runs - snapshot.index = index - snapshot.component = state.components.get(index) - } else { - this.cells.set(result, { - runs: state.runs, - index, - component: state.components.get(index) - }) - } - return result - } - const set = line.setCell - line.setCell = (index, cell) => { - const origin = this.cells.get(cell) - set.call(line, index, cell) - if (!edits.collect(index, false)) { - this.writeCell(line, index, { - runs: origin ? terminalOriginSpan(1, terminalOriginAt(origin.runs, origin.index)) : [], - components: origin?.component ? new Map([[0, origin.component]]) : new Map() - }) - } - } - const setData = line.set - if (setData) { - line.set = (index, cell) => { - setData.call(line, index, cell) - this.writeCell(line, index) - } - } - const copy = line.copyFrom - line.copyFrom = (source, blank) => { - const state = this.state(source) - copy.call(line, source, blank) - this.assign(line, { - runs: state.runs, - components: blank ? new Map() : new Map(state.components) - }) - } - const clone = line.clone - line.clone = (blank) => { - const result = this.attachLine(clone.call(line, blank)) - const state = this.state(line) - this.assign(result, { - runs: state.runs, - components: blank ? new Map() : new Map(state.components) - }) - return result - } - const copyCells = line.copyCellsFrom - line.copyCellsFrom = (source, from, to, count, reverse) => { - const original = this.state(source) - const target = this.state(line) - const next = replaceOriginCells(target, to, to + count, original, from) - copyCells.call(line, source, from, to, count, reverse) - this.assign(line, next) - } - this.printHooks.attach(line, edits) - edits.attach(line) + const originals = captureOriginLineMethods(line, this.sharedOriginals) + this.sharedOriginals ??= originals + attachOriginLineState(line, { + owner: this, + originals, + runs: [], + editing: false, + recordSets: true + }) + Object.assign(line, originals.set ? this.hooks : this.requiredHooks) return line } } diff --git a/src/main/daemon/emulation/headless-origin-cell-snapshots.ts b/src/main/daemon/emulation/headless-origin-cell-snapshots.ts new file mode 100644 index 00000000000..dff68a0d864 --- /dev/null +++ b/src/main/daemon/emulation/headless-origin-cell-snapshots.ts @@ -0,0 +1,41 @@ +import type { + OriginBufferLine, + OriginCell, + OriginLineAttachment +} from './headless-content-origin-lines' +import { readOriginLineAttachment } from './headless-content-origin-lines' +import type { TerminalOriginRuns } from '../../../shared/terminal/output/terminal-content-origin' + +type OriginCellSnapshot = { + runs: TerminalOriginRuns + index: number + component?: TerminalOriginRuns +} + +/** A reused editor cell keeps the provenance captured when it was loaded. */ +export class HeadlessOriginCellSnapshots { + private readonly cells = new WeakMap() + constructor(private readonly read: (line: OriginBufferLine) => OriginLineAttachment) {} + + get(cell: OriginCell): OriginCellSnapshot | undefined { + return this.cells.get(cell) + } + + load(line: OriginBufferLine, index: number, cell: OriginCell): OriginCell { + const attached = readOriginLineAttachment(line)! + const result = attached.originals.loadCell.call(line, index, cell) + if (attached.editing) { + return result + } + const state = this.read(line) + const snapshot = this.cells.get(result) + if (snapshot) { + snapshot.runs = state.runs + snapshot.index = index + snapshot.component = state.components?.get(index) + } else { + this.cells.set(result, { runs: state.runs, index, component: state.components?.get(index) }) + } + return result + } +} diff --git a/src/main/daemon/emulation/headless-origin-line-edits.ts b/src/main/daemon/emulation/headless-origin-line-edits.ts index 9e13b87f392..476e6891bdd 100644 --- a/src/main/daemon/emulation/headless-origin-line-edits.ts +++ b/src/main/daemon/emulation/headless-origin-line-edits.ts @@ -3,7 +3,12 @@ import { writeTerminalOrigins, type TerminalOriginRuns } from '../../../shared/terminal/output/terminal-content-origin' -import type { OriginBufferLine, OriginLineState } from './headless-content-origin-lines' +import type { + OriginBufferLine, + OriginLineState, + OriginLineAttachment, + OriginCell +} from './headless-content-origin-lines' export function replaceOriginCells( target: OriginLineState, @@ -13,19 +18,20 @@ export function replaceOriginCells( from = 0 ): OriginLineState { const copied: [number, TerminalOriginRuns][] = [] - for (let index = from; source?.components.size && index < from + end - start; index++) { + for (let index = from; source?.components?.size && index < from + end - start; index++) { const runs = source.components.get(index) if (runs) { copied.push([index - from + start, runs]) } } - const components = target.components - if (components.size) { + let components = target.components + if (components?.size) { for (let index = start; index < end; index++) { components.delete(index) } } for (const [index, runs] of copied) { + components ??= new Map() components.set(index, runs) } return { @@ -39,126 +45,149 @@ export function replaceOriginCells( } } -/** Nested setters report only actual erasures; the outer edit owns shifts and protected cells. */ +/** Actual nested erasures stay on their row; outer edits own structural moves. */ export class HeadlessOriginLineEdits { - private editing = false - private recordSets = true - private erased: { start: number; end: number }[] = [] - constructor( - private readonly read: () => OriginLineState, - private readonly assign: (state: OriginLineState) => void + private readonly read: (line: OriginBufferLine) => OriginLineAttachment, + private readonly assign: (line: OriginBufferLine, state: OriginLineState) => void ) {} - get active(): boolean { - return this.editing - } - - collect(index: number, codepoint: boolean): boolean { - if (!this.editing) { + collect(state: OriginLineAttachment, index: number, codepoint: boolean): boolean { + if (!state.editing) { return false } if (codepoint) { - this.erase(index) + this.erase(state, index) } return true } - private erase(index: number): void { - const last = this.erased.at(-1) + erase(state: OriginLineAttachment, index: number): void { + const erased = (state.erased ??= []) + const last = erased.at(-1) if (last?.end === index) { last.end++ } else { - this.erased.push({ start: index, end: index + 1 }) + erased.push({ start: index, end: index + 1 }) } } - attach(line: OriginBufferLine): void { - const run = ( - operation: () => T, - transform: (before: OriginLineState) => OriginLineState, - recordSets = true - ): T => { - const before = this.read() - this.recordSets = recordSets - this.editing = true - this.erased = [] - try { - return operation() - } finally { - this.editing = false - let next = transform(before) - for (const span of this.erased) { - next = replaceOriginCells(next, span.start, span.end) - } - this.assign(next) - this.erased = [] + insert(line: OriginBufferLine, position: number, count: number, fill: OriginCell): void { + this.shift(line, position, count, fill, true) + } + + remove(line: OriginBufferLine, position: number, count: number, fill: OriginCell): void { + this.shift(line, position, count, fill, false) + } + + private shift( + line: OriginBufferLine, + position: number, + count: number, + fill: OriginCell, + insert: boolean + ): void { + const state = this.read(line) + const start = position % line.length + const width = line.length + const source = { + runs: state.runs, + components: state.components?.size ? new Map(state.components) : undefined + } + this.begin(state, false) + try { + if (insert) { + state.originals.insertCells.call(line, position, count, fill) + } else { + state.originals.deleteCells.call(line, position, count, fill) } - } - const insert = line.insertCells - line.insertCells = (position, count, fill) => { - const start = position % line.length - const width = line.length - run( - () => insert.call(line, position, count, fill), - (before) => { - const source = { runs: before.runs, components: new Map(before.components) } - let next = replaceOriginCells(before, start, width) - if (count < width - start) { - next = replaceOriginCells(next, start + count, width, source, start) - } - return next - }, - false - ) - } - const remove = line.deleteCells - line.deleteCells = (position, count, fill) => { - const start = position % line.length - const width = line.length - run( - () => remove.call(line, position, count, fill), - (before) => { - const source = { runs: before.runs, components: new Map(before.components) } - let next = replaceOriginCells(before, start, width) - if (count < width - start) { - next = replaceOriginCells(next, start, width - count, source, start + count) - } - return next - }, - false - ) - } - const replace = line.replaceCells - line.replaceCells = (start, end, fill, protect) => { - run( - () => replace.call(line, start, end, fill, protect), - (before) => before - ) - } - const fill = line.fill - line.fill = (cell, protect) => { - run( - () => fill.call(line, cell, protect), - (before) => before - ) - } - const resize = line.resize - line.resize = (cols, cell) => - run( - () => resize.call(line, cols, cell), - (before) => ({ - runs: sliceTerminalOrigins(before.runs, 0, cols), - components: new Map([...before.components].filter(([index]) => index < cols)) - }) - ) - const set = line.setCell - line.setCell = (index, cell) => { - set.call(line, index, cell) - // Fill/replace skip protected columns, so collect the actual nested footprint. - if (this.editing && this.recordSets) { - this.erase(index) + } finally { + state.editing = false + try { + let next = replaceOriginCells(state, start, width) + if (count < width - start) { + next = insert + ? replaceOriginCells(next, start + count, width, source, start) + : replaceOriginCells(next, start, width - count, source, start + count) + } + this.finish(line, state, next) + } finally { + state.erased = undefined } } } + + replace( + line: OriginBufferLine, + start: number, + end: number, + fill: OriginCell, + protect?: boolean + ): void { + const state = this.read(line) + this.begin(state) + try { + state.originals.replaceCells.call(line, start, end, fill, protect) + } finally { + this.end(line, state) + } + } + + fill(line: OriginBufferLine, cell: OriginCell, protect?: boolean): void { + const state = this.read(line) + this.begin(state) + try { + state.originals.fill.call(line, cell, protect) + } finally { + this.end(line, state) + } + } + + resize(line: OriginBufferLine, cols: number, cell: OriginCell): boolean { + const state = this.read(line) + this.begin(state) + try { + return state.originals.resize.call(line, cols, cell) + } finally { + state.editing = false + try { + let components: OriginLineState['components'] + if (state.components?.size) { + for (const [index, runs] of state.components) { + if (index < cols) { + components ??= new Map() + components.set(index, runs) + } + } + } + this.finish(line, state, { runs: sliceTerminalOrigins(state.runs, 0, cols), components }) + } finally { + state.erased = undefined + } + } + } + + private begin(state: OriginLineAttachment, recordSets = true): void { + state.recordSets = recordSets + state.editing = true + state.erased = undefined + } + + private end(line: OriginBufferLine, state: OriginLineAttachment): void { + state.editing = false + try { + this.finish(line, state, state) + } finally { + state.erased = undefined + } + } + + private finish(line: OriginBufferLine, state: OriginLineAttachment, next: OriginLineState): void { + if (state.erased) { + for (const span of state.erased) { + next = replaceOriginCells(next, span.start, span.end) + } + } + this.assign(line, next) + } } diff --git a/src/main/daemon/emulation/headless-origin-line-hooks.ts b/src/main/daemon/emulation/headless-origin-line-hooks.ts new file mode 100644 index 00000000000..f2f52fe355b --- /dev/null +++ b/src/main/daemon/emulation/headless-origin-line-hooks.ts @@ -0,0 +1,102 @@ +import type { OriginBufferLine, OriginLineMethods } from './headless-content-origin-lines' + +type OriginLineHandlers = { + [K in keyof OriginLineMethods]: ( + line: OriginBufferLine, + ...args: Parameters + ) => ReturnType +} + +const LINE_METHOD_NAMES = [ + 'loadCell', + 'setCell', + 'setCellFromCodepoint', + 'addCodepointToCell', + 'set', + 'insertCells', + 'deleteCells', + 'replaceCells', + 'fill', + 'resize', + 'copyFrom', + 'clone', + 'copyCellsFrom' +] as const + +/** One method target per emulator; original methods are captured before any hook is installed. */ +export function createOriginLineHooks(handlers: OriginLineHandlers): OriginLineMethods { + return { + loadCell(this: OriginBufferLine, index, cell) { + return handlers.loadCell(this, index, cell) + }, + setCell(this: OriginBufferLine, index, cell) { + handlers.setCell(this, index, cell) + }, + setCellFromCodepoint(this: OriginBufferLine, index, code, width, attrs) { + handlers.setCellFromCodepoint(this, index, code, width, attrs) + }, + addCodepointToCell(this: OriginBufferLine, index, code, width) { + handlers.addCodepointToCell(this, index, code, width) + }, + set(this: OriginBufferLine, index, cell) { + handlers.set(this, index, cell) + }, + insertCells(this: OriginBufferLine, position, count, fill) { + handlers.insertCells(this, position, count, fill) + }, + deleteCells(this: OriginBufferLine, position, count, fill) { + handlers.deleteCells(this, position, count, fill) + }, + replaceCells(this: OriginBufferLine, start, end, fill, protect) { + handlers.replaceCells(this, start, end, fill, protect) + }, + fill(this: OriginBufferLine, cell, protect) { + handlers.fill(this, cell, protect) + }, + resize(this: OriginBufferLine, cols, cell) { + return handlers.resize(this, cols, cell) + }, + copyFrom(this: OriginBufferLine, source, blank) { + handlers.copyFrom(this, source, blank) + }, + clone(this: OriginBufferLine, blank) { + return handlers.clone(this, blank) + }, + copyCellsFrom(this: OriginBufferLine, source, from, to, count, reverse) { + handlers.copyCellsFrom(this, source, from, to, count, reverse) + } + } +} + +export function captureOriginLineMethods( + line: OriginBufferLine, + shared?: OriginLineMethods +): OriginLineMethods { + if (shared) { + let matches = true + for (const name of LINE_METHOD_NAMES) { + if (line[name] !== shared[name]) { + matches = false + break + } + } + if (matches) { + return shared + } + } + return { + loadCell: line.loadCell, + setCell: line.setCell, + setCellFromCodepoint: line.setCellFromCodepoint, + addCodepointToCell: line.addCodepointToCell, + set: line.set, + insertCells: line.insertCells, + deleteCells: line.deleteCells, + replaceCells: line.replaceCells, + fill: line.fill, + resize: line.resize, + copyFrom: line.copyFrom, + clone: line.clone, + copyCellsFrom: line.copyCellsFrom + } +} diff --git a/src/main/daemon/emulation/headless-origin-print-hooks.ts b/src/main/daemon/emulation/headless-origin-print-hooks.ts deleted file mode 100644 index e63446bdf4f..00000000000 --- a/src/main/daemon/emulation/headless-origin-print-hooks.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { OriginBufferLine } from './headless-content-origin-lines' -import type { HeadlessOriginLineEdits } from './headless-origin-line-edits' - -type Print = OriginBufferLine['setCellFromCodepoint'] -type Combine = OriginBufferLine['addCodepointToCell'] - -/** Keep xterm's per-character call targets shared while each line retains its own editor. */ -export class HeadlessOriginPrintHooks { - private readonly lines = new WeakMap< - OriginBufferLine, - { edits: HeadlessOriginLineEdits; print: Print; combine: Combine } - >() - private readonly print: Print - private readonly combine: Combine - - constructor( - printed: (line: OriginBufferLine, index: number, width: number, code: number) => void, - combined: ( - line: OriginBufferLine, - index: number, - code: number, - width: number, - original: Combine - ) => void - ) { - const lines = this.lines - this.print = function (this: OriginBufferLine, index, code, width, attrs) { - const state = lines.get(this)! - state.print.call(this, index, code, width, attrs) - if (!state.edits.collect(index, true)) { - printed(this, index, width, code) - } - } - this.combine = function (this: OriginBufferLine, index, code, width) { - combined(this, index, code, width, lines.get(this)!.combine) - } - } - - attach(line: OriginBufferLine, edits: HeadlessOriginLineEdits): void { - this.lines.set(line, { - edits, - print: line.setCellFromCodepoint, - combine: line.addCodepointToCell - }) - line.setCellFromCodepoint = this.print - line.addCodepointToCell = this.combine - } -} diff --git a/src/main/daemon/emulation/testing/headless-emulator.test.ts b/src/main/daemon/emulation/testing/headless-emulator.test.ts index a4bd222a7f9..82b428072a4 100644 --- a/src/main/daemon/emulation/testing/headless-emulator.test.ts +++ b/src/main/daemon/emulation/testing/headless-emulator.test.ts @@ -389,7 +389,7 @@ it.each([100, 1000])( const spy = vi.spyOn(seam, 'assign').mockImplementation((line, state) => { assign(line, { runs: observeRuns(state.runs), - components: observeComponents(state.components) + components: state.components ? observeComponents(state.components) : undefined }) }) const write = async (data: string) => {