[Init] Initial commit - NetMesh terminal manager
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
Some checks failed
build-packages / resolve bundled mosh-client (push) Has been cancelled
build-packages / resolve bundled et-client (push) Has been cancelled
build-packages / build-macos (push) Has been cancelled
build-packages / build-windows (push) Has been cancelled
build-packages / build-linux-x64 (push) Has been cancelled
build-packages / build-linux-arm64 (push) Has been cancelled
build-packages / release (push) Has been cancelled
build-packages / update Nix release metadata (push) Has been cancelled
build-packages / bump homebrew tap (push) Has been cancelled
test / lint-and-test (push) Has been cancelled
AI automation / Route event (push) Has been cancelled
AI automation / Hand reopened issue to maintainers (push) Has been cancelled
AI automation / Clean source issue state (push) Has been cancelled
AI automation / Reconcile handoffs (push) Has been cancelled
AI automation / Classify issue (push) Has been cancelled
AI automation / Claude Code smoke (push) Has been cancelled
AI automation / Review issue follow-up (push) Has been cancelled
AI automation / Publish issue follow-up (push) Has been cancelled
AI automation / Implement with Claude Code (push) Has been cancelled
AI automation / Publish implement PR (push) Has been cancelled
AI automation / Continue queued issue comments (push) Has been cancelled
AI automation / Codex review loop (push) Has been cancelled
AI automation / Publish Codex fix (push) Has been cancelled
AI automation / Clear Codex dispatch marker (push) Has been cancelled
AI automation / Own PR re-request Codex (push) Has been cancelled
AI automation / External PR re-request Codex (push) Has been cancelled
AI automation / Poll Codex reaction / retry (push) Has been cancelled
build-et-binaries / build-linux-x64 (push) Has been cancelled
build-et-binaries / build-linux-arm64 (push) Has been cancelled
build-et-binaries / build-macos-universal (push) Has been cancelled
build-et-binaries / build-windows-x64 (push) Has been cancelled
build-et-binaries / release (push) Has been cancelled
This commit is contained in:
498
components/terminal/runtime/cursorLineHighlight.test.ts
Normal file
498
components/terminal/runtime/cursorLineHighlight.test.ts
Normal file
@@ -0,0 +1,498 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
|
||||
import { CursorLineHighlighter } from './cursorLineHighlight.ts';
|
||||
|
||||
type Handler = () => void;
|
||||
type FakeElement = {
|
||||
style: Record<string, string>;
|
||||
attributes: Record<string, string>;
|
||||
setAttribute: (name: string, value: string) => void;
|
||||
};
|
||||
const createFakeTerm = (cols = 80) => {
|
||||
let cursorY = 0;
|
||||
let cursorX = 0;
|
||||
let hasSelection = false;
|
||||
let baseY = 0;
|
||||
let bufferType: 'normal' | 'alternate' = 'normal';
|
||||
let lineLength = cols;
|
||||
const coloredBackgrounds = new Set<number>();
|
||||
const coloredForegrounds = new Set<number>();
|
||||
const inverseCells = new Set<number>();
|
||||
const cursorMoveHandlers: Handler[] = [];
|
||||
const writeParsedHandlers: Handler[] = [];
|
||||
const resizeHandlers: Handler[] = [];
|
||||
const bufferChangeHandlers: Handler[] = [];
|
||||
const renderHandlers: Handler[] = [];
|
||||
const selectionChangeHandlers: Handler[] = [];
|
||||
const decorations: Array<{
|
||||
options: Record<string, unknown>;
|
||||
disposed: boolean;
|
||||
element: FakeElement;
|
||||
dispose: () => void;
|
||||
onRender: (handler: (element: FakeElement) => void) => { dispose: () => void };
|
||||
onDispose: (handler: Handler) => { dispose: () => void };
|
||||
}> = [];
|
||||
const markers: Array<{ line: number; disposed: boolean }> = [];
|
||||
|
||||
const term = {
|
||||
cols,
|
||||
buffer: {
|
||||
active: {
|
||||
get type() {
|
||||
return bufferType;
|
||||
},
|
||||
get baseY() {
|
||||
return baseY;
|
||||
},
|
||||
get cursorY() {
|
||||
return cursorY;
|
||||
},
|
||||
get cursorX() {
|
||||
return cursorX;
|
||||
},
|
||||
getLine() {
|
||||
return {
|
||||
length: cols,
|
||||
getCell(x: number) {
|
||||
return {
|
||||
getChars: () => (x < lineLength ? 'x' : ''),
|
||||
getWidth: () => 1,
|
||||
isAttributeDefault: () => x >= lineLength,
|
||||
isBgDefault: () => !coloredBackgrounds.has(x),
|
||||
isFgDefault: () => !coloredForegrounds.has(x),
|
||||
isInverse: () => (inverseCells.has(x) ? 1 : 0),
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
getNullCell() {
|
||||
return { isBgDefault: () => true };
|
||||
},
|
||||
},
|
||||
onBufferChange(handler: Handler) {
|
||||
bufferChangeHandlers.push(handler);
|
||||
return { dispose() {} };
|
||||
},
|
||||
},
|
||||
onCursorMove(handler: Handler) {
|
||||
cursorMoveHandlers.push(handler);
|
||||
return { dispose() {} };
|
||||
},
|
||||
onResize(handler: Handler) {
|
||||
resizeHandlers.push(handler);
|
||||
return { dispose() {} };
|
||||
},
|
||||
onWriteParsed(handler: Handler) {
|
||||
writeParsedHandlers.push(handler);
|
||||
return { dispose() {} };
|
||||
},
|
||||
onRender(handler: Handler) {
|
||||
renderHandlers.push(handler);
|
||||
return { dispose() {} };
|
||||
},
|
||||
onSelectionChange(handler: Handler) {
|
||||
selectionChangeHandlers.push(handler);
|
||||
return { dispose() {} };
|
||||
},
|
||||
hasSelection() {
|
||||
return hasSelection;
|
||||
},
|
||||
registerMarker(offset: number) {
|
||||
const marker = {
|
||||
line: baseY + cursorY + offset,
|
||||
disposed: false,
|
||||
get isDisposed() {
|
||||
return this.disposed;
|
||||
},
|
||||
dispose() {
|
||||
this.disposed = true;
|
||||
},
|
||||
};
|
||||
markers.push(marker);
|
||||
return marker;
|
||||
},
|
||||
registerDecoration(options: Record<string, unknown>) {
|
||||
const disposeHandlers = new Set<Handler>();
|
||||
const element: FakeElement = {
|
||||
style: {},
|
||||
attributes: {},
|
||||
setAttribute(name, value) {
|
||||
this.attributes[name] = value;
|
||||
},
|
||||
};
|
||||
const decoration = {
|
||||
options,
|
||||
disposed: false,
|
||||
element,
|
||||
dispose() {
|
||||
if (this.disposed) return;
|
||||
this.disposed = true;
|
||||
for (const handler of disposeHandlers) handler();
|
||||
disposeHandlers.clear();
|
||||
},
|
||||
onDispose(handler: Handler) {
|
||||
disposeHandlers.add(handler);
|
||||
return { dispose: () => disposeHandlers.delete(handler) };
|
||||
},
|
||||
onRender(handler: (element: FakeElement) => void) {
|
||||
handler(element);
|
||||
return { dispose() {} };
|
||||
},
|
||||
};
|
||||
decorations.push(decoration);
|
||||
return decoration;
|
||||
},
|
||||
moveCursor(nextY: number) {
|
||||
cursorY = nextY;
|
||||
for (const handler of cursorMoveHandlers) handler();
|
||||
for (const handler of renderHandlers) handler();
|
||||
},
|
||||
scrollOutput(lines: number) {
|
||||
baseY += lines;
|
||||
for (const handler of writeParsedHandlers) handler();
|
||||
for (const handler of renderHandlers) handler();
|
||||
},
|
||||
writeHiddenOutput(lines: number) {
|
||||
baseY += lines;
|
||||
for (const handler of writeParsedHandlers) handler();
|
||||
},
|
||||
trimScrollback(lines: number) {
|
||||
for (const marker of markers) {
|
||||
if (!marker.disposed) marker.line -= lines;
|
||||
}
|
||||
for (const handler of writeParsedHandlers) handler();
|
||||
for (const handler of renderHandlers) handler();
|
||||
},
|
||||
setBufferType(nextType: 'normal' | 'alternate') {
|
||||
bufferType = nextType;
|
||||
for (const handler of bufferChangeHandlers) handler();
|
||||
},
|
||||
setColoredBackgrounds(columns: number[]) {
|
||||
coloredBackgrounds.clear();
|
||||
for (const column of columns) coloredBackgrounds.add(column);
|
||||
for (const handler of writeParsedHandlers) handler();
|
||||
for (const handler of renderHandlers) handler();
|
||||
},
|
||||
setLineLength(nextLength: number) {
|
||||
lineLength = nextLength;
|
||||
for (const handler of writeParsedHandlers) handler();
|
||||
for (const handler of renderHandlers) handler();
|
||||
},
|
||||
setCursorX(nextX: number) {
|
||||
cursorX = nextX;
|
||||
for (const handler of cursorMoveHandlers) handler();
|
||||
for (const handler of renderHandlers) handler();
|
||||
},
|
||||
setSelection(next: boolean) {
|
||||
hasSelection = next;
|
||||
for (const handler of selectionChangeHandlers) handler();
|
||||
},
|
||||
render() {
|
||||
for (const handler of renderHandlers) handler();
|
||||
},
|
||||
setColoredForegrounds(columns: number[]) {
|
||||
coloredForegrounds.clear();
|
||||
for (const column of columns) coloredForegrounds.add(column);
|
||||
for (const handler of writeParsedHandlers) handler();
|
||||
for (const handler of renderHandlers) handler();
|
||||
},
|
||||
setInverseCells(columns: number[]) {
|
||||
inverseCells.clear();
|
||||
for (const column of columns) inverseCells.add(column);
|
||||
for (const handler of writeParsedHandlers) handler();
|
||||
for (const handler of renderHandlers) handler();
|
||||
},
|
||||
resetDecorations() {
|
||||
for (const decoration of decorations) decoration.dispose();
|
||||
},
|
||||
setCols(nextCols: number) {
|
||||
term.cols = nextCols;
|
||||
for (const handler of resizeHandlers) handler();
|
||||
},
|
||||
decorations,
|
||||
markers,
|
||||
};
|
||||
|
||||
return term;
|
||||
};
|
||||
|
||||
test('CursorLineHighlighter paints an opaque background without changing text', () => {
|
||||
const term = createFakeTerm(100);
|
||||
const highlighter = new CursorLineHighlighter(term as never);
|
||||
highlighter.setBackgroundColor('#263449');
|
||||
highlighter.setEnabled(true);
|
||||
|
||||
assert.equal(term.decorations.length, 1);
|
||||
assert.equal(term.decorations[0]?.options.width, 100);
|
||||
assert.equal(term.decorations[0]?.options.backgroundColor, '#263449');
|
||||
assert.equal(term.decorations[0]?.options.layer, 'bottom');
|
||||
highlighter.dispose();
|
||||
});
|
||||
|
||||
test('CursorLineHighlighter gives selection and search matches priority', () => {
|
||||
const term = createFakeTerm(10);
|
||||
const highlighter = new CursorLineHighlighter(term as never);
|
||||
highlighter.setEnabled(true);
|
||||
assert.equal(term.decorations.at(-1)?.disposed, false);
|
||||
|
||||
term.setSelection(true);
|
||||
assert.equal(term.decorations.at(-1)?.disposed, true);
|
||||
|
||||
term.setSelection(false);
|
||||
assert.equal(term.decorations.at(-1)?.disposed, false);
|
||||
highlighter.dispose();
|
||||
});
|
||||
|
||||
test('CursorLineHighlighter refreshes after hidden writes become visible', () => {
|
||||
const term = createFakeTerm(10);
|
||||
const highlighter = new CursorLineHighlighter(term as never);
|
||||
highlighter.setEnabled(true);
|
||||
assert.equal(term.markers.at(-1)?.line, 0);
|
||||
|
||||
term.writeHiddenOutput(1);
|
||||
assert.equal(term.decorations.length, 1);
|
||||
term.render();
|
||||
|
||||
assert.equal(term.decorations.length, 2);
|
||||
assert.equal(term.decorations[0]?.disposed, true);
|
||||
assert.equal(term.markers.at(-1)?.line, 1);
|
||||
highlighter.dispose();
|
||||
});
|
||||
|
||||
test('CursorLineHighlighter keeps a continuous background under colored keyword text', () => {
|
||||
const term = createFakeTerm(10);
|
||||
const keywordMarker = term.registerMarker(0);
|
||||
term.registerDecoration({
|
||||
marker: keywordMarker,
|
||||
x: 2,
|
||||
width: 2,
|
||||
foregroundColor: '#F87171',
|
||||
});
|
||||
const highlighter = new CursorLineHighlighter(term as never);
|
||||
highlighter.setBackgroundColor('#263449');
|
||||
highlighter.setEnabled(true);
|
||||
|
||||
assert.equal(term.decorations[0]?.options.foregroundColor, '#F87171');
|
||||
assert.equal(term.decorations[0]?.options.backgroundColor, undefined);
|
||||
assert.equal(term.decorations[1]?.options.x, 0);
|
||||
assert.equal(term.decorations[1]?.options.width, 10);
|
||||
assert.equal(term.decorations[1]?.options.backgroundColor, '#263449');
|
||||
assert.equal(term.decorations[1]?.options.layer, 'bottom');
|
||||
highlighter.dispose();
|
||||
});
|
||||
|
||||
test('CursorLineHighlighter leaves ANSI background cells untouched', () => {
|
||||
const term = createFakeTerm(10);
|
||||
term.setColoredBackgrounds([2, 3, 7]);
|
||||
const highlighter = new CursorLineHighlighter(term as never);
|
||||
highlighter.setBackgroundColor('#263449');
|
||||
highlighter.setEnabled(true);
|
||||
|
||||
assert.deepEqual(
|
||||
term.decorations.map(({ options }) => ({ x: options.x, width: options.width })),
|
||||
[
|
||||
{ x: 0, width: 2 },
|
||||
{ x: 4, width: 3 },
|
||||
{ x: 8, width: 2 },
|
||||
],
|
||||
);
|
||||
highlighter.dispose();
|
||||
});
|
||||
|
||||
test('CursorLineHighlighter fills the blank tail after short output', () => {
|
||||
const term = createFakeTerm(10);
|
||||
term.setLineLength(4);
|
||||
const highlighter = new CursorLineHighlighter(term as never);
|
||||
highlighter.setBackgroundColor('#263449');
|
||||
highlighter.setEnabled(true);
|
||||
|
||||
assert.equal(term.decorations.length, 2);
|
||||
assert.equal(term.decorations[0]?.options.width, 4);
|
||||
assert.equal(term.decorations[1]?.options.x, 4);
|
||||
assert.equal(term.decorations[1]?.options.width, 6);
|
||||
assert.equal(term.decorations[1]?.options.backgroundColor, '#263449');
|
||||
assert.equal(term.decorations[1]?.options.layer, 'bottom');
|
||||
highlighter.dispose();
|
||||
});
|
||||
|
||||
test('CursorLineHighlighter fills the blank tail through the cursor cell', () => {
|
||||
const term = createFakeTerm(10);
|
||||
term.setLineLength(4);
|
||||
term.setCursorX(5);
|
||||
const highlighter = new CursorLineHighlighter(term as never);
|
||||
highlighter.setBackgroundColor('#263449');
|
||||
highlighter.setEnabled(true);
|
||||
|
||||
assert.deepEqual(
|
||||
term.decorations.map(({ options }) => ({ x: options.x, width: options.width })),
|
||||
[
|
||||
{ x: 0, width: 4 },
|
||||
{ x: 4, width: 6 },
|
||||
],
|
||||
);
|
||||
highlighter.dispose();
|
||||
});
|
||||
|
||||
test('CursorLineHighlighter fills a wrap-pending final cell', () => {
|
||||
const term = createFakeTerm(10);
|
||||
term.setLineLength(4);
|
||||
term.setCursorX(10);
|
||||
const highlighter = new CursorLineHighlighter(term as never);
|
||||
highlighter.setBackgroundColor('#263449');
|
||||
highlighter.setEnabled(true);
|
||||
|
||||
assert.deepEqual(
|
||||
term.decorations.map(({ options }) => ({ x: options.x, width: options.width })),
|
||||
[
|
||||
{ x: 0, width: 4 },
|
||||
{ x: 4, width: 6 },
|
||||
],
|
||||
);
|
||||
highlighter.dispose();
|
||||
});
|
||||
|
||||
test('CursorLineHighlighter keeps foreground colors and leaves inverse cells untouched', () => {
|
||||
const term = createFakeTerm(10);
|
||||
term.setColoredForegrounds([2, 3]);
|
||||
term.setInverseCells([7]);
|
||||
const highlighter = new CursorLineHighlighter(term as never);
|
||||
highlighter.setBackgroundColor('#263449');
|
||||
highlighter.setEnabled(true);
|
||||
|
||||
assert.deepEqual(
|
||||
term.decorations.map(({ options }) => ({ x: options.x, width: options.width })),
|
||||
[
|
||||
{ x: 0, width: 7 },
|
||||
{ x: 8, width: 2 },
|
||||
],
|
||||
);
|
||||
highlighter.dispose();
|
||||
});
|
||||
|
||||
test('CursorLineHighlighter follows cursor moves and clears when disabled', () => {
|
||||
const term = createFakeTerm(80);
|
||||
const highlighter = new CursorLineHighlighter(term as never);
|
||||
highlighter.setEnabled(true);
|
||||
assert.equal(term.decorations.length, 1);
|
||||
|
||||
term.moveCursor(3);
|
||||
assert.equal(term.decorations.length, 2);
|
||||
assert.equal(term.decorations[0]?.disposed, true);
|
||||
assert.equal(term.decorations[1]?.disposed, false);
|
||||
|
||||
highlighter.setEnabled(false);
|
||||
assert.equal(term.decorations[1]?.disposed, true);
|
||||
highlighter.dispose();
|
||||
});
|
||||
|
||||
test('CursorLineHighlighter swaps decorations atomically on refresh', () => {
|
||||
const term = createFakeTerm(80);
|
||||
const highlighter = new CursorLineHighlighter(term as never);
|
||||
highlighter.setEnabled(true);
|
||||
const firstDecoration = term.decorations[0];
|
||||
assert.ok(firstDecoration);
|
||||
assert.equal(firstDecoration.disposed, false);
|
||||
|
||||
let sawOverlap = false;
|
||||
const originalRegisterMarker = term.registerMarker.bind(term);
|
||||
term.registerMarker = (offset: number) => {
|
||||
// New marker must be created while the previous decoration is still live.
|
||||
if (!firstDecoration.disposed) sawOverlap = true;
|
||||
return originalRegisterMarker(offset);
|
||||
};
|
||||
|
||||
term.moveCursor(2);
|
||||
|
||||
assert.equal(sawOverlap, true, 'new marker should register before old decoration disposal');
|
||||
assert.equal(firstDecoration.disposed, true);
|
||||
assert.equal(term.decorations.at(-1)?.disposed, false);
|
||||
highlighter.dispose();
|
||||
});
|
||||
|
||||
test('CursorLineHighlighter recreates on resize and overlay color changes', () => {
|
||||
const term = createFakeTerm(40);
|
||||
const highlighter = new CursorLineHighlighter(term as never);
|
||||
highlighter.setEnabled(true);
|
||||
highlighter.setBackgroundColor('#112233');
|
||||
assert.equal(
|
||||
term.decorations.at(-1)?.options.backgroundColor,
|
||||
'#112233',
|
||||
);
|
||||
assert.equal(term.decorations.at(-1)?.options.width, 40);
|
||||
|
||||
term.setCols(120);
|
||||
assert.equal(term.decorations.at(-2)?.options.width, 40);
|
||||
assert.equal(term.decorations.at(-1)?.options.width, 80);
|
||||
|
||||
highlighter.setBackgroundColor('#445566');
|
||||
assert.equal(
|
||||
term.decorations.at(-2)?.options.backgroundColor,
|
||||
'#445566',
|
||||
);
|
||||
highlighter.dispose();
|
||||
});
|
||||
|
||||
test('CursorLineHighlighter follows bottom-row output when only baseY changes', () => {
|
||||
const term = createFakeTerm(80);
|
||||
const highlighter = new CursorLineHighlighter(term as never);
|
||||
highlighter.setEnabled(true);
|
||||
assert.equal(term.markers.at(-1)?.line, 0);
|
||||
|
||||
term.scrollOutput(3);
|
||||
assert.equal(term.decorations.length, 2);
|
||||
assert.equal(term.decorations[0]?.disposed, true);
|
||||
assert.equal(term.markers.at(-1)?.line, 3);
|
||||
assert.equal(term.decorations.at(-1)?.disposed, false);
|
||||
highlighter.dispose();
|
||||
});
|
||||
|
||||
test('CursorLineHighlighter refreshes when saturated scrollback moves its marker', () => {
|
||||
const term = createFakeTerm(80);
|
||||
const highlighter = new CursorLineHighlighter(term as never);
|
||||
highlighter.setEnabled(true);
|
||||
const originalMarker = term.markers.at(-1);
|
||||
assert.equal(originalMarker?.line, 0);
|
||||
|
||||
term.trimScrollback(1);
|
||||
assert.equal(originalMarker?.disposed, true);
|
||||
assert.equal(term.decorations.length, 2);
|
||||
assert.equal(term.markers.at(-1)?.line, 0);
|
||||
assert.equal(term.decorations.at(-1)?.disposed, false);
|
||||
highlighter.dispose();
|
||||
});
|
||||
|
||||
test('CursorLineHighlighter restores after the terminal resets its decorations', () => {
|
||||
const term = createFakeTerm(80);
|
||||
const highlighter = new CursorLineHighlighter(term as never);
|
||||
highlighter.setEnabled(true);
|
||||
assert.equal(term.decorations.length, 1);
|
||||
|
||||
term.resetDecorations();
|
||||
assert.equal(term.decorations[0]?.disposed, true);
|
||||
|
||||
term.scrollOutput(0);
|
||||
assert.equal(term.decorations.length, 2);
|
||||
assert.equal(term.decorations.at(-1)?.disposed, false);
|
||||
highlighter.dispose();
|
||||
});
|
||||
|
||||
test('CursorLineHighlighter clears in the alternate buffer and restores in normal buffer', () => {
|
||||
const term = createFakeTerm(80);
|
||||
const highlighter = new CursorLineHighlighter(term as never);
|
||||
highlighter.setEnabled(true);
|
||||
assert.equal(term.decorations.at(-1)?.disposed, false);
|
||||
|
||||
term.setBufferType('alternate');
|
||||
assert.equal(term.decorations.at(-1)?.disposed, true);
|
||||
|
||||
const decorationCount = term.decorations.length;
|
||||
term.moveCursor(4);
|
||||
term.scrollOutput(1);
|
||||
assert.equal(term.decorations.length, decorationCount);
|
||||
|
||||
term.setBufferType('normal');
|
||||
assert.equal(term.decorations.length, decorationCount + 1);
|
||||
assert.equal(term.decorations.at(-1)?.disposed, false);
|
||||
highlighter.dispose();
|
||||
});
|
||||
Reference in New Issue
Block a user