mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 10:27:33 +00:00
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:
co-authored by
Jinjing
parent
6abdd55456
commit
8afea421b8
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user