Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2f5c424a8 | ||
|
|
3cdbc77386 | ||
|
|
f57b8e1534 | ||
|
|
c8f11d63b5 | ||
|
|
3d929a1db0 | ||
|
|
2fe3fd13a1 | ||
|
|
f33f502267 | ||
|
|
7c24c58007 | ||
|
|
596634ac41 | ||
|
|
0c8271db49 | ||
|
|
f3f4dcaf9f | ||
|
|
64718444e2 | ||
|
|
e9c648f519 | ||
|
|
60bae9528d |
+19
-27
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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`]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user