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>
This commit is contained in:
Brennan Benson
2026-11-13 17:54:09 -08:00
co-authored by Jinjing
parent 6abdd55456
commit 8afea421b8
7 changed files with 449 additions and 272 deletions
@@ -68,14 +68,47 @@ export function originTerminalCore(terminal: Terminal): OriginTerminalCore | und
export type OriginLineState = {
runs: TerminalOriginRuns
components: Map<number, TerminalOriginRuns>
components?: Map<number, TerminalOriginRuns>
}
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<OriginBufferLine, OriginLineState>
): 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)
@@ -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<OriginBufferLine, OriginLineState>()
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
}
}
@@ -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<OriginCell, OriginCellSnapshot>()
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
}
}
@@ -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 = <T>(
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)
}
}
@@ -0,0 +1,102 @@
import type { OriginBufferLine, OriginLineMethods } from './headless-content-origin-lines'
type OriginLineHandlers = {
[K in keyof OriginLineMethods]: (
line: OriginBufferLine,
...args: Parameters<OriginLineMethods[K]>
) => ReturnType<OriginLineMethods[K]>
}
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
}
}
@@ -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
}
}
@@ -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) => {