Compare commits

..
Author SHA1 Message Date
Daniel Imms c2f5c424a8 Merge branch 'master' into fix/buffer-copy-and-reflow-wrap 2026-06-06 11:16:13 -07:00
3cdbc77386 fix(addon-search): advance highlight scan by buffer match size (#5994)
* fix(addon-search): advance highlight scan by buffer match size

_highlightAllMatches() stepped to the next find() using term.length
and col + 1 when staying on the same row. Search results expose size
in buffer columns (wide glyphs, wrapped matches), which select() and
decorations already use.

Advance with col + size and wrap row/col when crossing the terminal
width so highlight-all does not skip, duplicate, or stop early on
non-ASCII matches.

Co-authored-by: Cursor <cursoragent@cursor.com>

* test(addon-search): regression for highlight-all wide char advance by size

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>

* test(addon-search): move highlight-all regression to integration tests

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 11:12:04 -07:00
f57b8e1534 Update GitHub Actions to Node 24 compatible versions (#6007)
Upgrade actions/checkout, setup-node, upload-artifact, download-artifact,
and codeql-action to their latest major versions that run on Node 24.

Replace NathanFirmo/wait-for-other-job with native needs: build for
test-unit and test-integration jobs.

Fixes #6006

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 16:58:42 +00:00
c8f11d63b5 fix(terminal): always run clear() at cursor home (#5992)
Remove early return when ybase===0 && y===0 so clear() always
clears markers, resets buffer lines, and refills the viewport.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 16:43:22 +00:00
3d929a1db0 fix(selection): drag-scroll end row and trim start clamp (#5993)
* fix(selection): drag-scroll end row matches last visible line

Use ydisp + rows - 1 for selectionEnd when scrolling down during drag,
consistent with finalSelectionEnd for select-all.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>

* fix(selection): reset start to origin when start row is trimmed

When handleTrim removes the selection start row, clamp to [0,0] and
refresh; do not keep a stale column on row 0.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 16:18:47 +00:00
2fe3fd13a1 fix(addon-webgl): skip missing buffer lines in _updateModel (#5991)
* fix(addon-webgl): skip missing buffer lines in _updateModel

_updateModel force-unwrapped buffer.lines.get(row), which can be
undefined during trim or resize. That threw and left stale glyphs on
rows below the gap.

Clear the WebGL model row and glyph cells, reset blink state, and
continue with remaining viewport rows, matching DomRenderer and
ImageStorage handling.

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor(addon-webgl): extract _nullModelCell helper

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 09:15:09 -07:00
Daniel ImmsandCursor f33f502267 fix(DomRenderer): resilience when buffer lines are missing (#5998)
* fix(DomRenderer): skip missing buffer lines during row render

renderRows and link underline refresh stopped updating when any
viewport row lacked buffer data, leaving stale DOM on rows below.
Match ImageStorage.render behavior: clear the row element when
present and continue with remaining rows.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(DomRenderer): clear blink state when skipping missing lines

When a viewport row has no buffer line, renderRows clears the row DOM
and continues but did not reset _rowHasBlinkingCells for that row.
That could leave the text blink manager thinking blinking cells remain
after the row was emptied.

Clear blink state for the row when skipping a missing buffer line,
including during link underline refresh.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-06 09:09:32 -07:00
jerch 7c24c58007 fix non-ascii clipboard handling (#6002)
* fix non-ascii clipboard handling

* rename type helpers

* make linter happy
2026-06-06 09:07:03 -07:00
jerch 596634ac41 fix #6003 (#6004) 2026-06-06 09:02:43 -07:00
jerch 0c8271db49 fix atlas callstack exception (#6001)
* fix atlas callstack exception

* remove spread operator, integrate stress test into demo
2026-06-06 09:01:49 -07:00
Daniel ImmsandCursor f3f4dcaf9f fix(CharacterJoiner): bound join ranges to trimmed line length (#5990)
Character joiner scanning already iterated cells with
getTrimmedLength(), but trailing attribute runs and
_stringRangesToCellRanges used buffer cols. On short lines that
could run joiners on a single trailing cell and extend open ranges
through empty tail columns, affecting ligature/join rendering.

Use trimmed length consistently for trailing-run detection, the
cell-range walk, and the final range end column.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 07:05:19 -07:00
64718444e2 fix(webgl): avoid glyph atlas mipmaps (#5987)
Co-authored-by: Will Hampson <whamp@ggl.slmail.me>
Co-authored-by: jerch <jerch@rockborn.de>
2026-06-03 06:00:44 -07:00
Daniel ImmsandCursor e9c648f519 fix(BufferReflow): clear stale isWrapped after larger reflow
When widening the terminal unwraps a multi-row soft wrap and removes
trailing buffer lines, the last retained row could still have
isWrapped set, and the following line could still think it continued
a removed row. That broke wrapped range translation.

Clear isWrapped on the last retained row in the group and on the
first line after the group when rows are removed during reflow.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 03:37:20 +00:00
Daniel ImmsandCursor 60bae9528d fix(BufferLine): limit copyCellsFrom metadata to copied range
copyCellsFrom copied Uint32 cell data only for the requested column
range but migrated every combined entry with key >= srcCol, which could
attach grapheme metadata to unrelated destination columns during
partial line copies in reflow and wraparound.

Copy combined and extended attributes only per copied cell and clear
stale entries on the destination when the source cell does not use
them.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-03 03:36:39 +00:00
26 changed files with 412 additions and 211 deletions
+19 -27
View File
@@ -11,9 +11,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Use Node.js 22.x
uses: actions/setup-node@v3
uses: actions/setup-node@v5
with:
node-version: 22.x
cache: 'npm'
@@ -69,7 +69,7 @@ jobs:
./addons/addon-webgl/out/* \
./addons/addon-webgl/out-*st/*
- name: Upload artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: build-artifacts
path: compressed-build.zip
@@ -79,9 +79,9 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Use Node.js 22.x
uses: actions/setup-node@v3
uses: actions/setup-node@v5
with:
node-version: 22.x
cache: 'npm'
@@ -100,16 +100,16 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Use Node.js 22.x
uses: actions/setup-node@v3
uses: actions/setup-node@v5
with:
node-version: 22.x
cache: 'npm'
- name: Install dependencies
run: |
npm ci
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v7
with:
name: build-artifacts
- name: Unzip artifacts
@@ -129,6 +129,7 @@ jobs:
exit $EXIT_CODE
test-unit:
needs: build
timeout-minutes: 20
strategy:
matrix:
@@ -136,21 +137,16 @@ jobs:
runs-on: [ubuntu, macos, windows]
runs-on: ${{ matrix.runs-on }}-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }}.x
uses: actions/setup-node@v3
uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node-version }}.x
cache: 'npm'
- name: Install dependencies
run: |
npm ci
- name: Wait for build job
uses: NathanFirmo/wait-for-other-job@v1.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
job: build
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v7
with:
name: build-artifacts
- name: Unzip artifacts
@@ -166,6 +162,7 @@ jobs:
run: npm run test-unit --forbid-only
test-integration:
needs: build
timeout-minutes: 20
strategy:
matrix:
@@ -174,9 +171,9 @@ jobs:
browser: [chromium, firefox, webkit]
runs-on: ${{ matrix.runs-on }}
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }}.x
uses: actions/setup-node@v3
uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node-version }}.x
cache: 'npm'
@@ -185,12 +182,7 @@ jobs:
npm ci
- name: Install playwright
run: npx playwright install --with-deps ${{ matrix.browser }}
- name: Wait for build job
uses: NathanFirmo/wait-for-other-job@v1.1.1
with:
token: ${{ secrets.GITHUB_TOKEN }}
job: build
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v7
with:
name: build-artifacts
- name: Unzip artifacts
@@ -240,9 +232,9 @@ jobs:
matrix:
node-version: [22]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Use Node.js ${{ matrix.node-version }}.x
uses: actions/setup-node@v3
uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node-version }}.x
cache: 'npm'
@@ -251,7 +243,7 @@ jobs:
npm ci
- name: Install playwright
run: npx playwright install
- uses: actions/download-artifact@v4
- uses: actions/download-artifact@v7
with:
name: build-artifacts
- name: Unzip artifacts
+4 -4
View File
@@ -24,12 +24,12 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3
uses: actions/checkout@v5
- name: Initialize CodeQL
uses: github/codeql-action/init@v2
uses: github/codeql-action/init@v4
with:
languages: ${{ matrix.language }}
- name: Autobuild
uses: github/codeql-action/autobuild@v2
uses: github/codeql-action/autobuild@v4
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v2
uses: github/codeql-action/analyze@v4
+2 -2
View File
@@ -18,9 +18,9 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: Use Node.js 22.x
uses: actions/setup-node@v3
uses: actions/setup-node@v5
with:
node-version: 22.x
cache: 'npm'
+2 -2
View File
@@ -10,9 +10,9 @@ jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: Use Node.js 22.x
uses: actions/setup-node@v3
uses: actions/setup-node@v5
with:
node-version: 22.x
cache: 'npm'
+32 -2
View File
@@ -78,13 +78,43 @@ export class BrowserClipboardProvider implements IClipboardProvider {
}
}
/**
* TODO: Once the base64 handling on Uint8Arrays is more common,
* remove the btoa/atob fallbacks below.
*/
interface IUint8ArrayB64 extends Uint8Array {
toBase64(): string;
}
interface IUint8ArrayB64Ctor extends Uint8ArrayConstructor {
fromBase64(s: string): IUint8ArrayB64;
}
export class Base64 implements IBase64 {
public encodeText(data: string): string {
return btoa(data);
const bytes = new TextEncoder().encode(data) as IUint8ArrayB64;
if (bytes.toBase64 !== undefined) {
return bytes.toBase64();
}
let bin = '';
for (let i = 0; i < bytes.length; i++) {
bin += String.fromCharCode(bytes[i]);
}
return btoa(bin);
}
public decodeText(data: string): string {
if ((Uint8Array as IUint8ArrayB64Ctor).fromBase64 !== undefined) {
try {
return new TextDecoder().decode((Uint8Array as IUint8ArrayB64Ctor).fromBase64(data));
} catch {}
return '';
}
try {
return atob(data);
const bin = atob(data);
const bytes = new Uint8Array(bin.length);
for (let i = 0; i < bin.length; ++i) {
bytes[i] = bin.charCodeAt(i);
}
return new TextDecoder().decode(bytes);
} catch {}
return '';
}
@@ -29,7 +29,7 @@ test.describe('ClipboardAddon', () => {
await ctx.page.context().grantPermissions(['clipboard-read', 'clipboard-write']);
}
await ctx.page.evaluate(`
window.term.reset()
window.term.reset();
window.clipboard?.dispose();
window.clipboard = new ClipboardAddon();
window.term.loadAddon(window.clipboard);
@@ -92,4 +92,22 @@ test.describe('ClipboardAddon', () => {
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), '');
});
});
test.describe('non-ASCII', () => {
const testDataEncoded = '4oKsbWzDpMO8dMOf';
const testDataDecoded = '€mläütß';
test('write simple string', async () => {
await ctx.proxy.write(`\x1b]52;c;${testDataEncoded}\x07`);
deepEqual(await ctx.page.evaluate(() => window.navigator.clipboard.readText()), testDataDecoded);
});
test('read simple string', async () => {
await ctx.page.evaluate(`
window.data2 = [];
window.term.onData(e => data2.push(e));
`);
await ctx.page.evaluate((d) => window.navigator.clipboard.writeText(d), testDataDecoded);
await ctx.proxy.write(`\x1b]52;c;?\x07`);
deepEqual(await ctx.page.evaluate('window.data2'), [`\x1b]52;c;${testDataEncoded}\x07`]);
});
});
});
+8 -6
View File
@@ -142,12 +142,14 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
}
prevResult = result;
results.push(prevResult);
result = this._engine.find(
term,
prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row,
prevResult.col + prevResult.term.length >= this._terminal.cols ? 0 : prevResult.col + 1,
searchOptions
);
const cols = this._terminal.cols;
let nextCol = prevResult.col + prevResult.size;
let nextRow = prevResult.row;
if (nextCol >= cols) {
nextRow += Math.floor(nextCol / cols);
nextCol = nextCol % cols;
}
result = this._engine.find(term, nextRow, nextCol, searchOptions);
}
this._resultTracker.updateResults(results, this._highlightLimit);
@@ -414,6 +414,20 @@ test.describe('Search Tests', () => {
});
test.describe('Regression tests', () => {
test('should advance highlight-all scan by buffer match size for wide characters', async () => {
await ctx.page.evaluate(`
window.calls = [];
window.search.onDidChangeResults(e => window.calls.push(e));
`);
await ctx.proxy.resize(3, 5);
await ctx.proxy.write('𝄞𝄞𝄞');
strictEqual(await ctx.page.evaluate(`window.search.findNext('𝄞', { decorations: { activeMatchColorOverviewRuler: '#ff0000', matchOverviewRuler: '#ffff00' } })`), true);
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
{ resultCount: 3, resultIndex: 0 }
]);
await ctx.proxy.resize(80, 24);
});
test.describe('#2444 wrapped line content not being found', () => {
let fixture: string;
test.beforeAll(async () => {
+2 -1
View File
@@ -389,8 +389,9 @@ export class GlyphRenderer extends Disposable {
gl.bindTexture(gl.TEXTURE_2D, this._atlasTextures[i].texture);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, atlas.pages[i].canvas);
gl.generateMipmap(gl.TEXTURE_2D);
this._atlasTextures[i].version = atlas.pages[i].version;
}
+22 -6
View File
@@ -1080,17 +1080,33 @@ class AtlasPage {
size: number,
sourcePages?: AtlasPage[]
) {
if (sourcePages) {
for (const p of sourcePages) {
this._glyphs.push(...p.glyphs);
this._usedPixels += p._usedPixels;
}
}
this.canvas = createCanvas(document, size, size);
// The canvas needs alpha because we use clearColor to convert the background color to alpha.
// It might also contain some characters with transparent backgrounds if allowTransparency is
// set.
this.ctx = throwIfFalsy(this.canvas.getContext('2d', { alpha: true }));
if (sourcePages) {
if (sourcePages.length === 4) {
// optimized for quadmerge
this._glyphs = this._glyphs.concat(
sourcePages[0].glyphs,
sourcePages[1].glyphs,
sourcePages[2].glyphs,
sourcePages[3].glyphs
);
this._usedPixels = sourcePages[0]._usedPixels +
sourcePages[1]._usedPixels +
sourcePages[2]._usedPixels +
sourcePages[3]._usedPixels;
} else {
// fallback for non quadmerges (should never be used)
for (let i = 0; i < sourcePages.length; ++i) {
this._glyphs = this._glyphs.concat(sourcePages[i].glyphs);
this._usedPixels += sourcePages[i]._usedPixels;
}
}
}
}
public clear(): void {
+21 -6
View File
@@ -456,7 +456,18 @@ export class WebglRenderer extends Disposable implements IRenderer {
for (y = start; y <= end; y++) {
row = y + terminal.buffer.ydisp;
line = terminal.buffer.lines.get(row)!;
const bufferLine = terminal.buffer.lines.get(row);
if (!bufferLine) {
this._model.lineLengths[y] = 0;
for (x = 0; x < terminal.cols; x++) {
j = ((y * terminal.cols) + x) * RenderModelConstants.INDICIES_PER_CELL;
modelUpdated = true;
this._nullModelCell(x, y, j, 0, 0, 0);
}
this._setRowBlinkState(y, false);
continue;
}
line = bufferLine;
let rowHasBlinkingCells = false;
this._model.lineLengths[y] = 0;
isCursorRow = cursorY === row;
@@ -587,13 +598,9 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Null out non-first cells
for (x++; x <= lastCharX; x++) {
j = ((y * terminal.cols) + x) * RenderModelConstants.INDICIES_PER_CELL;
this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0, 0);
this._model.cells[j] = NULL_CELL_CODE;
// Don't re-resolve the cell color since multi-colored ligature backgrounds are not
// supported
this._model.cells[j + RenderModelConstants.BG_OFFSET] = this._cellColorResolver.result.bg;
this._model.cells[j + RenderModelConstants.FG_OFFSET] = this._cellColorResolver.result.fg;
this._model.cells[j + RenderModelConstants.EXT_OFFSET] = this._cellColorResolver.result.ext;
this._nullModelCell(x, y, j, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext);
}
x--; // Go back to the previous update cell for next iteration
}
@@ -607,6 +614,14 @@ export class WebglRenderer extends Disposable implements IRenderer {
this._updateTextBlinkState();
}
private _nullModelCell(x: number, y: number, cellIndex: number, bg: number, fg: number, ext: number): void {
this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, bg, fg, ext, NULL_CELL_CHAR, 0, 0);
this._model.cells[cellIndex] = NULL_CELL_CODE;
this._model.cells[cellIndex + RenderModelConstants.BG_OFFSET] = bg;
this._model.cells[cellIndex + RenderModelConstants.FG_OFFSET] = fg;
this._model.cells[cellIndex + RenderModelConstants.EXT_OFFSET] = ext;
}
private _resetBlinkingRowState(): void {
this._rowHasBlinkingCells = new Array(this._terminal.rows).fill(false);
this._rowHasBlinkingCellsCount = 0;
@@ -11,8 +11,33 @@ export class WebglWindow extends BaseWindow implements IControlWindow {
public readonly label = 'webgl';
private _textureAtlasContainer!: HTMLElement;
private _stressRunning = false;
private _stressScrolling: HTMLInputElement | undefined;
public build(container: HTMLElement): void {
const stressDiv = document.createElement('div');
const stressStart = document.createElement('button');
stressStart.id = 'stress-start';
stressStart.textContent = 'atlas stress start';
stressStart.addEventListener('click', () => this._stress());
stressDiv.appendChild(stressStart);
const stressStop = document.createElement('button');
stressStop.id = 'stress-stop';
stressStop.textContent = 'atlas stress stop';
stressStop.addEventListener('click', () => this._stressRunning = false);
stressDiv.appendChild(stressStop);
this._stressScrolling = document.createElement('input');
this._stressScrolling.type = 'checkbox';
this._stressScrolling.id = 'stress-scrolling';
stressDiv.appendChild(this._stressScrolling);
const stressScrollingLabel = document.createElement('label');
stressScrollingLabel.htmlFor = 'stress-scrolling';
stressScrollingLabel.textContent = 'stress scrolling';
stressDiv.appendChild(stressScrollingLabel);
container.appendChild(stressDiv);
const zoomCheckbox = document.createElement('input');
zoomCheckbox.type = 'checkbox';
zoomCheckbox.id = 'texture-atlas-zoom';
@@ -48,4 +73,33 @@ export class WebglWindow extends BaseWindow implements IControlWindow {
canvas.style.width = `${canvas.width / dpr}px`;
canvas.style.height = `${canvas.height / dpr}px`;
}
private async _stress(): Promise<void> {
const TEXT = '!"#$%&\'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~';
if (this._stressRunning) {
return;
}
this._stressRunning = true;
const scrolling = !!(this._stressScrolling?.checked);
for (let r = 0; r < 256; r += 2) {
for (let g = 0; g < 256; g += 2) {
let s: string[] = [];
for (let b = 0; b < 256; b += 2) {
if (!this._stressRunning) {
return;
}
const rbg = `RGB: ${[r, g, b]}`;
s.push(`\r\x1b[38;2;${r};${g};${b}m${rbg.padEnd(18, ' ')}${TEXT}`);
if (s.length >= 16) {
if (scrolling) {
await new Promise<void>(r => this._terminal.write(s.join('\r\n') + '\r\n', r));
} else {
await new Promise<void>(r => this._terminal.write('\x1b[H' + s.join('\r\n'), r));
}
s = [];
}
}
}
}
}
}
-4
View File
@@ -1072,10 +1072,6 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
* Clear the entire buffer, making the prompt line the new first line.
*/
public clear(): void {
if (this.buffer.ybase === 0 && this.buffer.y === 0) {
// Don't clear if it's already clear
return;
}
this.buffer.clearAllMarkers();
this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!);
this.buffer.lines.length = 1;
+14 -4
View File
@@ -536,9 +536,14 @@ export class DomRenderer extends Disposable implements IRenderer {
for (let y = start; y <= end; y++) {
const row = y + buffer.ydisp;
const rowElement = this._rowElements[y];
if (!rowElement) {
continue;
}
const lineData = buffer.lines.get(row);
if (!rowElement || !lineData) {
break;
if (!lineData) {
rowElement.replaceChildren();
this._setRowBlinkState(y, false);
continue;
}
rowElement.replaceChildren(
...this._rowFactory.createRow(
@@ -610,9 +615,14 @@ export class DomRenderer extends Disposable implements IRenderer {
for (let i = y; i <= y2; ++i) {
const row = i + buffer.ydisp;
const rowElement = this._rowElements[i];
if (!rowElement) {
continue;
}
const bufferline = buffer.lines.get(row);
if (!rowElement || !bufferline) {
break;
if (!bufferline) {
rowElement.replaceChildren();
this._setRowBlinkState(i, false);
continue;
}
rowElement.replaceChildren(
...this._rowFactory.createRow(
@@ -64,6 +64,13 @@ describe('SelectionModel', () => {
assert.deepEqual(model.finalSelectionStart, undefined);
assert.deepEqual(model.finalSelectionEnd, undefined);
});
it('should reset selection start to origin when start row is trimmed', () => {
model.selectionStart = [50, 0];
model.selectionEnd = [10, 2];
assert.equal(model.handleTrim(1), true);
assert.deepEqual(model.finalSelectionStart, [0, 0]);
assert.deepEqual(model.finalSelectionEnd, [10, 1]);
});
});
describe('finalSelectionStart', () => {
+3 -2
View File
@@ -135,9 +135,10 @@ export class SelectionModel {
return true;
}
// If the selection start is trimmed, ensure the start column is 0.
// If the selection start row is trimmed away, reset to the buffer origin.
if (this.selectionStart && this.selectionStart[1] < 0) {
this.selectionStart[1] = 0;
this.selectionStart = [0, 0];
return true;
}
return false;
}
@@ -100,6 +100,7 @@ export class CharacterJoinerService implements ICharacterJoinerService {
const ranges: [number, number][] = [];
const lineStr = line.translateToString(true);
const trimmedLength = line.getTrimmedLength();
// Because some cells can be represented by multiple javascript characters,
// we track the cell and the string indexes separately. This allows us to
@@ -111,7 +112,7 @@ export class CharacterJoinerService implements ICharacterJoinerService {
let rangeAttrFG = line.getFg(0);
let rangeAttrBG = line.getBg(0);
for (let x = 0; x < line.getTrimmedLength(); x++) {
for (let x = 0; x < trimmedLength; x++) {
line.loadCell(x, this._workCell);
if (this._workCell.getWidth() === 0) {
@@ -147,7 +148,7 @@ export class CharacterJoinerService implements ICharacterJoinerService {
}
// Process any trailing ranges.
if (this._bufferService.cols - rangeStartColumn > 1) {
if (trimmedLength - rangeStartColumn > 1) {
const joinedRanges = this._getJoinedRanges(
lineStr,
rangeStartStringIndex,
@@ -216,7 +217,8 @@ export class CharacterJoinerService implements ICharacterJoinerService {
return;
}
for (let x = startCol; x < this._bufferService.cols; x++) {
const trimmedLength = line.getTrimmedLength();
for (let x = startCol; x < trimmedLength; x++) {
const width = line.getWidth(x);
const length = line.getString(x).length || WHITESPACE_CELL_CHAR.length;
@@ -264,7 +266,7 @@ export class CharacterJoinerService implements ICharacterJoinerService {
// If there is still a range left at the end, it must extend all the way to
// the end of the line.
if (currentRange) {
currentRange[1] = this._bufferService.cols;
currentRange[1] = trimmedLength;
}
}
+1 -1
View File
@@ -704,7 +704,7 @@ export class SelectionService extends Disposable implements ISelectionService {
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
this._model.selectionEnd[0] = this._bufferService.cols;
}
this._model.selectionEnd[1] = Math.min(buffer.ydisp + this._bufferService.rows, buffer.lines.length - 1);
this._model.selectionEnd[1] = Math.min(buffer.ydisp + this._bufferService.rows - 1, buffer.lines.length - 1);
} else {
if (this._activeSelectionMode !== SelectionMode.COLUMN) {
this._model.selectionEnd[0] = 0;
+15
View File
@@ -495,6 +495,21 @@ describe('BufferLine', function(): void {
assert.deepEqual(columns, [0]);
});
});
describe('copyCellsFrom', () => {
it('should only move combined data within the copied range', () => {
const src = new TestBufferLine(10, NULL_CELL_DATA, false);
src.setCell(1, createCellData(1, 'a', 1));
src.addCodepointToCell(1, '\u0301'.charCodeAt(0), 0);
src.setCell(5, createCellData(1, 'z', 1));
src.addCodepointToCell(5, '\u0301'.charCodeAt(0), 0);
const dest = new TestBufferLine(10, NULL_CELL_DATA, false);
dest.copyCellsFrom(src, 0, 0, 2, false);
assert.property(dest.combined, '1');
assert.notProperty(dest.combined, '5');
});
});
describe('addCharToCell', () => {
it('should set width to 1 for empty cell', () => {
const line = new TestBufferLine(3, NULL_CELL_DATA, false);
+4
View File
@@ -609,9 +609,13 @@ export class BufferLine implements IBufferLine {
const srcStart = srcCol * Constants.CELL_INDICIES;
if (src._data[srcStart + Cell.CONTENT] & Content.IS_COMBINED_MASK) {
this._combined[destCol] = src._combined[srcCol];
} else {
delete this._combined[destCol];
}
if (src._data[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) {
this._extendedAttrs[destCol] = src._extendedAttrs[srcCol];
} else {
delete this._extendedAttrs[destCol];
}
}
+23
View File
@@ -10,6 +10,7 @@ import { CellData } from './CellData';
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Constants';
import { reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow';
import { IBufferLine } from './Types';
import { createCellData, NULL_CELL_DATA } from '../TestUtils.test';
const TEST_STRING_CACHE = new BufferLineStringCache();
@@ -122,5 +123,27 @@ describe('BufferReflow', () => {
const lines = createWrappedLines('abcde');
assert.notDeepEqual(reflowLargerGetLinesToRemove(lines, 1, 5, 10, nullCell, false), []);
});
it('should clear isWrapped on the last retained row after unwrap', () => {
const lines = new CircularList<BufferLine>(10);
const set2 = (line: BufferLine, s: string) => {
for (let i = 0; i < s.length; i++) {
line.setCell(i, createCellData(0, s[i], 1));
}
};
const l0 = new BufferLine(TEST_STRING_CACHE, 2);
set2(l0, 'ab');
const l1 = new BufferLine(TEST_STRING_CACHE, 2, undefined, true);
set2(l1, 'cd');
const l2 = new BufferLine(TEST_STRING_CACHE, 2, undefined, true);
set2(l2, 'ef');
lines.push(l0);
lines.push(l1);
lines.push(l2);
reflowLargerGetLinesToRemove(lines, 2, 6, 99, NULL_CELL_DATA, false);
assert.equal(l1.isWrapped, false);
});
});
});
+5
View File
@@ -99,6 +99,11 @@ export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, o
}
if (countToRemove > 0) {
wrappedLines[destLineIndex].isWrapped = false;
const lineAfterGroup = lines.get(y + wrappedLines.length);
if (lineAfterGroup) {
lineAfterGroup.isWrapped = false;
}
toRemove.push(y + wrappedLines.length - countToRemove); // index
toRemove.push(countToRemove);
}
+45 -19
View File
@@ -79,7 +79,7 @@ function assertDecodedRange(
}
const target = new Uint32Array(count);
const length = decode(input, target);
assert.equal(length, count);
assert.strictEqual(length, count);
let mismatchIndex = -1;
let index = 0;
for (let i = min; i < max; ++i) {
@@ -92,8 +92,8 @@ function assertDecodedRange(
}
index++;
}
assert.equal(mismatchIndex, -1);
assert.equal(outputToString(target, length), input);
assert.strictEqual(mismatchIndex, -1);
assert.strictEqual(outputToString(target, length), input);
}
const BATCH_SIZE = 8192;
@@ -116,9 +116,9 @@ describe('text encodings', () => {
const data = new Uint32Array(s.length);
for (let i = 0; i < s.length; ++i) {
data[i] = s.charCodeAt(i);
assert.equal(stringFromCodePoint(data[i]), s[i]);
assert.strictEqual(stringFromCodePoint(data[i]), s[i]);
}
assert.equal(utf32ToString(data), s);
assert.strictEqual(utf32ToString(data), s);
});
describe('StringToUtf32 decoder', () => {
@@ -159,7 +159,7 @@ describe('text encodings', () => {
const decoder = new StringToUtf32();
const target = new Uint32Array(5);
const length = decoder.decode(String.fromCharCode(0xFEFF), target);
assert.equal(length, 0);
assert.strictEqual(length, 0);
decoder.clear();
});
});
@@ -169,7 +169,7 @@ describe('text encodings', () => {
const target = new Uint32Array(500);
for (let i = 0; i < TEST_STRINGS.length; ++i) {
const length = decoder.decode(TEST_STRINGS[i], target);
assert.equal(toString(target, length), TEST_STRINGS[i]);
assert.strictEqual(toString(target, length), TEST_STRINGS[i]);
decoder.clear();
}
});
@@ -184,7 +184,7 @@ describe('text encodings', () => {
const written = decoder.decode(input[i], target);
decoded += toString(target, written);
}
assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
assert.strictEqual(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
});
});
});
@@ -226,7 +226,7 @@ describe('text encodings', () => {
const target = new Uint32Array(5);
const utf8Data = stringToUtf8Bytes(String.fromCharCode(0xFEFF));
const length = decoder.decode(utf8Data, target);
assert.equal(length, 0);
assert.strictEqual(length, 0);
decoder.clear();
});
});
@@ -237,7 +237,7 @@ describe('text encodings', () => {
for (let i = 0; i < TEST_STRINGS.length; ++i) {
const utf8Data = stringToUtf8Bytes(TEST_STRINGS[i]);
const length = decoder.decode(utf8Data, target);
assert.equal(toString(target, length), TEST_STRINGS[i]);
assert.strictEqual(toString(target, length), TEST_STRINGS[i]);
decoder.clear();
}
});
@@ -252,7 +252,7 @@ describe('text encodings', () => {
const written = decoder.decode(utf8Data.slice(i, i + 1), target);
decoded += toString(target, written);
}
assert(decoded, 'ÄÖÜßöäü');
assert.strictEqual(decoded, 'ÄÖÜßöäü');
});
it('2/3 byte sequences - advance by 1', () => {
@@ -264,7 +264,7 @@ describe('text encodings', () => {
const written = decoder.decode(utf8Data.slice(i, i + 1), target);
decoded += toString(target, written);
}
assert(decoded, 'Āր܀߀ö€ä€ü');
assert.strictEqual(decoded, 'Āր܀߀ö€ä€ü');
});
it('2/3/4 byte sequences - advance by 1', () => {
@@ -276,7 +276,7 @@ describe('text encodings', () => {
const written = decoder.decode(utf8Data.slice(i, i + 1), target);
decoded += toString(target, written);
}
assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
assert.strictEqual(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
});
it('2/3/4 byte sequences - advance by 2', () => {
@@ -288,7 +288,7 @@ describe('text encodings', () => {
const written = decoder.decode(utf8Data.slice(i, i + 2), target);
decoded += toString(target, written);
}
assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
assert.strictEqual(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
});
it('2/3/4 byte sequences - advance by 3', () => {
@@ -300,7 +300,7 @@ describe('text encodings', () => {
const written = decoder.decode(utf8Data.slice(i, i + 3), target);
decoded += toString(target, written);
}
assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
assert.strictEqual(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
});
it('BOMs (3 byte sequences) - advance by 2', () => {
@@ -312,7 +312,7 @@ describe('text encodings', () => {
const written = decoder.decode(utf8Data.slice(i, i + 2), target);
decoded += toString(target, written);
}
assert.equal(decoded, '');
assert.strictEqual(decoded, '');
});
it('test break after 3 bytes - issue #2495', () => {
@@ -320,11 +320,37 @@ describe('text encodings', () => {
const target = new Uint32Array(5);
const utf8Data = fromByteString('\xf0\xa0\x9c\x8e');
let written = decoder.decode(utf8Data.slice(0, 3), target);
assert.equal(written, 0);
assert.strictEqual(written, 0);
written = decoder.decode(utf8Data.slice(3), target);
assert.equal(written, 1);
assert(toString(target, written), '𠜎');
assert.strictEqual(written, 1);
assert.strictEqual(toString(target, written), '𠜎');
});
describe('0x80 not swallowed in continuation', () => {
it('A—B', () => {
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
const utf8Data = new TextEncoder().encode('A—BA—BA—BA—BA—B');
let decoded = '';
for (let i = 0; i < utf8Data.length; i += 2) {
const written = decoder.decode(utf8Data.slice(i, i + 2), target);
decoded += toString(target, written);
}
assert.strictEqual(decoded, 'A—BA—BA—BA—BA—B');
});
it('A𐀀B', () => {
const decoder = new Utf8ToUtf32();
const target = new Uint32Array(5);
const utf8Data = new TextEncoder().encode('A𐀀BA𐀀BA𐀀BA𐀀BA𐀀B');
let decoded = '';
for (let i = 0; i < utf8Data.length; i += 2) {
const written = decoder.decode(utf8Data.slice(i, i + 2), target);
decoded += toString(target, written);
}
assert.strictEqual(decoded, 'A𐀀BA𐀀BA𐀀BA𐀀BA𐀀B');
});
});
});
});
});
+2 -2
View File
@@ -158,9 +158,9 @@ export class Utf8ToUtf32 {
cp &= ((((cp & 0xE0) === 0xC0)) ? 0x1F : (((cp & 0xF0) === 0xE0)) ? 0x0F : 0x07);
let pos = 0;
let tmp: number;
while ((tmp = this.interim[++pos] & 0x3F) && pos < 4) {
while ((tmp = this.interim[++pos]) && pos < 4) {
cp <<= 6;
cp |= tmp;
cp |= tmp & 0x3F;
}
// missing bytes - read ahead from input
const type = (((this.interim[0] & 0xE0) === 0xC0)) ? 2 : (((this.interim[0] & 0xF0) === 0xE0)) ? 3 : 4;
-4
View File
@@ -99,10 +99,6 @@ export class Terminal extends CoreTerminal {
* Clear the entire buffer, making the prompt line the new first line.
*/
public clear(): void {
if (this.buffer.ybase === 0 && this.buffer.y === 0) {
// Don't clear if it's already clear
return;
}
this.buffer.clearAllMarkers();
this.buffer.lines.set(0, this.buffer.lines.get(this.buffer.ybase + this.buffer.y)!);
this.buffer.lines.length = 1;
+88 -114
View File
@@ -208,14 +208,14 @@ test.describe('InputHandler Integration Tests', () => {
await ctx.proxy.write('\n\rdef\x1b[2;1H\x1b[2P');
await pollFor(ctx.page, () => getLinesAsArray(2), ['bc', 'f']);
});
test('CSI Pm # P - XTPUSHCOLORS: Push current dynamic- and ANSI-palette colors onto stack, xterm', async () => {
await assertNoModeChange('\x1b[#P');
test.skip('CSI Pm # P - XTPUSHCOLORS: Push current dynamic- and ANSI-palette colors onto stack, xterm', async () => {
// TODO: Implement
});
test('CSI Pm # Q - XTPOPCOLORS: Pop stack to set dynamic- and ANSI-palette colors, xterm', async () => {
await assertNoModeChange('\x1b[#Q');
test.skip('CSI Pm # Q - XTPOPCOLORS: Pop stack to set dynamic- and ANSI-palette colors, xterm', async () => {
// TODO: Implement
});
test('CSI # R - XTREPORTCOLORS: Report the current entry on the palette stack, and the number of palettes stored on the stack, using the same form as XTPOPCOLOR (default = 0), xterm', async () => {
await assertNoModeChange('\x1b[#R');
test.skip('CSI # R - XTREPORTCOLORS: Report the current entry on the palette stack, and the number of palettes stored on the stack, using the same form as XTPOPCOLOR (default = 0), xterm', async () => {
// TODO: Implement
});
test('CSI Ps S - SU: Scroll up Ps lines (default = 1), VT420, ECMA-48', async () => {
await ctx.proxy.write('1\r\n2\r\n3\r\n4\r\n5');
@@ -240,11 +240,11 @@ test.describe('InputHandler Integration Tests', () => {
await ctx.proxy.write('\x1b[2T');
await pollFor(ctx.page, () => getLinesAsArray(5), ['', '', '1', '2', '3']);
});
// Not implemented; the 5-parameter form is currently parsed as SD (scroll down) using the first parameter.
test.skip('CSI Ps ; Ps ; Ps ; Ps ; Ps T - XTHIMOUSE: Initiate highlight mouse tracking (XTHIMOUSE), xterm', async () => {
// TODO: Implement
});
test('CSI > Pm T - XTRMTITLE: Reset title mode features to default value, xterm', async () => {
await assertNoModeChange('\x1b[>T');
test.skip('CSI > Pm T - XTRMTITLE: Reset title mode features to default value, xterm', async () => {
// TODO: Implement
});
test('CSI Ps X - ECH: Erase Ps Character(s) (default = 1)', async () => {
await ctx.proxy.write('abcdef\x1b[1;1H\x1b[X');
@@ -320,8 +320,8 @@ test.describe('InputHandler Integration Tests', () => {
await ctx.proxy.write('\x1b[c');
await pollFor(ctx.page, () => recordedData, ['\x1b[?1;2c']);
});
test('CSI = Ps c - ', async () => {
await assertNoModeChange('\x1b[=c');
test.skip('CSI = Ps c - ', async () => {
// TODO: Implement
});
test('CSI > Ps c - ', async () => {
await ctx.proxy.write('\x1b[>c');
@@ -491,7 +491,6 @@ test.describe('InputHandler Integration Tests', () => {
await pollFor(ctx.page, () => getLinesAsArray(2), ['1234Y', 'X']);
await ctx.proxy.resize(80, 24);
});
// Mode 45 is XTREVWRAP; covered by the test above.
test.skip('Ps = 4 5 - Enable Graphic Print Color Syntax (DECGPCS), VT340', async () => {
});
test('Ps = 4 6 - Start logging (XTLOGGING), xterm', async () => {
@@ -512,7 +511,6 @@ test.describe('InputHandler Integration Tests', () => {
await pollFor(ctx.page, async () => await ctx.proxy.buffer.active.type, 'normal');
await pollFor(ctx.page, () => getLinesAsArray(1), ['main']);
});
// Mode 47 is the alternate screen buffer; covered by the test above.
test.skip('Ps = 4 7 - Enable Graphic Rotated Print Mode (DECGRPM), VT340', async () => {
});
test('Ps = 6 6 - Application keypad mode (DECNKM), VT320', async () => {
@@ -726,11 +724,11 @@ test.describe('InputHandler Integration Tests', () => {
await assertNoModeChange('\x1b[?2006h');
});
});
test('CSI Ps i - MC: Media Copy', async () => {
await assertNoModeChange('\x1b[0i');
test.skip('CSI Ps i - MC: Media Copy', async () => {
// TODO: Implement
});
test('CSI ? Ps i - MC: Media Copy, DEC-specified', async () => {
await assertNoModeChange('\x1b[?0i');
test.skip('CSI ? Ps i - MC: Media Copy, DEC-specified', async () => {
// TODO: Implement
});
test('CSI Pm l - RM: Reset Mode', async () => {
await ctx.proxy.write('\x1b[4h\x1b[20h');
@@ -858,7 +856,6 @@ test.describe('InputHandler Integration Tests', () => {
await ctx.proxy.write('\x1b[?45l');
await pollFor(ctx.page, async () => (await ctx.proxy.modes).reverseWraparoundMode, false);
});
// Mode 45 is XTREVWRAP; covered by the test above.
test.skip('Ps = 4 5 - Disable Graphic Print Color Syntax (DECGPCS), VT340.', async () => {
});
test('Ps = 4 6 - Stop logging (XTLOGGING), xterm. This is normally disabled by a compile-time option.', async () => {
@@ -870,7 +867,6 @@ test.describe('InputHandler Integration Tests', () => {
await ctx.proxy.write('\x1b[?47l');
await pollFor(ctx.page, async () => await ctx.proxy.buffer.active.type, 'normal');
});
// Mode 47 is the alternate screen buffer; covered by the test above.
test.skip('Ps = 4 7 - Disable Graphic Rotated Print Mode (DECGRPM), VT340.', async () => {
});
test('Ps = 6 6 - Numeric keypad mode (DECNKM), VT320.', async () => {
@@ -1402,11 +1398,11 @@ test.describe('InputHandler Integration Tests', () => {
deepStrictEqual(await cell!.getBgColor(), 0x123456);
});
});
test('CSI > Pp [; Pv] m - XTMODKEYS: Set/reset key modifier options, xterm', async () => {
await assertNoModeChange('\x1b[>2;1m');
test.skip('CSI > Pp [; Pv] m - XTMODKEYS: Set/reset key modifier options, xterm', () => {
// TODO: Implement
});
test('CSI ? Pp m - XTQMODKEYS: Query key modifier options, xterm', async () => {
await assertNoModeChange('\x1b[?27m');
test.skip('CSI ? Pp m - XTQMODKEYS: Query key modifier options, xterm', () => {
// TODO: Implement
});
test.describe('CSI Ps n - DSR: Device Status Report', () => {
test('Status Report - CSI 5 n', async () => {
@@ -1434,8 +1430,8 @@ test.describe('InputHandler Integration Tests', () => {
deepStrictEqual(recordedData, ['\x1b[?3;4R']);
});
});
test('CSI > Ps n - Disable key modifier options, xterm', async () => {
await assertNoModeChange('\x1b[>0n');
test.skip('CSI > Ps n - Disable key modifier options, xterm', () => {
// TODO: Implement
});
test.describe('CSI ? Ps n - DECDSR: Device Status Report (DEC-specific)', () => {
test('Color Scheme Query - CSI ? 996 n (dark theme)', async () => {
@@ -1462,8 +1458,8 @@ test.describe('InputHandler Integration Tests', () => {
await ctx.page.evaluate(`window.term.options.vtExtensions = { colorSchemeQuery: true }`);
});
});
test('CSI > Ps p - XTSMPOINTER: Set resource value pointerMode, xterm', async () => {
await assertNoModeChange('\x1b[>3p');
test.skip('CSI > Ps p - XTSMPOINTER: Set resource value pointerMode, xterm', () => {
// TODO: Implement
});
test('CSI ! p - DECSTR: Soft terminal reset, VT220 and up.', async () => {
const rows = await ctx.proxy.rows;
@@ -1475,8 +1471,8 @@ test.describe('InputHandler Integration Tests', () => {
await pollFor(ctx.page, async () => (await ctx.proxy.modes).originMode, false);
await pollFor(ctx.page, () => ctx.page.evaluate(`({ top: window.term._core._bufferService.buffer.scrollTop, bottom: window.term._core._bufferService.buffer.scrollBottom })`), { top: 0, bottom: rows - 1 });
});
test('CSI Pl ; Pc " p - DECSCL: Set conformance level, VT220 and up.', async () => {
await assertNoModeChange('\x1b[61;0"p');
test.skip('CSI Pl ; Pc " p - DECSCL: Set conformance level, VT220 and up.', () => {
// TODO: Implement
});
test('CSI Ps $ p - DECRQM: Request ANSI mode', async () => {
await ctx.proxy.write('\x1b[4h');
@@ -1503,8 +1499,8 @@ test.describe('InputHandler Integration Tests', () => {
await ctx.proxy.write('\x1b[?1$p');
deepStrictEqual(recordedData, ['\x1b[?1;2$y']);
});
test('CSI [Pm] # p - Push video attributes onto stack (XTPUSHSGR), xterm. This is an alias for CSI # { , used to work around language limitations of C#.', async () => {
await assertNoModeChange('\x1b[#p');
test.skip('CSI [Pm] # p - Push video attributes onto stack (XTPUSHSGR), xterm. This is an alias for CSI # { , used to work around language limitations of C#.', async () => {
// TODO: Implement
});
test('CSI > Ps q - Report xterm name and version (XTVERSION).', async () => {
await ctx.proxy.write('\x1b[>q');
@@ -1512,8 +1508,8 @@ test.describe('InputHandler Integration Tests', () => {
ok(recordedData[0].startsWith('\x1bP>|xterm.js('));
ok(recordedData[0].endsWith('\x1b\\'));
});
test('CSI Ps q - Load LEDs (DECLL), VT100.', async () => {
await assertNoModeChange('\x1b[0q');
test.skip('CSI Ps q - Load LEDs (DECLL), VT100.', async () => {
// TODO: Implement
});
test('CSI Ps SP q - Set cursor style (DECSCUSR), VT520.', async () => {
const getCursorMode = async () => ctx.proxy.core.evaluate(([core]) => {
@@ -1544,8 +1540,8 @@ test.describe('InputHandler Integration Tests', () => {
await ctx.proxy.write('\x1b[1;1H\x1b[?2K');
await pollFor(ctx.page, () => getLinesAsArray(1), ['PROT']);
});
test('CSI # q - Pop video attributes from stack (XTPOPSGR), xterm.', async () => {
await assertNoModeChange('\x1b[#q');
test.skip('CSI # q - Pop video attributes from stack (XTPOPSGR), xterm.', async () => {
// TODO: Implement
});
test('CSI Ps ; Ps r - Set Scrolling Region [top;bottom] (default = full size of window) (DECSTBM), VT100.', async () => {
const rows = await ctx.proxy.rows;
@@ -1554,11 +1550,11 @@ test.describe('InputHandler Integration Tests', () => {
await ctx.proxy.write('\x1b[r');
await pollFor(ctx.page, () => ctx.page.evaluate(`({ top: window.term._core._bufferService.buffer.scrollTop, bottom: window.term._core._bufferService.buffer.scrollBottom })`), { top: 0, bottom: rows - 1 });
});
test('CSI ? Pm r - Restore DEC Private Mode Values (XTRESTORE), xterm.', async () => {
await assertNoModeChange('\x1b[?1r');
test.skip('CSI ? Pm r - Restore DEC Private Mode Values (XTRESTORE), xterm.', async () => {
// TODO: Implement
});
test('CSI Pt ; Pl ; Pb ; Pr ; Pm $ r - Change Attributes in Rectangular Area (DECCARA), VT400 and up.', async () => {
await assertNoModeChange('\x1b[1;1;1;1;0$r');
test.skip('CSI Pt ; Pl ; Pb ; Pr ; Pm $ r - Change Attributes in Rectangular Area (DECCARA), VT400 and up.', async () => {
// TODO: Implement
});
test('CSI s - Save cursor, available only when DECLRMM is disabled (SCOSC, also ANSI.SYS).', async () => {
await ctx.proxy.write('\x1b[3;4H');
@@ -1567,23 +1563,23 @@ test.describe('InputHandler Integration Tests', () => {
await ctx.proxy.write('\x1b[u');
await pollFor(ctx.page, () => getCursor(), { col: 3, row: 2 });
});
// Parsed as SCOSC (CSI s) today; keep skipped until DECSLRM is implemented.
test.skip('CSI Pl ; Pr s - Set left and right margins (DECSLRM), VT420 and up.', async () => {
// TODO: Implement
});
test('CSI > Ps s - Set/reset shift-escape options (XTSHIFTESCAPE), xterm.', async () => {
await assertNoModeChange('\x1b[>0s');
test.skip('CSI > Ps s - Set/reset shift-escape options (XTSHIFTESCAPE), xterm.', async () => {
// TODO: Implement
});
test('CSI ? Pm s - Save DEC Private Mode Values (XTSAVE), xterm. Ps values are the same as for DECSET.', async () => {
await assertNoModeChange('\x1b[?1s');
test.skip('CSI ? Pm s - Save DEC Private Mode Values (XTSAVE), xterm. Ps values are the same as for DECSET.', async () => {
// TODO: Implement
});
test('CSI > Pm t - This xterm control sets one or more features of the title modes (XTSMTITLE), xterm.', async () => {
await assertNoModeChange('\x1b[>3t');
test.skip('CSI > Pm t - This xterm control sets one or more features of the title modes (XTSMTITLE), xterm.', async () => {
// TODO: Implement
});
test('CSI Ps SP t - Set warning-bell volume (DECSWBV), VT520.', async () => {
await assertNoModeChange('\x1b[1 t');
test.skip('CSI Ps SP t - Set warning-bell volume (DECSWBV), VT520.', async () => {
// TODO: Implement
});
test('CSI Pt ; Pl ; Pb ; Pr ; Pm $ t - Reverse Attributes in Rectangular Area (DECRARA), VT400 and up.', async () => {
await assertNoModeChange('\x1b[1;1;1;1;0$t');
test.skip('CSI Pt ; Pl ; Pb ; Pr ; Pm $ t - Reverse Attributes in Rectangular Area (DECRARA), VT400 and up.', async () => {
// TODO: Implement
});
test('CSI u - Restore cursor (SCORC, also ANSI.SYS).', async () => {
await ctx.proxy.write('\x1b[4;6H');
@@ -1592,62 +1588,62 @@ test.describe('InputHandler Integration Tests', () => {
await ctx.proxy.write('\x1b[u');
await pollFor(ctx.page, () => getCursor(), { col: 5, row: 3 });
});
test('CSI Ps SP u - Set margin-bell volume (DECSMBV), VT520.', async () => {
await assertNoModeChange('\x1b[1 u');
test.skip('CSI Ps SP u - Set margin-bell volume (DECSMBV), VT520.', async () => {
// TODO: Implement
});
test('CSI Pt ; Pl ; Pb ; Pr ; Pp ; Pt ; Pl ; Pp $ v - Copy Rectangular Area (DECCRA), VT400 and up.', async () => {
await assertNoModeChange('\x1b[1;1;1;1;1;1;1;1$v');
test.skip('CSI Pt ; Pl ; Pb ; Pr ; Pp ; Pt ; Pl ; Pp $ v - Copy Rectangular Area (DECCRA), VT400 and up.', async () => {
// TODO: Implement
});
test('CSI Ps $ w - Request presentation state report (DECRQPSR), VT320 and up.', async () => {
await assertNoModeChange('\x1b[1$w');
test.skip('CSI Ps $ w - Request presentation state report (DECRQPSR), VT320 and up.', async () => {
// TODO: Implement
});
test('CSI Pt ; Pl ; Pb ; Pr \' w - Enable Filter Rectangle (DECEFR), VT420 and up.', async () => {
await assertNoModeChange('\x1b[1;1;1;1\'w');
test.skip('CSI Pt ; Pl ; Pb ; Pr \' w - Enable Filter Rectangle (DECEFR), VT420 and up.', async () => {
// TODO: Implement
});
test('CSI Ps x - Request Terminal Parameters (DECREQTPARM).', async () => {
await assertNoModeChange('\x1b[0x');
test.skip('CSI Ps x - Request Terminal Parameters (DECREQTPARM).', async () => {
// TODO: Implement
});
test('CSI Ps * x - Select Attribute Change Extent (DECSACE), VT420 and up.', async () => {
await assertNoModeChange('\x1b[0*x');
test.skip('CSI Ps * x - Select Attribute Change Extent (DECSACE), VT420 and up.', async () => {
// TODO: Implement
});
test('CSI Pc ; Pt ; Pl ; Pb ; Pr $ x - Fill Rectangular Area (DECFRA), VT420 and up.', async () => {
await assertNoModeChange('\x1b[0;0;1;1;1$x');
test.skip('CSI Pc ; Pt ; Pl ; Pb ; Pr $ x - Fill Rectangular Area (DECFRA), VT420 and up.', async () => {
// TODO: Implement
});
test('CSI Ps # y - Select checksum extension (XTCHECKSUM), xterm.', async () => {
await assertNoModeChange('\x1b[0#y');
test.skip('CSI Ps # y - Select checksum extension (XTCHECKSUM), xterm.', async () => {
// TODO: Implement
});
test('CSI Pi ; Pg ; Pt ; Pl ; Pb ; Pr * y - Request Checksum of Rectangular Area (DECRQCRA), VT420 and up.', async () => {
await assertNoModeChange('\x1b[0;0;1;1;1*y');
test.skip('CSI Pi ; Pg ; Pt ; Pl ; Pb ; Pr * y - Request Checksum of Rectangular Area (DECRQCRA), VT420 and up.', async () => {
// TODO: Implement
});
test('CSI Ps ; Pu \' z - Enable Locator Reporting (DECELR).', async () => {
await assertNoModeChange('\x1b[0\'z');
test.skip('CSI Ps ; Pu \' z - Enable Locator Reporting (DECELR).', async () => {
// TODO: Implement
});
test('CSI Pt ; Pl ; Pb ; Pr $ z - Erase Rectangular Area (DECERA), VT400 and up.', async () => {
await assertNoModeChange('\x1b[1;1;1;1$z');
test.skip('CSI Pt ; Pl ; Pb ; Pr $ z - Erase Rectangular Area (DECERA), VT400 and up.', async () => {
// TODO: Implement
});
test('CSI Pm \' { - Select Locator Events (DECSLE).', async () => {
await assertNoModeChange('\x1b[0\'{');
test.skip('CSI Pm \' { - Select Locator Events (DECSLE).', async () => {
// TODO: Implement
});
test('CSI [Pm] # { Push video attributes onto stack (XTPUSHSGR), xterm.', async () => {
await assertNoModeChange('\x1b[#{');
test.skip('CSI [Pm] # { Push video attributes onto stack (XTPUSHSGR), xterm.', async () => {
// TODO: Implement
});
test('CSI Pt ; Pl ; Pb ; Pr $ { - Selective Erase Rectangular Area (DECSERA), VT400 and up.', async () => {
await assertNoModeChange('\x1b[1;1;1;1${');
test.skip('CSI Pt ; Pl ; Pb ; Pr $ { - Selective Erase Rectangular Area (DECSERA), VT400 and up.', async () => {
// TODO: Implement
});
test('CSI Pt ; Pl ; Pb ; Pr # | - Report selected graphic rendition (XTREPORTSGR), xterm.', async () => {
await assertNoModeChange('\x1b[1;1;1;1#|');
test.skip('CSI Pt ; Pl ; Pb ; Pr # | - Report selected graphic rendition (XTREPORTSGR), xterm.', async () => {
// TODO: Implement
});
test('CSI Ps $ | - Select columns per page (DECSCPP), VT340.', async () => {
await assertNoModeChange('\x1b[80$|');
test.skip('CSI Ps $ | - Select columns per page (DECSCPP), VT340.', async () => {
// TODO: Implement
});
test('CSI Ps \' | - Request Locator Position (DECRQLP).', async () => {
await assertNoModeChange('\x1b[0\'|');
test.skip('CSI Ps \' | - Request Locator Position (DECRQLP).', async () => {
// TODO: Implement
});
test('CSI Ps * | - Select number of lines per screen (DECSNLS), VT420 and up.', async () => {
await assertNoModeChange('\x1b[24*|');
test.skip('CSI Ps * | - Select number of lines per screen (DECSNLS), VT420 and up.', async () => {
// TODO: Implement
});
test('CSI # } - Pop video attributes from stack (XTPOPSGR), xterm.', async () => {
await assertNoModeChange('\x1b[#}');
test.skip('CSI # } - Pop video attributes from stack (XTPOPSGR), xterm.', async () => {
// TODO: Implement
});
test('CSI Ps \' } - Insert Ps Column(s) (default = 1) (DECIC), VT420 and up.', async () => {
await ctx.proxy.resize(5, 5);
@@ -1656,8 +1652,8 @@ test.describe('InputHandler Integration Tests', () => {
await ctx.proxy.write('\x1b[\'}');
await pollFor(ctx.page, () => getLinesAsArray(6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
});
test('CSI Ps $ } - Select active status display (DECSASD), VT320 and up.', async () => {
await assertNoModeChange('\x1b[0$}');
test.skip('CSI Ps $ } - Select active status display (DECSASD), VT320 and up.', async () => {
// TODO: Implement
});
test('CSI Ps \' ~ - Delete Ps Column(s) (default = 1) (DECDC), VT420 and up.', async () => {
await ctx.proxy.resize(5, 5);
@@ -1666,8 +1662,8 @@ test.describe('InputHandler Integration Tests', () => {
await ctx.proxy.write('\x1b[\'~');
await pollFor(ctx.page, () => getLinesAsArray(6), ['12345', '1245', '1245', '1245', '1245', '1245']);
});
test('CSI Ps $ ~ - Select status line type (DECSSDT), VT320 and up.', async () => {
await assertNoModeChange('\x1b[0$~');
test.skip('CSI Ps $ ~ - Select status line type (DECSSDT), VT320 and up.', async () => {
// TODO: Implement
});
test.describe('CSI Ps ; Ps ; Ps t - Window Options', () => {
test('should be disabled by default', async () => {
@@ -1690,28 +1686,6 @@ test.describe('InputHandler Integration Tests', () => {
const d = await getDimensions();
deepStrictEqual(recordedData, [`\x1b[6;${d.cellHeight};${d.cellWidth}t`]);
});
test('18 - GetWinSizeChars', async () => {
await ctx.proxy.setOption('windowOptions', { getWinSizeChars: true });
await ctx.proxy.write('\x1b[18t');
deepStrictEqual(recordedData, [`\x1b[8;${await ctx.proxy.rows};${await ctx.proxy.cols}t`]);
});
test('22/23 - PushTitle/PopTitle', async () => {
const titles: string[] = [];
const titleDisposable = ctx.proxy.onTitleChange(t => titles.push(t));
await ctx.proxy.setOption('windowOptions', { pushTitle: true, popTitle: true });
await ctx.proxy.write('\x1b]0;1\x07');
await ctx.proxy.write('\x1b[22t');
await ctx.proxy.write('\x1b]0;2\x07');
await ctx.proxy.write('\x1b[22t');
await ctx.proxy.write('\x1b]0;3\x07');
await ctx.proxy.write('\x1b[22t');
await pollFor(ctx.page, () => titles, ['1', '2', '3']);
await ctx.proxy.write('\x1b[23t');
await ctx.proxy.write('\x1b[23t');
await ctx.proxy.write('\x1b[23t');
await pollFor(ctx.page, () => titles, ['1', '2', '3', '3', '2', '1']);
titleDisposable.dispose();
});
});
});