Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
08b1db2c9d | ||
|
|
53a98a720a | ||
|
|
b69ae4b3c9 | ||
|
|
3cdbc77386 | ||
|
|
f57b8e1534 | ||
|
|
c8f11d63b5 | ||
|
|
3d929a1db0 | ||
|
|
2fe3fd13a1 | ||
|
|
f33f502267 | ||
|
|
7c24c58007 | ||
|
|
596634ac41 | ||
|
|
0c8271db49 | ||
|
|
f3f4dcaf9f | ||
|
|
64718444e2 | ||
|
|
ea75b99e2b | ||
|
|
564113e053 | ||
|
|
b3de9fa6db | ||
|
|
1f054097f3 | ||
|
|
a250987508 | ||
|
|
21c99d0574 | ||
|
|
0b6fd2e8b2 | ||
|
|
4226e64245 | ||
|
|
a374c6c5af | ||
|
|
454e0749c2 | ||
|
|
59264f1f5b | ||
|
|
16ef64f8bf | ||
|
|
a0f93cdcc4 | ||
|
|
5b911db9cf | ||
|
|
6426573f45 | ||
|
|
096707d736 | ||
|
|
967d3f59e6 | ||
|
|
9a70816dad | ||
|
|
8e7e4f16e5 | ||
|
|
a9a2a8e3c1 | ||
|
|
92a1bfcf2e | ||
|
|
be38252e37 | ||
|
|
0b590dacd6 | ||
|
|
1faa41eac1 | ||
|
|
fa5eb33cdb | ||
|
|
3494f6b5b0 | ||
|
|
58fc09ff0e | ||
|
|
a47e4905e6 | ||
|
|
1252c69ccc | ||
|
|
bc26cc16c1 | ||
|
|
80468eeb4c | ||
|
|
69b11a1a48 | ||
|
|
98d7c25bd5 | ||
|
|
6d1f12cb7b | ||
|
|
4500485e79 | ||
|
|
b4bd92d62c | ||
|
|
1a01e9634f | ||
|
|
e35a9c1e90 | ||
|
|
1dd113b38a | ||
|
|
536eeb4db0 | ||
|
|
3f3f6abfb9 | ||
|
|
c961408bf7 |
+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'
|
||||
|
||||
@@ -254,6 +254,7 @@ Xterm.js is used in many world-class applications to provide great terminal expe
|
||||
- [**WooTTY**](https://github.com/icoretech/wootty): Flawless browser terminal for real operators.
|
||||
- [**Orca**](https://github.com/stablyai/orca): Agentic development environment for 100x builders.
|
||||
- [**4bit**](https://ciembor.github.io/4bit): Terminal Color Scheme Designer.
|
||||
- [**Cate**](https://github.com/0-AI-UG/cate): Open source desktop IDE on an infinite zoomable canvas. Editor, browser, and terminal panels float in spatial space; the terminals are xterm.js with the WebGL addon, backed by node-pty.
|
||||
- [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D)
|
||||
|
||||
Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Please add any new contributions to the end of the list.
|
||||
|
||||
@@ -15,7 +15,7 @@ test.beforeAll(async ({ browser }) => {
|
||||
});
|
||||
test.afterAll(async () => await ctx.page.close());
|
||||
|
||||
test.describe('Search Tests', () => {
|
||||
test.describe('AttachAddon', () => {
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await ctx.proxy.reset();
|
||||
|
||||
@@ -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`]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+1
-1
@@ -82,7 +82,7 @@ declare module '@xterm/addon-clipboard' {
|
||||
/**
|
||||
* The clipboard provider interface that enables xterm.js to access the browser clipboard.
|
||||
*
|
||||
* Note this this clipboard provider does not implement own cut buffers as xterm does,
|
||||
* Note that this clipboard provider does not implement own cut buffers as xterm does,
|
||||
* but redirect the selection parameter always to navigator.clipboard.
|
||||
*/
|
||||
export class BrowserClipboardProvider implements IClipboardProvider{
|
||||
|
||||
@@ -90,7 +90,7 @@ terminal.loadAddon(imageAddon);
|
||||
therefore palette animations cannot be used.
|
||||
|
||||
- **SIXEL Raster Attributes Handling**
|
||||
If raster attributes were found in the SIXEL data (level 2), the image will always be truncated to the given height/width extend. We deviate here from the specification on purpose, as it allows several processing optimizations. For level 1 SIXEL data without any raster attributes the image can freely grow in width and height up to the last data byte, which has a much higher processing penalty. In general encoding libraries should not create level 1 data anymore and should not produce pixel information beyond the announced height/width extend. Both is discouraged by the >30 years old specification.
|
||||
If raster attributes were found in the SIXEL data (level 2), the image will always be truncated to the given height/width extent. We deviate here from the specification on purpose, as it allows several processing optimizations. For level 1 SIXEL data without any raster attributes the image can freely grow in width and height up to the last data byte, which has a much higher processing penalty. In general encoding libraries should not create level 1 data anymore and should not produce pixel information beyond the announced height/width extent. Both is discouraged by the >30 years old specification.
|
||||
|
||||
Currently the SIXEL implementation of the addon does not take custom pixel sizes into account, a SIXEL pixel will map 1:1 to a screen pixel.
|
||||
|
||||
@@ -149,9 +149,9 @@ The addon provides the following API endpoints to retrieve raw image data as can
|
||||
|
||||
### Memory Usage
|
||||
|
||||
The addon does most image processing in Javascript and therefore can occupy a rather big amount of memory. To get an idea where the memory gets eaten, lets look at the data flow and processing steps:
|
||||
- Incomming image data chunk at `term.write` (terminal)
|
||||
`term.write` might stock up incoming chunks. To circumvent this, you will need proper flow control (see xterm.js docs). Note that with image output it is more likely to run into this issue, as it can create lots of MBs in very short time.
|
||||
The addon does most image processing in JavaScript and therefore can occupy a rather big amount of memory. To get an idea where the memory gets eaten, let's look at the data flow and processing steps:
|
||||
- Incoming image data chunk at `term.write` (terminal)
|
||||
`term.write` might stack up incoming chunks. To circumvent this, you will need proper flow control (see xterm.js docs). Note that with image output it is more likely to run into this issue, as it can create lots of MBs in very short time.
|
||||
- Sequence Parser (terminal)
|
||||
The parser operates on a buffer containing up to 2^17 codepoints (~0.5 MB).
|
||||
- Sequence Handler - Chunk Decoding (addon)
|
||||
@@ -177,18 +177,18 @@ const totalActive = storageBytes + decodingBytes;
|
||||
```
|
||||
|
||||
Note that browsers have offloading tricks for rarely touched memory segments, esp. `storageBytes` might not directly translate into real memory usage. Usage peaks will happen during active decoding of multiple big images due to the need of 2 full pixel buffers at the same time, which cannot be offloaded. Thus you may want to keep an eye on `pixelLimit` under limited memory conditions.
|
||||
Further note that the formulas above do not respect the Javascript object's overhead. Compared to the raw buffer needs the book keeping by these objects is rather small (<<5%).
|
||||
Further note that the formulas above do not respect the JavaScript object's overhead. Compared to the raw buffer needs the bookkeeping by these objects is rather small (<<5%).
|
||||
|
||||
_Why should I care about memory usage at all?_
|
||||
Well you don't have to, and it probably will just work fine with the addon defaults. But for bigger integrations, where much more data is held in the Javascript context (like multiple terminals on one page), it is likely to hit the engine's memory limit sooner or later under decoding and/or storage pressure.
|
||||
Well you don't have to, and it probably will just work fine with the addon defaults. But for bigger integrations, where much more data is held in the JavaScript context (like multiple terminals on one page), it is likely to hit the engine's memory limit sooner or later under decoding and/or storage pressure.
|
||||
|
||||
_How can I adjust the memory usage?_
|
||||
- `pixelLimit`
|
||||
A constructor setting, thus you would have to anticipate, whether (multiple) terminals in your page gonna do lots of concurrent decoding. Since this is normally not the case and the memory usage is only temporarily peaking, a rather high value should work even with multiple terminals in one page.
|
||||
A constructor setting, thus you would have to anticipate, whether (multiple) terminals in your page gonna do lots of concurrent decoding. Since this is normally not the case and the memory usage only peaks temporarily, a rather high value should work even with multiple terminals in one page.
|
||||
- `storageLimit`
|
||||
A constructor and a runtime setting. In conjunction with `storageUsage` you can do runtime checks and adjust the limit to your needs. If you have to lower the limit below the current usage, images will be removed in FIFO order and may turn into a placeholder in the terminal's scrollback (if `showPlaceholder` is set). When adjusting keep in mind to leave enough room for memory peaking for decoding.
|
||||
A constructor and a runtime setting. In conjunction with `storageUsage` you can do runtime checks and adjust the limit to your needs. If you have to lower the limit below the current usage, images will be removed in FIFO order and may turn into a placeholder in the terminal's scrollback (if `showPlaceholder` is set). When adjusting keep in mind to leave enough room for peak memory use during decoding.
|
||||
- `sixelSizeLimit | iipSizeLimit | kittySizeLimit`
|
||||
Constructor settings. This has only a small impact on peaking memory during decoding. It is meant to avoid processing of overly big or broken image sequences at an earlier phase, thus may stop the invoked decoders from entering memory intensive tasks for potentially invalid data.
|
||||
Constructor settings. This has only a small impact on peak memory during decoding. It is meant to avoid processing of overly big or broken image sequences at an earlier phase, thus may stop the invoked decoders from entering memory intensive tasks for potentially invalid data.
|
||||
|
||||
|
||||
### Terminal Interaction
|
||||
|
||||
@@ -21,7 +21,7 @@ const CACHE_SIZE = 100000;
|
||||
|
||||
/**
|
||||
* Enable ligature support for the provided Terminal instance. To function
|
||||
* properly, this must be called after `open()` is called on the therminal. If
|
||||
* properly, this must be called after `open()` is called on the terminal. If
|
||||
* the font currently in use supports ligatures, the terminal will automatically
|
||||
* start to render them.
|
||||
* @param term Terminal instance from xterm.js
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { DecorationManager } from './DecorationManager';
|
||||
import { SearchEngine } from './SearchEngine';
|
||||
import { SearchLineCache } from './SearchLineCache';
|
||||
import { Terminal } from 'browser/public/Terminal';
|
||||
import type { ISearchDecorationOptions } from '@xterm/addon-search';
|
||||
import type { IDecorationOptions } from '@xterm/xterm';
|
||||
import { DisposableStore } from 'common/Lifecycle';
|
||||
|
||||
function writeP(terminal: Terminal, data: string): Promise<void> {
|
||||
return new Promise(r => terminal.write(data, r));
|
||||
}
|
||||
|
||||
describe('DecorationManager', () => {
|
||||
let store: DisposableStore;
|
||||
let terminal: Terminal;
|
||||
let decorationManager: DecorationManager;
|
||||
|
||||
beforeEach(() => {
|
||||
store = new DisposableStore();
|
||||
terminal = store.add(new Terminal({ cols: 10, rows: 5 }));
|
||||
decorationManager = store.add(new DecorationManager(terminal));
|
||||
});
|
||||
|
||||
it('should split highlight decorations for a wrapped match', async () => {
|
||||
await writeP(terminal, '0123456789abcde');
|
||||
const searchEngine = new SearchEngine(terminal, store.add(new SearchLineCache(terminal)));
|
||||
const match = searchEngine.find('9abc', 0, 0);
|
||||
assert.ok(match);
|
||||
|
||||
const decorationOptions: IDecorationOptions[] = [];
|
||||
const registerDecoration = terminal.registerDecoration.bind(terminal);
|
||||
terminal.registerDecoration = (options: IDecorationOptions) => {
|
||||
decorationOptions.push(options);
|
||||
return registerDecoration(options);
|
||||
};
|
||||
|
||||
const options: ISearchDecorationOptions = {
|
||||
matchOverviewRuler: '#ff0000',
|
||||
activeMatchColorOverviewRuler: '#00ff00'
|
||||
};
|
||||
decorationManager.createHighlightDecorations([match], options);
|
||||
|
||||
assert.strictEqual(decorationOptions.length, 2);
|
||||
assert.strictEqual(decorationOptions[0].x, 9);
|
||||
assert.strictEqual(decorationOptions[0].width, 1);
|
||||
assert.strictEqual(decorationOptions[1].x, 0);
|
||||
assert.strictEqual(decorationOptions[1].width, 3);
|
||||
|
||||
const withOverviewRuler = decorationOptions.filter(o => o.overviewRulerOptions !== undefined);
|
||||
assert.strictEqual(withOverviewRuler.length, 2);
|
||||
});
|
||||
|
||||
it('should only add one overview ruler marker per buffer line', async () => {
|
||||
await writeP(terminal, 'abcdefghij');
|
||||
const decorationOptions: IDecorationOptions[] = [];
|
||||
const registerDecoration = terminal.registerDecoration.bind(terminal);
|
||||
terminal.registerDecoration = (options: IDecorationOptions) => {
|
||||
decorationOptions.push(options);
|
||||
return registerDecoration(options);
|
||||
};
|
||||
|
||||
const options: ISearchDecorationOptions = {
|
||||
matchOverviewRuler: '#ff0000',
|
||||
activeMatchColorOverviewRuler: '#00ff00'
|
||||
};
|
||||
decorationManager.createHighlightDecorations([
|
||||
{ term: 'a', col: 0, row: 0, size: 1 },
|
||||
{ term: 'f', col: 5, row: 0, size: 1 }
|
||||
], options);
|
||||
|
||||
const withOverviewRuler = decorationOptions.filter(o => o.overviewRulerOptions !== undefined);
|
||||
assert.strictEqual(withOverviewRuler.length, 1);
|
||||
assert.strictEqual(withOverviewRuler[0].x, 0);
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -60,7 +60,7 @@ export class SearchEngine {
|
||||
this._terminal.clearSelection();
|
||||
return undefined;
|
||||
}
|
||||
if (startCol > this._terminal.cols) {
|
||||
if (startCol >= this._terminal.cols) {
|
||||
throw new Error(`Invalid col: ${startCol} to search in terminal of ${this._terminal.cols} cols`);
|
||||
}
|
||||
|
||||
|
||||
@@ -414,10 +414,24 @@ 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 () => {
|
||||
fixture = (await new Promise<Buffer>(r => readFile(resolve(__dirname, '../fixtures/issue-2444'), (err, data) => r(data)))).toString();
|
||||
fixture = (await new Promise<Buffer>((res, rej) => readFile(resolve(__dirname, '../fixtures/issue-2444'), (err, data) => err ? rej(err) : res(data)))).toString();
|
||||
if (process.platform !== 'win32') {
|
||||
fixture = fixture.replace(/\n/g, '\n\r');
|
||||
}
|
||||
@@ -501,7 +515,7 @@ test.describe('Search Tests', () => {
|
||||
window.search.onDidChangeResults(e => window.calls.push(e));
|
||||
`);
|
||||
// Move cursor forward 1 time to create a null character, as opposed to regular whitespace
|
||||
await ctx.proxy.write('\\x1b[CHi Hi');
|
||||
await ctx.proxy.write('\x1b[CHi Hi');
|
||||
strictEqual(await ctx.page.evaluate(`window.search.findPrevious('h', { decorations: { activeMatchColorOverviewRuler: '#ff0000' } })`), true);
|
||||
deepStrictEqual(await ctx.page.evaluate('window.calls'), [
|
||||
{ resultCount: 2, resultIndex: 1 }
|
||||
|
||||
@@ -173,7 +173,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
private _thisRowLastSecondChar: IBufferCell = this._buffer.getNullCell();
|
||||
private _nextRowFirstChar: IBufferCell = this._buffer.getNullCell();
|
||||
protected _rowEnd(row: number, isLastRow: boolean): void {
|
||||
// if there is colorful empty cell at line end, whe must pad it back, or the the color block
|
||||
// if there is colorful empty cell at line end, we must pad it back, or the color block
|
||||
// will missing
|
||||
if (this._nullCellCount > 0 && !equalBg(this._cursorStyle, this._backgroundCell)) {
|
||||
// use clear right to set background.
|
||||
@@ -221,7 +221,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
// you can't use control sequence to move cursor to (x === row)
|
||||
(thisRowLastChar.getChars() || thisRowLastChar.getWidth() === 0) &&
|
||||
// change background of the first wrapped cell also affects BCE
|
||||
// so we mark it as invalid to simply the process to determine line separator
|
||||
// so we mark it as invalid to simplify the process to determine line separator
|
||||
equalBg(thisRowLastChar, nextRowFirstChar)
|
||||
) {
|
||||
isValid = true;
|
||||
@@ -233,7 +233,7 @@ class StringSerializeHandler extends BaseSerializeHandler {
|
||||
isNextRowFirstCharDoubleWidth &&
|
||||
(thisRowLastSecondChar.getChars() || thisRowLastSecondChar.getWidth() === 0) &&
|
||||
// change background of the first wrapped cell also affects BCE
|
||||
// so we mark it as invalid to simply the process to determine line separator
|
||||
// so we mark it as invalid to simplify the process to determine line separator
|
||||
equalBg(thisRowLastChar, nextRowFirstChar) &&
|
||||
equalBg(thisRowLastSecondChar, nextRowFirstChar)
|
||||
) {
|
||||
|
||||
@@ -192,7 +192,7 @@ test.describe('SerializeAddon', () => {
|
||||
const cols = 10;
|
||||
const line = '+'.repeat(cols);
|
||||
const lines: string[] = [
|
||||
sgr(FG_P16_GREEN) + line, // Workaround: If we clear all flags a the end, serialize will use \x1b[0m to clear instead of the sepcific disable sequence
|
||||
sgr(FG_P16_GREEN) + line, // Workaround: If we clear all flags at the end, serialize will use \x1b[0m to clear instead of the specific disable sequence
|
||||
sgr(INVERSE) + line,
|
||||
sgr(BOLD) + line,
|
||||
sgr(UNDERLINED) + line,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Copyright (c) 2023 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*
|
||||
* UnicodeVersionProvider for V15 with grapeme cluster handleing.
|
||||
* UnicodeVersionProvider for V15 with grapheme cluster handling.
|
||||
*/
|
||||
|
||||
import type { Terminal, ITerminalAddon, IUnicodeHandling } from '@xterm/xterm';
|
||||
|
||||
@@ -57,7 +57,8 @@ function isUrl(urlString: string): boolean {
|
||||
|
||||
export class LinkComputer {
|
||||
public static computeLink(y: number, regex: RegExp, terminal: Terminal, activate: (event: MouseEvent, uri: string) => void): ILink[] {
|
||||
const rex = new RegExp(regex.source, (regex.flags || '') + 'g');
|
||||
const flags = regex.flags.includes('g') ? regex.flags : `${regex.flags}g`;
|
||||
const rex = new RegExp(regex.source, flags);
|
||||
|
||||
const [lines, startLineIndex] = LinkComputer._getWindowedLineStrings(y - 1, terminal);
|
||||
const line = lines.join('');
|
||||
|
||||
@@ -7,13 +7,13 @@ import type { Terminal, ITerminalAddon, IDisposable } from '@xterm/xterm';
|
||||
import type { WebLinksAddon as IWebLinksApi } from '@xterm/addon-web-links';
|
||||
import { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider';
|
||||
|
||||
// consider everthing starting with http:// or https://
|
||||
// consider everything starting with http:// or https://
|
||||
// up to first whitespace, `"` or `'` as url
|
||||
// NOTE: The repeated end clause is needed to not match a dangling `:`
|
||||
// resembling the old (...)*([^:"\'\\s]) final path clause
|
||||
// additionally exclude early + final:
|
||||
// - unsafe from rfc3986: !*'()
|
||||
// - unsafe chars from rfc1738: {}|\^~[]` (minus [] as we need them for ipv6 adresses, also allow ~)
|
||||
// - unsafe chars from rfc1738: {}|\^~[]` (minus [] for ipv6 addresses, also allow ~)
|
||||
// also exclude as finals:
|
||||
// - final interpunction like ,.!?
|
||||
// - any sort of brackets <>()[]{} (not spec conform, but often used to enclose urls)
|
||||
|
||||
@@ -23,7 +23,7 @@ See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/addon-
|
||||
|
||||
### Handling Context Loss
|
||||
|
||||
The browser may drop WebGL contexts for various reasons like OOM or after the system has been suspended. There is an API exposed that fires the `webglcontextlost` event fired on the canvas so embedders can handle it however they wish. An easy, but suboptimal way, to handle this is by disposing of WebglAddon when the event fires:
|
||||
The browser may drop WebGL contexts for various reasons like OOM or after the system has been suspended. There is an API exposed that fires the `webglcontextlost` event on the canvas so embedders can handle it however they wish. An easy, but suboptimal way, to handle this is by disposing of WebglAddon when the event fires:
|
||||
|
||||
```ts
|
||||
const terminal = new Terminal();
|
||||
|
||||
@@ -220,7 +220,7 @@ export class GlyphRenderer extends Disposable {
|
||||
public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, width: number, lastBg: number): void {
|
||||
// Since this function is called for every cell (`rows*cols`), it must be very optimized. It
|
||||
// should not instantiate any variables unless a new glyph is drawn to the cache where the
|
||||
// slight slowdown is acceptable for the developer ergonomics provided as it's a once of for
|
||||
// slight slowdown is acceptable for the developer ergonomics provided as it's a one-off for
|
||||
// each glyph.
|
||||
this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, width, lastBg);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -69,7 +69,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
private _overflowSizePage: AtlasPage | undefined;
|
||||
|
||||
private _tmpCanvas: HTMLCanvasElement;
|
||||
// A temporary context that glyphs are drawn to before being transfered to the atlas.
|
||||
// A temporary context that glyphs are drawn to before being transferred to the atlas.
|
||||
private _tmpCtx: CanvasRenderingContext2D;
|
||||
|
||||
private _workBoundingBox: IBoundingBox = { top: 0, left: 0, bottom: 0, right: 0 };
|
||||
@@ -152,7 +152,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
}
|
||||
|
||||
private _createNewPage(): AtlasPage {
|
||||
// Try merge the set of the 4 most used pages of the largest size. This is is deferred to a
|
||||
// Try merge the set of the 4 most used pages of the largest size. This is deferred to a
|
||||
// microtask to ensure it does not interrupt textures that will be rendered in the current
|
||||
// animation frame which would result in blank rendered areas. This is actually not that
|
||||
// expensive relative to drawing the glyphs, so there is no need to wait for an idle callback.
|
||||
@@ -191,7 +191,7 @@ export class TextureAtlas implements ITextureAtlas {
|
||||
return newPage;
|
||||
}
|
||||
|
||||
const sortedMergingPagesIndexes = mergingPages.map(e => e.glyphs[0].texturePage).sort((a, b) => a > b ? 1 : -1);
|
||||
const sortedMergingPagesIndexes = mergingPages.map(e => e.glyphs[0].texturePage).sort((a, b) => a - b);
|
||||
const mergedPageIndex = this.pages.length - mergingPages.length;
|
||||
|
||||
// Merge into the new page
|
||||
@@ -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 {
|
||||
@@ -1125,7 +1141,7 @@ function clearColor(imageData: ImageData, bg: IColor, fg: IColor, enableThreshol
|
||||
// were covered (fg=#8ae234, bg=#c4a000).
|
||||
const threshold = Math.floor((Math.abs(r - fgR) + Math.abs(g - fgG) + Math.abs(b - fgB)) / 12);
|
||||
|
||||
// Set alpha channel of relevent pixels to 0
|
||||
// Set alpha channel of relevant pixels to 0
|
||||
let isEmpty = true;
|
||||
for (let offset = 0; offset < imageData.data.length; offset += 4) {
|
||||
// Check exact match
|
||||
|
||||
@@ -40,7 +40,7 @@ export class WebglAddon extends Disposable implements ITerminalAddon, IWebglApi
|
||||
};
|
||||
const gl = document.createElement('canvas').getContext('webgl2', contextAttributes) as IWebGL2RenderingContext;
|
||||
if (!gl) {
|
||||
throw new Error('Webgl2 is only supported on Safari 16 and above');
|
||||
throw new Error('WebGL2 is only supported on Safari 16 and above');
|
||||
}
|
||||
}
|
||||
super();
|
||||
|
||||
@@ -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;
|
||||
@@ -664,10 +679,10 @@ export class WebglRenderer extends Disposable implements IRenderer {
|
||||
this.dimensions.device.canvas.height = this._terminal.rows * this.dimensions.device.cell.height;
|
||||
this.dimensions.device.canvas.width = this._terminal.cols * this.dimensions.device.cell.width;
|
||||
|
||||
// The the size of the canvas on the page. It's important that this rounds to nearest integer
|
||||
// The size of the canvas on the page. It's important that this rounds to nearest integer
|
||||
// and not ceils as browsers often have floating point precision issues where
|
||||
// `window.devicePixelRatio` ends up being something like `1.100000023841858` for example, when
|
||||
// it's actually 1.1. Ceiling may causes blurriness as the backing canvas image is 1 pixel too
|
||||
// it's actually 1.1. Ceiling may cause blurriness as the backing canvas image is 1 pixel too
|
||||
// large for the canvas element size.
|
||||
this.dimensions.css.canvas.height = Math.round(this.dimensions.device.canvas.height / this._devicePixelRatio);
|
||||
this.dimensions.css.canvas.width = Math.round(this.dimensions.device.canvas.width / this._devicePixelRatio);
|
||||
|
||||
+2
-2
@@ -23,12 +23,12 @@ declare module '@xterm/addon-webgl' {
|
||||
public readonly onChangeTextureAtlas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
/**
|
||||
* An event that is fired when the a new page is added to the texture atlas.
|
||||
* An event that is fired when a new page is added to the texture atlas.
|
||||
*/
|
||||
public readonly onAddTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
/**
|
||||
* An event that is fired when the a page is removed from the texture atlas.
|
||||
* An event that is fired when a page is removed from the texture atlas.
|
||||
*/
|
||||
public readonly onRemoveTextureAtlasCanvas: IEvent<HTMLCanvasElement>;
|
||||
|
||||
|
||||
+5
-2
@@ -19,14 +19,17 @@ for (const addon of addons) {
|
||||
// Demo job - This requires the others to be built so it's not included when building all
|
||||
// jobs.push(createJob('demo-client', [`--demo-client`]));
|
||||
|
||||
await Promise.all(jobs.map((job, i) => {
|
||||
const codes = await Promise.all(jobs.map(job => {
|
||||
return new Promise(r => {
|
||||
job.cp.on('exit', code => {
|
||||
log(`Finished \x1b[32m${job.name}\x1b[0m${code ? ' \x1b[31mwith errors\x1b[0m' : ''}`);
|
||||
r(code);
|
||||
r(code ?? 1);
|
||||
});
|
||||
});
|
||||
}));
|
||||
if (codes.some(code => code !== 0)) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} message
|
||||
|
||||
+2
-2
@@ -5,7 +5,7 @@ import { spawnSync } from 'node:child_process';
|
||||
import { basename, dirname, resolve, sep } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '../..');
|
||||
const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
||||
const nodeModulesPath = resolve(repoRoot, 'node_modules');
|
||||
const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
|
||||
@@ -13,7 +13,7 @@ const npmExecutable = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
||||
|
||||
/** @param {string} message */
|
||||
function log(message) {
|
||||
console.info(`[setup-fast] ${message}`);
|
||||
console.info(`[setup-full] ${message}`);
|
||||
}
|
||||
|
||||
/** @param {string[]} args */
|
||||
|
||||
+12
-3
@@ -12,7 +12,7 @@ let argv = process.argv.slice(2);
|
||||
let suiteFilter = undefined;
|
||||
while (argv.some(e => e.startsWith('--suite='))) {
|
||||
const i = argv.findIndex(e => e.startsWith('--suite='));
|
||||
const match = argv[i].match(/--suite=(?<suitename>.+)/)
|
||||
const match = argv[i].match(/--suite=(?<suitename>.+)/);
|
||||
suiteFilter = match?.groups?.suitename ?? undefined;
|
||||
argv.splice(i, 1);
|
||||
}
|
||||
@@ -42,12 +42,18 @@ if (suiteFilter) {
|
||||
configs = configs.filter(e => e.name === suiteFilter);
|
||||
}
|
||||
|
||||
if (suiteFilter && configs.length === 0) {
|
||||
console.error(`Unknown suite: ${suiteFilter}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function npmBinScript(script) {
|
||||
return path.resolve(__dirname, `../node_modules/.bin/` + (process.platform === 'win32' ?
|
||||
`${script}.cmd` : script));
|
||||
}
|
||||
|
||||
async function run() {
|
||||
let exitCode = 0;
|
||||
for (const config of configs) {
|
||||
const command = npmBinScript('playwright');
|
||||
const args = ['test', '-c', config.path, ...argv];
|
||||
@@ -62,10 +68,13 @@ async function run() {
|
||||
|
||||
if (run.error) {
|
||||
console.error(run.error);
|
||||
process.exit(run.status ?? -1);
|
||||
process.exit(run.status ?? 1);
|
||||
}
|
||||
|
||||
process.exit(run.status);
|
||||
if (run.status) {
|
||||
exitCode = run.status;
|
||||
}
|
||||
}
|
||||
process.exit(exitCode);
|
||||
}
|
||||
run();
|
||||
|
||||
@@ -177,7 +177,7 @@ const ENC = {
|
||||
}
|
||||
|
||||
function printMenu() {
|
||||
console.log('\x1b[2J\x1b\[HTest mouse reports [Ctrl-C to exit]');
|
||||
console.log('\x1b[2J\x1b[HTest mouse reports [Ctrl-C to exit]');
|
||||
console.log();
|
||||
console.log(' Selected protocol [Ctrl-A to switch]');
|
||||
const protocols = Object.keys(PROTOCOLS);
|
||||
|
||||
+2
-4
@@ -21,7 +21,7 @@ if (process.argv.length > 2) {
|
||||
flagArgs = args.filter(e => e.startsWith('--'));
|
||||
// ability to inject particular test files via
|
||||
// npm run test [testFileA testFileB ...]
|
||||
files = args.filter(e => !e.startsWith('--'));
|
||||
const files = args.filter(e => !e.startsWith('--'));
|
||||
if (files.length) {
|
||||
testFiles = files;
|
||||
}
|
||||
@@ -39,8 +39,6 @@ if (checkCoverage) {
|
||||
...testFiles,
|
||||
...flagArgs
|
||||
];
|
||||
console.info('executable', executable);
|
||||
console.info('args', args);
|
||||
const run = cp.spawnSync(
|
||||
executable,
|
||||
args,
|
||||
@@ -51,7 +49,7 @@ if (checkCoverage) {
|
||||
stdio: 'inherit'
|
||||
}
|
||||
);
|
||||
process.exit(run.status);
|
||||
process.exit(run.status ?? -1);
|
||||
}
|
||||
|
||||
const run = cp.spawnSync(
|
||||
|
||||
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+112
-143
@@ -663,9 +663,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -680,9 +680,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -697,9 +697,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -714,9 +714,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -731,9 +731,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -748,9 +748,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -765,9 +765,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -782,9 +782,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -799,9 +799,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
|
||||
"integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz",
|
||||
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
@@ -816,9 +816,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -833,9 +833,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -850,9 +850,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
|
||||
"integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz",
|
||||
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
@@ -867,9 +867,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
|
||||
"integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz",
|
||||
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
@@ -884,9 +884,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
|
||||
"integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz",
|
||||
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
@@ -901,9 +901,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
|
||||
"integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz",
|
||||
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
@@ -918,9 +918,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
|
||||
"integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz",
|
||||
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
@@ -935,9 +935,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -952,9 +952,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -969,9 +969,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -986,9 +986,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1003,9 +1003,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1020,9 +1020,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1037,9 +1037,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -1054,9 +1054,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
|
||||
"integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz",
|
||||
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
@@ -1071,9 +1071,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
|
||||
"integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz",
|
||||
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
@@ -1088,9 +1088,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
|
||||
"integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz",
|
||||
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
@@ -3869,9 +3869,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.28.0",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
|
||||
"integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
|
||||
"version": "0.28.1",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
|
||||
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
@@ -3882,32 +3882,32 @@
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.28.0",
|
||||
"@esbuild/android-arm": "0.28.0",
|
||||
"@esbuild/android-arm64": "0.28.0",
|
||||
"@esbuild/android-x64": "0.28.0",
|
||||
"@esbuild/darwin-arm64": "0.28.0",
|
||||
"@esbuild/darwin-x64": "0.28.0",
|
||||
"@esbuild/freebsd-arm64": "0.28.0",
|
||||
"@esbuild/freebsd-x64": "0.28.0",
|
||||
"@esbuild/linux-arm": "0.28.0",
|
||||
"@esbuild/linux-arm64": "0.28.0",
|
||||
"@esbuild/linux-ia32": "0.28.0",
|
||||
"@esbuild/linux-loong64": "0.28.0",
|
||||
"@esbuild/linux-mips64el": "0.28.0",
|
||||
"@esbuild/linux-ppc64": "0.28.0",
|
||||
"@esbuild/linux-riscv64": "0.28.0",
|
||||
"@esbuild/linux-s390x": "0.28.0",
|
||||
"@esbuild/linux-x64": "0.28.0",
|
||||
"@esbuild/netbsd-arm64": "0.28.0",
|
||||
"@esbuild/netbsd-x64": "0.28.0",
|
||||
"@esbuild/openbsd-arm64": "0.28.0",
|
||||
"@esbuild/openbsd-x64": "0.28.0",
|
||||
"@esbuild/openharmony-arm64": "0.28.0",
|
||||
"@esbuild/sunos-x64": "0.28.0",
|
||||
"@esbuild/win32-arm64": "0.28.0",
|
||||
"@esbuild/win32-ia32": "0.28.0",
|
||||
"@esbuild/win32-x64": "0.28.0"
|
||||
"@esbuild/aix-ppc64": "0.28.1",
|
||||
"@esbuild/android-arm": "0.28.1",
|
||||
"@esbuild/android-arm64": "0.28.1",
|
||||
"@esbuild/android-x64": "0.28.1",
|
||||
"@esbuild/darwin-arm64": "0.28.1",
|
||||
"@esbuild/darwin-x64": "0.28.1",
|
||||
"@esbuild/freebsd-arm64": "0.28.1",
|
||||
"@esbuild/freebsd-x64": "0.28.1",
|
||||
"@esbuild/linux-arm": "0.28.1",
|
||||
"@esbuild/linux-arm64": "0.28.1",
|
||||
"@esbuild/linux-ia32": "0.28.1",
|
||||
"@esbuild/linux-loong64": "0.28.1",
|
||||
"@esbuild/linux-mips64el": "0.28.1",
|
||||
"@esbuild/linux-ppc64": "0.28.1",
|
||||
"@esbuild/linux-riscv64": "0.28.1",
|
||||
"@esbuild/linux-s390x": "0.28.1",
|
||||
"@esbuild/linux-x64": "0.28.1",
|
||||
"@esbuild/netbsd-arm64": "0.28.1",
|
||||
"@esbuild/netbsd-x64": "0.28.1",
|
||||
"@esbuild/openbsd-arm64": "0.28.1",
|
||||
"@esbuild/openbsd-x64": "0.28.1",
|
||||
"@esbuild/openharmony-arm64": "0.28.1",
|
||||
"@esbuild/sunos-x64": "0.28.1",
|
||||
"@esbuild/win32-arm64": "0.28.1",
|
||||
"@esbuild/win32-ia32": "0.28.1",
|
||||
"@esbuild/win32-x64": "0.28.1"
|
||||
}
|
||||
},
|
||||
"node_modules/escalade": {
|
||||
@@ -6689,16 +6689,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/randombytes": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
|
||||
"integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/range-parser": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
|
||||
@@ -6932,27 +6922,6 @@
|
||||
"tslib": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
@@ -7078,13 +7047,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/serialize-javascript": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz",
|
||||
"integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==",
|
||||
"version": "7.0.5",
|
||||
"resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.5.tgz",
|
||||
"integrity": "sha512-F4LcB0UqUl1zErq+1nYEEzSHJnIwb3AF2XWB94b+afhrekOUijwooAYqFyRbjYkm2PAKBabx6oYv/xDxNi8IBw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"randombytes": "^2.1.0"
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
|
||||
+6
-3
@@ -11,6 +11,9 @@
|
||||
"workspaces": [
|
||||
"addons/*"
|
||||
],
|
||||
"overrides": {
|
||||
"serialize-javascript": "^7.0.3"
|
||||
},
|
||||
"keywords": [
|
||||
"cli",
|
||||
"command-line",
|
||||
@@ -61,9 +64,9 @@
|
||||
"test-integration-firefox": "node ./bin/test_integration.js --workers=75% \"--project=FirefoxStable\"",
|
||||
"test-integration-webkit": "node ./bin/test_integration.js --workers=75% \"--project=WebKit\"",
|
||||
"test-integration-debug": "node ./bin/test_integration.js --workers=1 --headed --timeout=30000",
|
||||
"benchmark": "xterm-benchmark -r 5 -c test/benchmark/benchmark.json",
|
||||
"benchmark-baseline": "xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/*benchmark.js",
|
||||
"benchmark-eval": "xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/*benchmark.js",
|
||||
"benchmark": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json",
|
||||
"benchmark-baseline": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --baseline out-test/benchmark/*benchmark.js",
|
||||
"benchmark-eval": "NODE_PATH=./out xterm-benchmark -r 5 -c test/benchmark/benchmark.json --eval out-test/benchmark/*benchmark.js",
|
||||
"clean": "rm -rf lib out addons/*/lib addons/*/out",
|
||||
"vtfeatures": "node bin/extract_vtfeatures.js src/*/*.ts src/*/*/*.ts src/*.ts addons/**/src/*.ts",
|
||||
"prepackage": "npm run build",
|
||||
|
||||
@@ -505,7 +505,7 @@ export class CoreBrowserTerminal extends CoreTerminal implements ITerminal {
|
||||
this.textarea,
|
||||
parent.ownerDocument.defaultView ?? window,
|
||||
// Force unsafe null in node.js environment for tests
|
||||
this._document ?? (typeof window !== 'undefined') ? window.document : null as any
|
||||
this._document ?? ((typeof window !== 'undefined') ? window.document : null as any)
|
||||
));
|
||||
this._instantiationService.setService(ICoreBrowserService, this._coreBrowserService);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -23,7 +23,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
if (this._animationFrame) {
|
||||
if (this._animationFrame !== undefined) {
|
||||
this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame);
|
||||
this._animationFrame = undefined;
|
||||
}
|
||||
@@ -31,9 +31,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
|
||||
|
||||
public addRefreshCallback(callback: FrameRequestCallback): number {
|
||||
this._refreshCallbacks.push(callback);
|
||||
if (!this._animationFrame) {
|
||||
this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
|
||||
}
|
||||
this._animationFrame ??= this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh());
|
||||
return this._animationFrame;
|
||||
}
|
||||
|
||||
@@ -46,7 +44,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback {
|
||||
this._rowStart = this._rowStart !== undefined ? Math.min(this._rowStart, rowStart) : rowStart;
|
||||
this._rowEnd = this._rowEnd !== undefined ? Math.max(this._rowEnd, rowEnd) : rowEnd;
|
||||
|
||||
if (this._animationFrame) {
|
||||
if (this._animationFrame !== undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
const RENDER_DEBOUNCE_THRESHOLD_MS = 1000; // 1 Second
|
||||
|
||||
import { IRenderDebouncer } from './Types';
|
||||
|
||||
const RENDER_DEBOUNCE_THRESHOLD_MS = 1000; // 1 Second
|
||||
|
||||
/**
|
||||
* Debounces calls to update screen readers to update at most once configurable interval of time.
|
||||
*/
|
||||
@@ -31,7 +31,9 @@ export class TimeBasedDebouncer implements IRenderDebouncer {
|
||||
public dispose(): void {
|
||||
if (this._refreshTimeoutID) {
|
||||
clearTimeout(this._refreshTimeoutID);
|
||||
this._refreshTimeoutID = undefined;
|
||||
}
|
||||
this._additionalRefreshRequested = false;
|
||||
}
|
||||
|
||||
public refresh(rowStart: number | undefined, rowEnd: number | undefined, rowCount: number): void {
|
||||
@@ -48,6 +50,11 @@ export class TimeBasedDebouncer implements IRenderDebouncer {
|
||||
const refreshRequestTime: number = performance.now();
|
||||
if (refreshRequestTime - this._lastRefreshMs >= this._debounceThresholdMS) {
|
||||
// Enough time has elapsed since the last refresh; refresh immediately
|
||||
if (this._refreshTimeoutID !== undefined) {
|
||||
clearTimeout(this._refreshTimeoutID);
|
||||
this._refreshTimeoutID = undefined;
|
||||
this._additionalRefreshRequested = false;
|
||||
}
|
||||
this._lastRefreshMs = refreshRequestTime;
|
||||
this._innerRefresh();
|
||||
} else if (!this._additionalRefreshRequested) {
|
||||
|
||||
@@ -37,10 +37,6 @@ export function getCoords(window: Pick<Window, 'getComputedStyle'>, event: Pick<
|
||||
}
|
||||
|
||||
const coords = getCoordsRelativeToElement(window, event, element);
|
||||
if (!coords) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
coords[0] = Math.ceil((coords[0] + (isSelection ? cssCellWidth / 2 : 0)) / cssCellWidth);
|
||||
coords[1] = Math.ceil(coords[1] / cssCellHeight);
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ function wrappedRowsForRow(currentRow: number, bufferService: IBufferService): n
|
||||
*/
|
||||
function horizontalDirection(startX: number, startY: number, targetX: number, targetY: number, bufferService: IBufferService, applicationCursor: boolean): Direction {
|
||||
let startRow;
|
||||
if (moveToRequestedRow(targetX, targetY, bufferService, applicationCursor).length > 0) {
|
||||
if (moveToRequestedRow(startY, targetY, bufferService, applicationCursor).length > 0) {
|
||||
startRow = targetY - wrappedRowsForRow(targetY, bufferService);
|
||||
} else {
|
||||
startRow = startY;
|
||||
|
||||
@@ -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(
|
||||
@@ -694,7 +704,7 @@ class CursorBlinkStateManager {
|
||||
}
|
||||
|
||||
private _clearIdleTimer(): void {
|
||||
if (this._idleTimeout) {
|
||||
if (this._idleTimeout !== undefined) {
|
||||
this._coreBrowserService.window.clearTimeout(this._idleTimeout);
|
||||
this._idleTimeout = undefined;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ describe('MouseService _triggerMouseEvent', () => {
|
||||
for (let i = 0; i < bufferService.cols; ++i) {
|
||||
assert.equal(trigger({ col: i, row: 0, x: 0, y: 0, button: CoreMouseButton.LEFT, action: CoreMouseAction.DOWN }), true);
|
||||
if (i > 222) {
|
||||
// supress mouse reports if we are out of addressible range (max. 222)
|
||||
// suppress mouse reports if we are out of addressable range (max. 222)
|
||||
assert.deepEqual(toBytes(reports.pop()), []);
|
||||
} else {
|
||||
assert.deepEqual(toBytes(reports.pop()), [0x1b, 0x5b, 0x4d, 0x20, i + 33, 0x21]);
|
||||
@@ -351,3 +351,96 @@ describe('MouseService mouseEventsRequireAlt', () => {
|
||||
assert.isFalse(element.classList.contains(MouseEventCssClasses.ENABLE_MOUSE_EVENTS));
|
||||
});
|
||||
});
|
||||
|
||||
describe('MouseService multi-window', () => {
|
||||
interface IRecordingDocument {
|
||||
doc: Document;
|
||||
added: string[];
|
||||
removed: string[];
|
||||
listeners: { [type: string]: EventListener[] };
|
||||
}
|
||||
|
||||
function createRecordingDocument(): IRecordingDocument {
|
||||
const added: string[] = [];
|
||||
const removed: string[] = [];
|
||||
const listeners: { [type: string]: EventListener[] } = {};
|
||||
const doc = {
|
||||
addEventListener: (type: string, listener: EventListener) => {
|
||||
added.push(type);
|
||||
(listeners[type] ??= []).push(listener);
|
||||
},
|
||||
removeEventListener: (type: string) => removed.push(type)
|
||||
} as any;
|
||||
return { doc, added, removed, listeners };
|
||||
}
|
||||
|
||||
it('should manage protocol mouseup/mousemove listeners on the element\'s current document, not the document the terminal was opened with', () => {
|
||||
const mouseStateService = new MouseStateService();
|
||||
const optionsService = new OptionsService({});
|
||||
const mouseService = new MouseService(
|
||||
new MockRenderService(),
|
||||
{
|
||||
getMouseReportCoords: () => ({ col: 0, row: 0, x: 0, y: 0 })
|
||||
} as any,
|
||||
mouseStateService,
|
||||
{
|
||||
triggerDataEvent: () => {},
|
||||
triggerBinaryEvent: () => {},
|
||||
decPrivateModes: { applicationCursorKeys: false }
|
||||
} as any,
|
||||
bufferService,
|
||||
optionsService,
|
||||
new TestSelectionService(),
|
||||
logService,
|
||||
new MockCoreBrowserService()
|
||||
);
|
||||
|
||||
// Simulate a terminal that was opened with one document but whose element now lives in
|
||||
// another document (eg. it was moved to a child window)
|
||||
const openDocument = createRecordingDocument();
|
||||
const childDocument = createRecordingDocument();
|
||||
const elementListeners: { [type: string]: EventListener[] } = {};
|
||||
const element = {
|
||||
classList: {
|
||||
add: () => {},
|
||||
remove: () => {},
|
||||
contains: () => false
|
||||
},
|
||||
addEventListener: (type: string, listener: EventListener) => {
|
||||
(elementListeners[type] ??= []).push(listener);
|
||||
},
|
||||
removeEventListener: () => {},
|
||||
ownerDocument: childDocument.doc
|
||||
} as any;
|
||||
|
||||
mouseService.bindMouse({
|
||||
element,
|
||||
screenElement: createTestMouseTargetElement(),
|
||||
document: openDocument.doc
|
||||
}, disposable => disposable, () => {});
|
||||
mouseStateService.activeProtocol = 'ANY';
|
||||
|
||||
elementListeners['mousedown'][0]({
|
||||
type: 'mousedown',
|
||||
button: 0,
|
||||
buttons: 1,
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
shiftKey: false,
|
||||
preventDefault: () => {}
|
||||
} as any);
|
||||
assert.deepEqual(childDocument.added, ['mouseup', 'mousemove']);
|
||||
assert.deepEqual(openDocument.added, []);
|
||||
|
||||
childDocument.listeners['mouseup'][0]({
|
||||
type: 'mouseup',
|
||||
button: 0,
|
||||
buttons: 0,
|
||||
altKey: false,
|
||||
ctrlKey: false,
|
||||
shiftKey: false
|
||||
} as any);
|
||||
assert.deepEqual(childDocument.removed, ['mouseup', 'mousemove']);
|
||||
assert.deepEqual(openDocument.removed, []);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ interface IMouseBindContext {
|
||||
readonly target: IMouseServiceTarget;
|
||||
readonly focus: () => void;
|
||||
readonly requestedEvents: RequestedMouseEvents;
|
||||
listenerDocument: Document | null;
|
||||
}
|
||||
|
||||
export class MouseService implements IMouseService {
|
||||
@@ -61,7 +62,7 @@ export class MouseService implements IMouseService {
|
||||
mousedrag: null,
|
||||
mousemove: null
|
||||
};
|
||||
const ctx: IMouseBindContext = { target, focus, requestedEvents };
|
||||
const ctx: IMouseBindContext = { target, focus, requestedEvents, listenerDocument: null };
|
||||
const eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener> = {
|
||||
mouseup: (ev: Event) => this._handleMouseUp(ctx, ev as MouseEvent),
|
||||
wheel: (ev: Event) => this._handleWheel(ctx, ev as WheelEvent),
|
||||
@@ -87,11 +88,12 @@ export class MouseService implements IMouseService {
|
||||
|
||||
// Ensure document-level listeners are removed on dispose
|
||||
register(toDisposable(() => {
|
||||
const listenerDocument = ctx.listenerDocument ?? document;
|
||||
if (requestedEvents.mouseup) {
|
||||
document.removeEventListener('mouseup', requestedEvents.mouseup);
|
||||
listenerDocument.removeEventListener('mouseup', requestedEvents.mouseup);
|
||||
}
|
||||
if (requestedEvents.mousedrag) {
|
||||
document.removeEventListener('mousemove', requestedEvents.mousedrag);
|
||||
listenerDocument.removeEventListener('mousemove', requestedEvents.mousedrag);
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -199,12 +201,14 @@ export class MouseService implements IMouseService {
|
||||
this._sendEvent(ctx, ev);
|
||||
if (!ev.buttons) {
|
||||
// if no other button is held remove global handlers
|
||||
const listenerDocument = ctx.listenerDocument ?? ctx.target.document;
|
||||
if (ctx.requestedEvents.mouseup) {
|
||||
ctx.target.document.removeEventListener('mouseup', ctx.requestedEvents.mouseup);
|
||||
listenerDocument.removeEventListener('mouseup', ctx.requestedEvents.mouseup);
|
||||
}
|
||||
if (ctx.requestedEvents.mousedrag) {
|
||||
ctx.target.document.removeEventListener('mousemove', ctx.requestedEvents.mousedrag);
|
||||
listenerDocument.removeEventListener('mousemove', ctx.requestedEvents.mousedrag);
|
||||
}
|
||||
ctx.listenerDocument = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,12 +250,15 @@ export class MouseService implements IMouseService {
|
||||
// of the terminal element.
|
||||
// Note: Other emulators also do this for 'mousedown' while a button
|
||||
// is held, we currently limit 'mousedown' to the terminal only.
|
||||
// Use the element's current document in case it moved to another window after open.
|
||||
const listenerDocument = ctx.target.element.ownerDocument ?? ctx.target.document;
|
||||
if (ctx.requestedEvents.mouseup) {
|
||||
ctx.target.document.addEventListener('mouseup', ctx.requestedEvents.mouseup);
|
||||
listenerDocument.addEventListener('mouseup', ctx.requestedEvents.mouseup);
|
||||
}
|
||||
if (ctx.requestedEvents.mousedrag) {
|
||||
ctx.target.document.addEventListener('mousemove', ctx.requestedEvents.mousedrag);
|
||||
listenerDocument.addEventListener('mousemove', ctx.requestedEvents.mousedrag);
|
||||
}
|
||||
ctx.listenerDocument = listenerDocument;
|
||||
}
|
||||
|
||||
private _handlePassiveWheel(ctx: IMouseBindContext, ev: WheelEvent): false | void {
|
||||
@@ -398,8 +405,9 @@ export class MouseService implements IMouseService {
|
||||
}
|
||||
|
||||
private _handleProtocolChange(ctx: IMouseBindContext, eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener>, events: CoreMouseEventType): void {
|
||||
const { element, document } = ctx.target;
|
||||
const { element } = ctx.target;
|
||||
const { requestedEvents } = ctx;
|
||||
const listenerDocument = ctx.listenerDocument ?? ctx.target.document;
|
||||
// apply global changes on events
|
||||
if (events) {
|
||||
if (this._optionsService.rawOptions.logLevel === 'debug') {
|
||||
@@ -434,7 +442,7 @@ export class MouseService implements IMouseService {
|
||||
|
||||
if (!(events & CoreMouseEventType.UP)) {
|
||||
if (requestedEvents.mouseup) {
|
||||
document.removeEventListener('mouseup', requestedEvents.mouseup);
|
||||
listenerDocument.removeEventListener('mouseup', requestedEvents.mouseup);
|
||||
}
|
||||
requestedEvents.mouseup = null;
|
||||
} else {
|
||||
@@ -443,7 +451,7 @@ export class MouseService implements IMouseService {
|
||||
|
||||
if (!(events & CoreMouseEventType.DRAG)) {
|
||||
if (requestedEvents.mousedrag) {
|
||||
document.removeEventListener('mousemove', requestedEvents.mousedrag);
|
||||
listenerDocument.removeEventListener('mousemove', requestedEvents.mousedrag);
|
||||
}
|
||||
requestedEvents.mousedrag = null;
|
||||
} else {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -16,8 +16,10 @@ import { MockCoreService, MockBufferService, MockOptionsService, MockLogService,
|
||||
import { IBufferService, ICoreService, type IOscLinkService } from './services/Services';
|
||||
import { DEFAULT_OPTIONS } from './services/OptionsService';
|
||||
import { BufferService } from './services/BufferService';
|
||||
import { CharsetService } from './services/CharsetService';
|
||||
import { CoreService } from './services/CoreService';
|
||||
import { OscLinkService } from './services/OscLinkService';
|
||||
import { CHARSETS } from './data/Charsets';
|
||||
|
||||
|
||||
function getCursor(bufferService: IBufferService): number[] {
|
||||
@@ -639,19 +641,38 @@ describe('InputHandler', () => {
|
||||
});
|
||||
describe('print', () => {
|
||||
it('should not cause an infinite loop (regression test)', () => {
|
||||
const inputHandler = new TestInputHandler(
|
||||
new MockBufferService(80, 30),
|
||||
new MockCharsetService(),
|
||||
new MockCoreService(),
|
||||
new MockLogService(),
|
||||
new MockOptionsService(),
|
||||
new MockOscLinkService(),
|
||||
new MockMouseStateService(),
|
||||
new MockUnicodeService()
|
||||
);
|
||||
const container = new Uint32Array(10);
|
||||
container[0] = 0x200B;
|
||||
const lineCountBefore = bufferService.buffer.lines.length;
|
||||
inputHandler.print(container, 0, 1);
|
||||
assert.strictEqual(bufferService.buffer.y, 0);
|
||||
assert.strictEqual(bufferService.buffer.lines.length, lineCountBefore);
|
||||
});
|
||||
it('should join combining characters in a single print', async () => {
|
||||
await inputHandler.parseP('e\u0301');
|
||||
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), 'e\u0301');
|
||||
assert.strictEqual(bufferService.buffer.x, 1);
|
||||
});
|
||||
it('should join combining characters split across parse calls', async () => {
|
||||
await inputHandler.parseP('e');
|
||||
await inputHandler.parseP('\u0301');
|
||||
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), 'e\u0301');
|
||||
assert.strictEqual(bufferService.buffer.x, 1);
|
||||
});
|
||||
it('should repeat preceding grapheme cluster via REP', async () => {
|
||||
await inputHandler.parseP('e\u0301\x1b[2b');
|
||||
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), 'e\u0301e\u0301e\u0301');
|
||||
assert.strictEqual(bufferService.buffer.x, 3);
|
||||
});
|
||||
it('should not repeat when REP has no preceding join state', async () => {
|
||||
await inputHandler.parseP('\x1b[2b');
|
||||
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), '');
|
||||
assert.strictEqual(bufferService.buffer.x, 0);
|
||||
});
|
||||
it('should not repeat after an intervening escape sequence', async () => {
|
||||
await inputHandler.parseP('a\x1b[0m\x1b[2b');
|
||||
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), 'a');
|
||||
assert.strictEqual(bufferService.buffer.x, 1);
|
||||
});
|
||||
it('should clear cells to the right on early wrap-around', async () => {
|
||||
bufferService.resize(5, 5);
|
||||
@@ -668,6 +689,49 @@ describe('InputHandler', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('ISO-2022 character sets', () => {
|
||||
let charsetService: CharsetService;
|
||||
|
||||
beforeEach(() => {
|
||||
charsetService = new CharsetService();
|
||||
inputHandler = new TestInputHandler(
|
||||
bufferService,
|
||||
charsetService,
|
||||
coreService,
|
||||
new MockLogService(),
|
||||
optionsService,
|
||||
oscLinkService,
|
||||
new MockMouseStateService(),
|
||||
new MockUnicodeService()
|
||||
);
|
||||
});
|
||||
|
||||
it('should map G0 line drawing via ESC ( 0', async () => {
|
||||
await inputHandler.parseP('\x1b(0q\x1b(Bq');
|
||||
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), '\u2500q');
|
||||
});
|
||||
|
||||
it('should map G1 line drawing after ESC ) 0 and SO', async () => {
|
||||
await inputHandler.parseP('\x1b)0\x0eq\x0f\x1b(Bq');
|
||||
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), '\u2500q');
|
||||
});
|
||||
|
||||
it('should restore charset and glevel on ESC 7 / ESC 8', async () => {
|
||||
await inputHandler.parseP('\x1b)0\x0e');
|
||||
assert.strictEqual(charsetService.glevel, 1);
|
||||
assert.strictEqual(charsetService.charset, CHARSETS['0']);
|
||||
await inputHandler.parseP('\x1b7');
|
||||
await inputHandler.parseP('\x0f\x1b(B');
|
||||
assert.strictEqual(charsetService.glevel, 0);
|
||||
assert.ok(charsetService.charset === undefined);
|
||||
await inputHandler.parseP('\x1b8');
|
||||
assert.strictEqual(charsetService.glevel, 1);
|
||||
assert.strictEqual(charsetService.charset, CHARSETS['0']);
|
||||
await inputHandler.parseP('q');
|
||||
assert.strictEqual(bufferService.buffer.translateBufferLineToString(0, true), '\u2500');
|
||||
});
|
||||
});
|
||||
|
||||
describe('alt screen', () => {
|
||||
let bufferService: IBufferService;
|
||||
let handler: TestInputHandler;
|
||||
@@ -1182,6 +1246,19 @@ describe('InputHandler', () => {
|
||||
await inputHandler.parseP('\x1b[100;100H');
|
||||
assert.deepEqual(getCursor(bufferService), [9, 9]);
|
||||
});
|
||||
it('cursor position (CUP) with DECOM and scroll margins', async () => {
|
||||
await inputHandler.parseP('\x1b[?6h\x1b[2;3r\x1b[1;1H');
|
||||
assert.deepEqual(getCursor(bufferService), [0, 1]);
|
||||
await inputHandler.parseP('X');
|
||||
assert.strictEqual(getLines(bufferService, 3)[1], 'X');
|
||||
await inputHandler.parseP('\x1b[2;1H');
|
||||
assert.deepEqual(getCursor(bufferService), [0, 2]);
|
||||
await inputHandler.parseP('\x1b[10;10H');
|
||||
assert.deepEqual(getCursor(bufferService), [9, 2]);
|
||||
await inputHandler.parseP('\x1b[?6l');
|
||||
await inputHandler.parseP('\x1b[2;1H');
|
||||
assert.deepEqual(getCursor(bufferService), [0, 1]);
|
||||
});
|
||||
it('horizontal position absolute (HPA)', async () => {
|
||||
await inputHandler.parseP('\x1b[`');
|
||||
assert.deepEqual(getCursor(bufferService), [0, 0]);
|
||||
@@ -1241,7 +1318,7 @@ describe('InputHandler', () => {
|
||||
await inputHandler.parseP('\x1b[e');
|
||||
assert.deepEqual(getCursor(bufferService), [8, 5]);
|
||||
});
|
||||
describe('should clamp cursor into addressible range', () => {
|
||||
describe('should clamp cursor into addressable range', () => {
|
||||
it('CUF', async () => {
|
||||
bufferService.buffer.x = 10000;
|
||||
bufferService.buffer.y = 10000;
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { DEFAULT_ATTR_DATA } from './buffer/BufferLine';
|
||||
import { updateWindowsModeWrappedState } from './WindowsMode';
|
||||
import { BufferService } from './services/BufferService';
|
||||
import { OptionsService } from './services/OptionsService';
|
||||
import { MockLogService } from './TestUtils.test';
|
||||
|
||||
describe('WindowsMode', () => {
|
||||
describe('updateWindowsModeWrappedState', () => {
|
||||
it('should mark the next line wrapped when the previous line ends in a non-whitespace character', () => {
|
||||
const bufferService = new BufferService(new OptionsService({ rows: 5, cols: 10 }), new MockLogService());
|
||||
const buffer = bufferService.buffer;
|
||||
const previousLine = buffer.lines.get(buffer.ybase)!;
|
||||
for (let i = 0; i < bufferService.cols; i++) {
|
||||
previousLine!.setCellFromCodepoint(i, 'a'.charCodeAt(0), 1, DEFAULT_ATTR_DATA);
|
||||
}
|
||||
buffer.y = 1;
|
||||
|
||||
updateWindowsModeWrappedState(bufferService);
|
||||
|
||||
assert.strictEqual(buffer.lines.get(buffer.ybase + 1)!.isWrapped, true);
|
||||
});
|
||||
|
||||
it('should not mark the next line wrapped when the previous line ends in whitespace', () => {
|
||||
const bufferService = new BufferService(new OptionsService({ rows: 5, cols: 10 }), new MockLogService());
|
||||
const buffer = bufferService.buffer;
|
||||
const previousLine = buffer.lines.get(buffer.ybase)!;
|
||||
for (let i = 0; i < bufferService.cols - 1; i++) {
|
||||
previousLine!.setCellFromCodepoint(i, 'a'.charCodeAt(0), 1, DEFAULT_ATTR_DATA);
|
||||
}
|
||||
previousLine!.setCellFromCodepoint(bufferService.cols - 1, ' '.charCodeAt(0), 1, DEFAULT_ATTR_DATA);
|
||||
buffer.y = 1;
|
||||
|
||||
updateWindowsModeWrappedState(bufferService);
|
||||
|
||||
assert.strictEqual(buffer.lines.get(buffer.ybase + 1)!.isWrapped, false);
|
||||
});
|
||||
|
||||
it('should not mark the next line wrapped when the previous line ends in a null cell', () => {
|
||||
const bufferService = new BufferService(new OptionsService({ rows: 5, cols: 10 }), new MockLogService());
|
||||
const buffer = bufferService.buffer;
|
||||
buffer.y = 1;
|
||||
|
||||
updateWindowsModeWrappedState(bufferService);
|
||||
|
||||
assert.strictEqual(buffer.lines.get(buffer.ybase + 1)!.isWrapped, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -233,6 +233,27 @@ describe('Buffer', () => {
|
||||
assert.equal(buffer.lines.length, INIT_ROWS + 10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Windows ConPTY', () => {
|
||||
beforeEach(() => {
|
||||
optionsService.options.windowsPty = { backend: 'conpty', buildNumber: 19000 };
|
||||
});
|
||||
|
||||
it('should not adjust ybase or ydisp when growing rows', () => {
|
||||
buffer.fillViewportRows();
|
||||
for (let i = 0; i < 10; i++) {
|
||||
buffer.lines.push(buffer.getBlankLine(DEFAULT_ATTR_DATA));
|
||||
}
|
||||
buffer.y = INIT_ROWS - 1;
|
||||
buffer.ybase = 10;
|
||||
buffer.ydisp = 10;
|
||||
const linesBefore = buffer.lines.length;
|
||||
buffer.resize(INIT_COLS, INIT_ROWS + 5);
|
||||
assert.equal(buffer.ybase, 10);
|
||||
assert.equal(buffer.ydisp, 10);
|
||||
assert.equal(buffer.lines.length, linesBefore + 5);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('row and column increased', () => {
|
||||
@@ -305,6 +326,71 @@ describe('Buffer', () => {
|
||||
assert.equal(buffer.lines.get(8)!.translateToString(), ' ');
|
||||
assert.equal(buffer.lines.get(9)!.translateToString(), ' ');
|
||||
});
|
||||
it('should gate reflow on ConPTY buildNumber 21376', () => {
|
||||
const prepareWrappedShrink = () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(5, 10);
|
||||
const firstLine = buffer.lines.get(0)!;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const code = 'a'.charCodeAt(0) + i;
|
||||
firstLine.set(i, [0, String.fromCharCode(code), 1, code]);
|
||||
}
|
||||
buffer.y = 1;
|
||||
buffer.resize(1, 10);
|
||||
};
|
||||
|
||||
optionsService.options.windowsPty = { backend: 'conpty', buildNumber: 21375 };
|
||||
prepareWrappedShrink();
|
||||
assert.equal(buffer.lines.get(1)!.translateToString().trim(), '');
|
||||
|
||||
buffer = new TestBuffer(true, optionsService, bufferService, new MockLogService());
|
||||
optionsService.options.windowsPty = { backend: 'conpty', buildNumber: 21376 };
|
||||
prepareWrappedShrink();
|
||||
assert.equal(buffer.lines.get(1)!.translateToString().trim(), 'b');
|
||||
assert.ok(buffer.lines.get(1)!.isWrapped);
|
||||
});
|
||||
it('should unwrap lines on ConPTY builds with reflow support', () => {
|
||||
optionsService.options.windowsPty = { backend: 'conpty', buildNumber: 21376 };
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(5, 10);
|
||||
const firstLine = buffer.lines.get(0)!;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const code = 'a'.charCodeAt(0) + i;
|
||||
firstLine.set(i, [0, String.fromCharCode(code), 1, code]);
|
||||
}
|
||||
buffer.y = 1;
|
||||
buffer.resize(1, 10);
|
||||
buffer.resize(5, 10);
|
||||
assert.equal(buffer.lines.get(0)!.translateToString(), 'abcde');
|
||||
assert.equal(buffer.lines.get(1)!.translateToString(), ' ');
|
||||
});
|
||||
it('should reflow wrapped lines containing the cursor when reflowCursorLine is enabled', () => {
|
||||
optionsService.options.reflowCursorLine = true;
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(5, 10);
|
||||
const firstLine = buffer.lines.get(0)!;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const code = 'a'.charCodeAt(0) + i;
|
||||
firstLine.set(i, [0, String.fromCharCode(code), 1, code]);
|
||||
}
|
||||
buffer.resize(1, 10);
|
||||
buffer.y = 2;
|
||||
buffer.resize(5, 10);
|
||||
assert.equal(buffer.lines.get(0)!.translateToString(), 'abcde');
|
||||
});
|
||||
it('should not reflow wrapped lines containing the cursor by default', () => {
|
||||
buffer.fillViewportRows();
|
||||
buffer.resize(5, 10);
|
||||
const firstLine = buffer.lines.get(0)!;
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const code = 'a'.charCodeAt(0) + i;
|
||||
firstLine.set(i, [0, String.fromCharCode(code), 1, code]);
|
||||
}
|
||||
buffer.resize(1, 10);
|
||||
buffer.y = 2;
|
||||
buffer.resize(5, 10);
|
||||
assert.notEqual(buffer.lines.get(0)!.translateToString(), 'abcde');
|
||||
});
|
||||
it('should discard parts of wrapped lines that go out of the scrollback', () => {
|
||||
buffer.fillViewportRows();
|
||||
optionsService.options.scrollback = 1;
|
||||
|
||||
@@ -73,7 +73,9 @@ export interface IBufferLineStringCache {
|
||||
*/
|
||||
export class BufferLine implements IBufferLine {
|
||||
protected _data: Uint32Array;
|
||||
/** Sparse cache; only read when `IS_COMBINED_MASK` is set in `_data`. */
|
||||
protected _combined: {[index: number]: string} = {};
|
||||
/** Sparse cache; only read when `HAS_EXTENDED` is set in `_data`. */
|
||||
protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {};
|
||||
protected _stringCacheEntryRef: WeakRef<IBufferLineStringCacheEntry> | undefined;
|
||||
public length: number;
|
||||
@@ -205,9 +207,15 @@ export class BufferLine implements IBufferLine {
|
||||
cell.bg = this._data[$startIndex + Cell.BG];
|
||||
if (cell.content & Content.IS_COMBINED_MASK) {
|
||||
cell.combinedData = this._combined[index];
|
||||
} else {
|
||||
cell.combinedData = '';
|
||||
}
|
||||
if (cell.bg & BgFlags.HAS_EXTENDED) {
|
||||
cell.extended = this._extendedAttrs[index]!;
|
||||
} else {
|
||||
// Do not mutate cell.extended in place: it may still reference this line's map entry from a
|
||||
// prior loadCell into a reused CellData (e.g. $workCell during insert/delete).
|
||||
cell.extended = DEFAULT_ATTR_DATA.extended.clone();
|
||||
}
|
||||
return cell;
|
||||
}
|
||||
@@ -459,14 +467,7 @@ export class BufferLine implements IBufferLine {
|
||||
this._data.set(line._data);
|
||||
}
|
||||
this.length = line.length;
|
||||
this._combined = {};
|
||||
for (const el in line._combined) {
|
||||
this._combined[el] = line._combined[el];
|
||||
}
|
||||
this._extendedAttrs = {};
|
||||
for (const el in line._extendedAttrs) {
|
||||
this._extendedAttrs[el] = line._extendedAttrs[el];
|
||||
}
|
||||
this._copySparseMapsFrom(line);
|
||||
this.isWrapped = line.isWrapped;
|
||||
}
|
||||
|
||||
@@ -475,12 +476,7 @@ export class BufferLine implements IBufferLine {
|
||||
const newLine = new BufferLine(this._stringCache, 0, undefined, false);
|
||||
newLine._data = new Uint32Array(this._data);
|
||||
newLine.length = this.length;
|
||||
for (const el in this._combined) {
|
||||
newLine._combined[el] = this._combined[el];
|
||||
}
|
||||
for (const el in this._extendedAttrs) {
|
||||
newLine._extendedAttrs[el] = this._extendedAttrs[el];
|
||||
}
|
||||
newLine._copySparseMapsFrom(this);
|
||||
newLine.isWrapped = this.isWrapped;
|
||||
return newLine;
|
||||
}
|
||||
@@ -511,27 +507,14 @@ export class BufferLine implements IBufferLine {
|
||||
for (let i = 0; i < Constants.CELL_INDICIES; i++) {
|
||||
this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];
|
||||
}
|
||||
if (srcData[(srcCol + cell) * Constants.CELL_INDICIES + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell];
|
||||
}
|
||||
this._copyCellMapsFrom(src, srcCol + cell, destCol + cell);
|
||||
}
|
||||
} else {
|
||||
for (let cell = 0; cell < length; cell++) {
|
||||
for (let i = 0; i < Constants.CELL_INDICIES; i++) {
|
||||
this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];
|
||||
}
|
||||
if (srcData[(srcCol + cell) * Constants.CELL_INDICIES + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Move any combined data over as needed, FIXME: repeat for extended attrs
|
||||
const srcCombinedKeys = Object.keys(src._combined);
|
||||
for (let i = 0; i < srcCombinedKeys.length; i++) {
|
||||
const key = parseInt(srcCombinedKeys[i], 10);
|
||||
if (key >= srcCol) {
|
||||
this._combined[key - srcCol + destCol] = src._combined[key];
|
||||
this._copyCellMapsFrom(src, srcCol + cell, destCol + cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -620,4 +603,24 @@ export class BufferLine implements IBufferLine {
|
||||
cacheEntry.isTrimmed = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Copy sparse map entries for a single cell when `_data` flags require them. */
|
||||
private _copyCellMapsFrom(src: BufferLine, srcCol: number, destCol: number): void {
|
||||
const srcStart = srcCol * Constants.CELL_INDICIES;
|
||||
if (src._data[srcStart + Cell.CONTENT] & Content.IS_COMBINED_MASK) {
|
||||
this._combined[destCol] = src._combined[srcCol];
|
||||
}
|
||||
if (src._data[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) {
|
||||
this._extendedAttrs[destCol] = src._extendedAttrs[srcCol];
|
||||
}
|
||||
}
|
||||
|
||||
/** Rebuild sparse maps from another line, keyed only by `_data` flags. */
|
||||
private _copySparseMapsFrom(line: BufferLine): void {
|
||||
this._combined = {};
|
||||
this._extendedAttrs = {};
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
this._copyCellMapsFrom(line, i, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,13 @@
|
||||
* @license MIT
|
||||
*/
|
||||
import { assert } from 'chai';
|
||||
import { CircularList } from '../CircularList';
|
||||
import { BufferLine } from './BufferLine';
|
||||
import { BufferLineStringCache } from './BufferLineStringCache';
|
||||
import { CellData } from './CellData';
|
||||
import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Constants';
|
||||
import { reflowSmallerGetNewLineLengths } from './BufferReflow';
|
||||
import { reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from './BufferReflow';
|
||||
import { IBufferLine } from './Types';
|
||||
|
||||
const TEST_STRING_CACHE = new BufferLineStringCache();
|
||||
|
||||
@@ -93,4 +96,31 @@ describe('BufferReflow', () => {
|
||||
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 2), [2, 2], 'line: 汉, 语');
|
||||
});
|
||||
});
|
||||
describe('reflowLargerGetLinesToRemove', () => {
|
||||
const nullCell = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
|
||||
|
||||
function createWrappedLines(chars: string): CircularList<IBufferLine> {
|
||||
const lines = new CircularList<IBufferLine>(chars.length);
|
||||
for (let i = 0; i < chars.length; i++) {
|
||||
const line = new BufferLine(TEST_STRING_CACHE, 1);
|
||||
line.set(0, [0, chars[i], 1, chars.charCodeAt(i)]);
|
||||
line.isWrapped = i > 0;
|
||||
lines.push(line);
|
||||
}
|
||||
return lines;
|
||||
}
|
||||
|
||||
it('should skip reflow when the cursor is in a wrapped block and reflowCursorLine is false', () => {
|
||||
const lines = createWrappedLines('abcde');
|
||||
const skipped = reflowLargerGetLinesToRemove(lines, 1, 5, 2, nullCell, false);
|
||||
const reflowed = reflowLargerGetLinesToRemove(lines, 1, 5, 2, nullCell, true);
|
||||
assert.deepEqual(skipped, []);
|
||||
assert.notDeepEqual(reflowed, []);
|
||||
});
|
||||
|
||||
it('should reflow wrapped blocks when the cursor is outside the block', () => {
|
||||
const lines = createWrappedLines('abcde');
|
||||
assert.notDeepEqual(reflowLargerGetLinesToRemove(lines, 1, 5, 10, nullCell, false), []);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -21,7 +21,7 @@ function identifier(id: IFunctionIdentifier): number {
|
||||
throw new Error('only one byte as prefix supported');
|
||||
}
|
||||
res = id.prefix.charCodeAt(0);
|
||||
if (res && 0x3c > res || res > 0x3f) {
|
||||
if (res < 0x3c || res > 0x3f) {
|
||||
throw new Error('prefix must be in range 0x3c .. 0x3f');
|
||||
}
|
||||
}
|
||||
@@ -459,4 +459,29 @@ describe('ApcParser - async tests', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('reset', () => {
|
||||
it('should abort active handlers with end(false) when reset during payload', () => {
|
||||
const ident = identifier({ intermediates: '+', final: 'p' });
|
||||
parser.registerHandler(ident, new TestHandler(reports, 'th'));
|
||||
parser.start(ident);
|
||||
let data = toUtf32('partial');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.reset();
|
||||
assert.deepEqual(reports, [
|
||||
['th', 'START'],
|
||||
['th', 'PUT', 'partial'],
|
||||
['th', 'END', false]
|
||||
]);
|
||||
reports.length = 0;
|
||||
parser.start(ident);
|
||||
data = toUtf32('complete');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
['th', 'START'],
|
||||
['th', 'PUT', 'complete'],
|
||||
['th', 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,7 @@ function identifier(id: IFunctionIdentifier): number {
|
||||
throw new Error('only one byte as prefix supported');
|
||||
}
|
||||
res = id.prefix.charCodeAt(0);
|
||||
if (res && 0x3c > res || res > 0x3f) {
|
||||
if (res < 0x3c || res > 0x3f) {
|
||||
throw new Error('prefix must be in range 0x3c .. 0x3f');
|
||||
}
|
||||
}
|
||||
@@ -470,4 +470,30 @@ describe('DcsParser - async tests', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('reset', () => {
|
||||
it('should abort active handlers with unhook(false) when reset during payload', () => {
|
||||
const ident = identifier({ intermediates: '+', final: 'p' });
|
||||
const params = Params.fromArray([1, 2, 3]);
|
||||
parser.registerHandler(ident, new TestHandler(reports, 'th'));
|
||||
parser.hook(ident, params);
|
||||
let data = toUtf32('partial');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.reset();
|
||||
assert.deepEqual(reports, [
|
||||
['th', 'HOOK', [1, 2, 3]],
|
||||
['th', 'PUT', 'partial'],
|
||||
['th', 'UNHOOK', false]
|
||||
]);
|
||||
reports.length = 0;
|
||||
parser.hook(ident, params);
|
||||
data = toUtf32('complete');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.unhook(true);
|
||||
assert.deepEqual(reports, [
|
||||
['th', 'HOOK', [1, 2, 3]],
|
||||
['th', 'PUT', 'complete'],
|
||||
['th', 'UNHOOK', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -243,7 +243,7 @@ describe('EscapeSequenceParser', () => {
|
||||
p = new TestEscapeSequenceParser(tansitions);
|
||||
assert.deepEqual(p.transitions, tansitions);
|
||||
});
|
||||
it('inital states', () => {
|
||||
it('initial states', () => {
|
||||
assert.equal(parser.initialState, ParserState.GROUND);
|
||||
assert.equal(parser.currentState, ParserState.GROUND);
|
||||
assert.equal(parser.osc, '');
|
||||
@@ -2177,6 +2177,38 @@ describe('EscapeSequenceParser - async', () => {
|
||||
parser.reset();
|
||||
await parseP(parser, INPUT); // does not throw anymore
|
||||
});
|
||||
it('reset during async pause continues at next codepoint in chunk', async () => {
|
||||
const localParser = new TestEscapeSequenceParser();
|
||||
const localStack: any[] = [];
|
||||
localParser.setPrintHandler((data, start, end) => {
|
||||
let result = '';
|
||||
for (let i = start; i < end; ++i) {
|
||||
result += stringFromCodePoint(data[i]);
|
||||
}
|
||||
localStack.push(['PRINT', result]);
|
||||
});
|
||||
localParser.registerCsiHandler({ final: 'm' }, async params => {
|
||||
localStack.push(['SGR', params.toArray()]);
|
||||
return new Promise<boolean>(resolve => setTimeout(() => resolve(true), 0));
|
||||
});
|
||||
const data = '\x1b[1mXY';
|
||||
const container = new Uint32Array(data.length);
|
||||
const decoder = new StringToUtf32();
|
||||
const len = decoder.decode(data, container);
|
||||
|
||||
assert.ok(localParser.parse(container, len) instanceof Promise);
|
||||
localParser.reset();
|
||||
assert.equal(localParser.parseStack.state, ParserStackType.RESET);
|
||||
|
||||
let prev: boolean | undefined = true;
|
||||
let result: void | Promise<boolean> | undefined;
|
||||
while (result = localParser.parse(container, len, prev)) {
|
||||
prev = await result;
|
||||
}
|
||||
|
||||
assert.deepEqual(localStack, [['SGR', [1]], ['PRINT', 'XY']]);
|
||||
assert.equal(localParser.parseStack.state, ParserStackType.NONE);
|
||||
});
|
||||
it('correct result on awaited parse call', async () => {
|
||||
await parseP(parser, INPUT);
|
||||
assert.deepEqual(callstack, RESULT);
|
||||
|
||||
@@ -342,7 +342,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
|
||||
throw new Error('only one byte as prefix supported');
|
||||
}
|
||||
res = id.prefix.charCodeAt(0);
|
||||
if (res && 0x3c > res || res > 0x3f) {
|
||||
if (res < 0x3c || res > 0x3f) {
|
||||
throw new Error('prefix must be in range 0x3c .. 0x3f');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,4 +464,28 @@ describe('OscParser - async tests', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('reset', () => {
|
||||
it('should abort active handlers with end(false) when reset during payload', () => {
|
||||
parser.registerHandler(1234, new TestHandler(1234, reports, 'th'));
|
||||
parser.start();
|
||||
let data = toUtf32('1234;partial');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.reset();
|
||||
assert.deepEqual(reports, [
|
||||
['th', 1234, 'START'],
|
||||
['th', 1234, 'PUT', 'partial'],
|
||||
['th', 1234, 'END', false]
|
||||
]);
|
||||
reports.length = 0;
|
||||
parser.start();
|
||||
data = toUtf32('1234;complete');
|
||||
parser.put(data, 0, data.length);
|
||||
parser.end(true);
|
||||
assert.deepEqual(reports, [
|
||||
['th', 1234, 'START'],
|
||||
['th', 1234, 'PUT', 'complete'],
|
||||
['th', 1234, 'END', true]
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -199,13 +199,13 @@ describe('Params', () => {
|
||||
it('reject params lesser -1', () => {
|
||||
const params = new Params();
|
||||
params.addParam(-1);
|
||||
assert.throws(() => params.addParam(-2), 'values lesser than -1 are not allowed');
|
||||
assert.throws(() => params.addParam(-2), 'values less than -1 are not allowed');
|
||||
});
|
||||
it('reject subparams lesser -1', () => {
|
||||
const params = new Params();
|
||||
params.addParam(-1);
|
||||
params.addSubParam(-1);
|
||||
assert.throws(() => params.addSubParam(-2), 'values lesser than -1 are not allowed');
|
||||
assert.throws(() => params.addSubParam(-2), 'values less than -1 are not allowed');
|
||||
assert.deepEqual(params.toArray(), [-1, [-1]]);
|
||||
});
|
||||
it('clamp parsed params', () => {
|
||||
|
||||
@@ -162,7 +162,7 @@ export class Params implements IParams {
|
||||
return;
|
||||
}
|
||||
if (value < -1) {
|
||||
throw new Error('values lesser than -1 are not allowed');
|
||||
throw new Error('values less than -1 are not allowed');
|
||||
}
|
||||
this._subParamsIdx[this.length] = this._subParamsLength << 8 | this._subParamsLength;
|
||||
this.params[this.length++] = value > Constants.MAX_VALUE ? Constants.MAX_VALUE : value;
|
||||
@@ -185,7 +185,7 @@ export class Params implements IParams {
|
||||
return;
|
||||
}
|
||||
if (value < -1) {
|
||||
throw new Error('values lesser than -1 are not allowed');
|
||||
throw new Error('values less than -1 are not allowed');
|
||||
}
|
||||
this._subParams[this._subParamsLength++] = value > Constants.MAX_VALUE ? Constants.MAX_VALUE : value;
|
||||
this._subParamsIdx[this.length - 1]++;
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { AttributeData } from '../buffer/AttributeData';
|
||||
import { BufferService } from './BufferService';
|
||||
import { OptionsService } from './OptionsService';
|
||||
import { MockLogService } from '../TestUtils.test';
|
||||
|
||||
describe('BufferService', () => {
|
||||
describe('scroll', () => {
|
||||
const eraseAttr = new AttributeData();
|
||||
|
||||
it('should decrement ydisp when the buffer is full and the user has scrolled up', () => {
|
||||
const optionsService = new OptionsService({ rows: 3, cols: 10, scrollback: 2 });
|
||||
const bufferService = new BufferService(optionsService, new MockLogService());
|
||||
const buffer = bufferService.buffer;
|
||||
|
||||
while (!buffer.lines.isFull) {
|
||||
bufferService.scroll(eraseAttr);
|
||||
}
|
||||
assert.strictEqual(buffer.lines.length, 5);
|
||||
|
||||
bufferService.isUserScrolling = true;
|
||||
buffer.ydisp = 2;
|
||||
const ybaseBefore = buffer.ybase;
|
||||
|
||||
bufferService.scroll(eraseAttr);
|
||||
|
||||
assert.strictEqual(buffer.ybase, ybaseBefore);
|
||||
assert.strictEqual(buffer.ydisp, 1);
|
||||
});
|
||||
|
||||
it('should not advance ydisp with ybase while the user has scrolled up and the buffer is not full', () => {
|
||||
const optionsService = new OptionsService({ rows: 3, cols: 10, scrollback: 2 });
|
||||
const bufferService = new BufferService(optionsService, new MockLogService());
|
||||
const buffer = bufferService.buffer;
|
||||
|
||||
bufferService.isUserScrolling = true;
|
||||
buffer.ydisp = 0;
|
||||
const ybaseBefore = buffer.ybase;
|
||||
|
||||
bufferService.scroll(eraseAttr);
|
||||
|
||||
assert.strictEqual(buffer.ybase, ybaseBefore + 1);
|
||||
assert.strictEqual(buffer.ydisp, 0);
|
||||
});
|
||||
|
||||
it('should follow ybase with ydisp when the user is not scrolling', () => {
|
||||
const optionsService = new OptionsService({ rows: 3, cols: 10, scrollback: 2 });
|
||||
const bufferService = new BufferService(optionsService, new MockLogService());
|
||||
const buffer = bufferService.buffer;
|
||||
|
||||
while (!buffer.lines.isFull) {
|
||||
bufferService.scroll(eraseAttr);
|
||||
}
|
||||
|
||||
bufferService.isUserScrolling = false;
|
||||
bufferService.scroll(eraseAttr);
|
||||
|
||||
assert.strictEqual(buffer.ydisp, buffer.ybase);
|
||||
});
|
||||
|
||||
it('should scroll within DECSTBM margins without affecting lines outside the region', () => {
|
||||
const optionsService = new OptionsService({ rows: 5, cols: 10, scrollback: 10 });
|
||||
const bufferService = new BufferService(optionsService, new MockLogService());
|
||||
const buffer = bufferService.buffer;
|
||||
|
||||
const markRow = (row: number, ch: string) => {
|
||||
buffer.lines.get(buffer.ybase + row)!.setCellFromCodepoint(0, ch.charCodeAt(0), 1, eraseAttr);
|
||||
};
|
||||
markRow(0, 'A');
|
||||
markRow(1, 'B');
|
||||
markRow(2, 'C');
|
||||
markRow(3, 'D');
|
||||
markRow(4, 'E');
|
||||
buffer.scrollTop = 1;
|
||||
buffer.scrollBottom = 3;
|
||||
|
||||
bufferService.scroll(eraseAttr);
|
||||
|
||||
assert.strictEqual(buffer.lines.get(buffer.ybase + 0)!.translateToString().trim(), 'A');
|
||||
assert.strictEqual(buffer.lines.get(buffer.ybase + 1)!.translateToString().trim(), 'C');
|
||||
assert.strictEqual(buffer.lines.get(buffer.ybase + 2)!.translateToString().trim(), 'D');
|
||||
assert.strictEqual(buffer.lines.get(buffer.ybase + 3)!.translateToString(true).trim(), '');
|
||||
assert.strictEqual(buffer.lines.get(buffer.ybase + 4)!.translateToString().trim(), 'E');
|
||||
});
|
||||
});
|
||||
|
||||
describe('scrollLines', () => {
|
||||
it('should move ydisp and set isUserScrolling when scrolling up', () => {
|
||||
const optionsService = new OptionsService({ rows: 10, cols: 80, scrollback: 10 });
|
||||
const bufferService = new BufferService(optionsService, new MockLogService());
|
||||
const buffer = bufferService.buffer;
|
||||
buffer.ybase = 5;
|
||||
buffer.ydisp = 5;
|
||||
|
||||
let scrollEvent: number | undefined;
|
||||
bufferService.onScroll(e => { scrollEvent = e; });
|
||||
|
||||
bufferService.scrollLines(-2);
|
||||
|
||||
assert.strictEqual(buffer.ydisp, 3);
|
||||
assert.strictEqual(bufferService.isUserScrolling, true);
|
||||
assert.strictEqual(scrollEvent, 3);
|
||||
});
|
||||
|
||||
it('should not scroll above the top of the buffer', () => {
|
||||
const optionsService = new OptionsService({ rows: 10, cols: 80, scrollback: 10 });
|
||||
const bufferService = new BufferService(optionsService, new MockLogService());
|
||||
const buffer = bufferService.buffer;
|
||||
buffer.ybase = 5;
|
||||
buffer.ydisp = 0;
|
||||
|
||||
bufferService.scrollLines(-1);
|
||||
|
||||
assert.strictEqual(buffer.ydisp, 0);
|
||||
assert.strictEqual(bufferService.isUserScrolling, false);
|
||||
});
|
||||
|
||||
it('should clear isUserScrolling when scrolling to the bottom', () => {
|
||||
const optionsService = new OptionsService({ rows: 10, cols: 80, scrollback: 10 });
|
||||
const bufferService = new BufferService(optionsService, new MockLogService());
|
||||
const buffer = bufferService.buffer;
|
||||
buffer.ybase = 5;
|
||||
buffer.ydisp = 2;
|
||||
bufferService.isUserScrolling = true;
|
||||
|
||||
bufferService.scrollLines(10);
|
||||
|
||||
assert.strictEqual(buffer.ydisp, 5);
|
||||
assert.strictEqual(bufferService.isUserScrolling, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { CHARSETS } from '../data/Charsets';
|
||||
import { CharsetService } from './CharsetService';
|
||||
|
||||
describe('CharsetService', () => {
|
||||
let service: CharsetService;
|
||||
|
||||
beforeEach(() => {
|
||||
service = new CharsetService();
|
||||
});
|
||||
|
||||
it('should not update active charset when designating an inactive glevel', () => {
|
||||
service.setgCharset(1, CHARSETS['0']);
|
||||
assert.strictEqual(service.glevel, 0);
|
||||
assert.ok(service.charset === undefined);
|
||||
});
|
||||
|
||||
it('should expose the designated charset after setgLevel', () => {
|
||||
service.setgCharset(1, CHARSETS['0']);
|
||||
service.setgLevel(1);
|
||||
assert.strictEqual(service.charset, CHARSETS['0']);
|
||||
});
|
||||
|
||||
it('should update active charset when designating the current glevel', () => {
|
||||
service.setgLevel(1);
|
||||
service.setgCharset(1, CHARSETS['0']);
|
||||
assert.strictEqual(service.charset, CHARSETS['0']);
|
||||
});
|
||||
|
||||
it('should reset glevel, charsets, and active charset', () => {
|
||||
service.setgCharset(1, CHARSETS['0']);
|
||||
service.setgLevel(1);
|
||||
service.reset();
|
||||
assert.strictEqual(service.glevel, 0);
|
||||
assert.deepEqual(service.charsets, []);
|
||||
assert.ok(service.charset === undefined);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
*/
|
||||
|
||||
import { perfContext, before, RuntimeCase } from 'xterm-benchmark';
|
||||
import { DecorationService } from '../../src/common/services/DecorationService';
|
||||
import { MockLogService, MockBufferService, MockOptionsService } from '../../src/common/TestUtils.test';
|
||||
import { DecorationService } from 'common/services/DecorationService';
|
||||
import { MockLogService, MockBufferService, MockOptionsService } from 'common/TestUtils.test';
|
||||
const enum Constants {
|
||||
COLS = 80,
|
||||
ROWS = 30,
|
||||
|
||||
@@ -4,12 +4,12 @@
|
||||
*/
|
||||
import { perfContext, before, beforeEach, ThroughputRuntimeCase } from 'xterm-benchmark';
|
||||
|
||||
import { EscapeSequenceParser } from '../../src/common/parser/EscapeSequenceParser';
|
||||
import { C0, C1 } from '../../src/common/data/EscapeSequences';
|
||||
import { IDcsHandler, IOscHandler, IApcHandler, IParams } from '../../src/common/parser/Types';
|
||||
import { OscHandler } from '../../src/common/parser/OscParser';
|
||||
import { DcsHandler } from '../../src/common/parser/DcsParser';
|
||||
import { ApcHandler } from '../../src/common/parser/ApcParser';
|
||||
import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
|
||||
import { C0, C1 } from 'common/data/EscapeSequences';
|
||||
import { IDcsHandler, IOscHandler, IApcHandler, IParams } from 'common/parser/Types';
|
||||
import { OscHandler } from 'common/parser/OscParser';
|
||||
import { DcsHandler } from 'common/parser/DcsParser';
|
||||
import { ApcHandler } from 'common/parser/ApcParser';
|
||||
|
||||
const SIZE = 5000000;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { perfContext, before, RuntimeCase } from 'xterm-benchmark';
|
||||
import { Emitter } from '../../src/common/Event';
|
||||
import { Emitter } from 'common/Event';
|
||||
|
||||
const ITERATIONS = 1_000_000;
|
||||
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
import { perfContext, before, ThroughputRuntimeCase } from 'xterm-benchmark';
|
||||
|
||||
import { spawn } from 'node-pty';
|
||||
import { Utf8ToUtf32, stringFromCodePoint } from '../../src/common/input/TextDecoder';
|
||||
import { CoreBrowserTerminal } from '../../src/browser/CoreBrowserTerminal';
|
||||
import { Utf8ToUtf32, stringFromCodePoint } from 'common/input/TextDecoder';
|
||||
import { CoreBrowserTerminal } from 'browser/CoreBrowserTerminal';
|
||||
|
||||
perfContext('Terminal: ls -lR /usr/lib', () => {
|
||||
let content = '';
|
||||
|
||||
@@ -11,7 +11,11 @@
|
||||
"moduleResolution": "nodenext",
|
||||
"strict": false,
|
||||
"target": "es2021",
|
||||
"module": "nodenext"
|
||||
"module": "nodenext",
|
||||
"paths": {
|
||||
"common/*": ["../../src/common/*"],
|
||||
"browser/*": ["../../src/browser/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"./**/*",
|
||||
|
||||
@@ -980,7 +980,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
|
||||
});
|
||||
|
||||
test.describe('selectionInactiveBackground', async () => {
|
||||
test('should render the the inactive selection when not focused', async () => {
|
||||
test('should render the inactive selection when not focused', async () => {
|
||||
const theme: ITheme = {
|
||||
selectionBackground: '#FF000080',
|
||||
selectionInactiveBackground: '#0000FF80'
|
||||
|
||||
Reference in New Issue
Block a user