Compare commits

..
Author SHA1 Message Date
Cursor AgentandDaniel Imms dec5452fc0 fix(buffer): avoid shared _extendedAttrs sentinel breaking addon-image
EMPTY_SPARSE_EXTENDED was shared across lines via copyFrom; addon-image
writes _extendedAttrs directly without copy-on-write, which caused tile
eviction failures and broke instanceof Object checks.

Use undefined for empty extended maps (keep EMPTY_SPARSE_MAP for _combined).
Ensure addon-image forks the map before writes and update the accessor test.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 21:51:23 +00:00
Daniel Imms 81b3b1ec46 Merge branch 'master' into cursor/optimize-bufferline-copyfrom-71e1 2026-06-06 09:58:47 -07:00
Daniel Imms 8ab575621d Merge branch 'master' into cursor/optimize-bufferline-copyfrom-71e1 2026-06-06 09:35:21 -07:00
Cursor AgentandDaniel Imms 3b08b7dcef Remove CDP profiling script; document sentinel design in BufferLine
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-06 16:22:34 +00:00
Cursor AgentandDaniel Imms b44d036524 Improve copyFrom for empty and sparse sparse-map paths
Use shared empty-map sentinels with copy-on-write for scroll recycling,
and an inlined _data flag scan for populated sparse maps (faster than
per-cell function calls on master). Avoid slow Object.assign map copies.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-04 22:26:20 +00:00
Cursor AgentandDaniel Imms 9872be0c29 Optimize BufferLine.copyFrom sparse map copying
Skip O(cols) _copyCellMapsFrom loop when the source line has no sparse
combined/extended map entries (scroll recycle from cached blank line).

Copy sparse entries by map keys only when needed. Add CDP profiling
script and BufferLine.copyFrom micro-benchmark.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-06-04 21:05:33 +00:00
11 changed files with 318 additions and 268 deletions
-1
View File
@@ -254,7 +254,6 @@ 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. - [**WooTTY**](https://github.com/icoretech/wootty): Flawless browser terminal for real operators.
- [**Orca**](https://github.com/stablyai/orca): Agentic development environment for 100x builders. - [**Orca**](https://github.com/stablyai/orca): Agentic development environment for 100x builders.
- [**4bit**](https://ciembor.github.io/4bit): Terminal Color Scheme Designer. - [**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) - [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. 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.
+10 -10
View File
@@ -436,9 +436,9 @@ export class ImageStorage implements IDisposable {
for (let col = 0; col < cols; ++col) { for (let col = 0; col < cols; ++col) {
let e: IExtendedAttrsImage; let e: IExtendedAttrsImage;
if (line.getBg(col) & BgFlags.HAS_EXTENDED) { if (line.getBg(col) & BgFlags.HAS_EXTENDED) {
e = line._extendedAttrs[col] ?? EMPTY_ATTRS; e = line._extendedAttrs?.[col] ?? EMPTY_ATTRS;
} else { } else {
const maybeImg = line._extendedAttrs[col] as IExtendedAttrsImage | undefined; const maybeImg = line._extendedAttrs?.[col] as IExtendedAttrsImage | undefined;
if (!maybeImg || maybeImg.imageId === undefined || maybeImg.imageId === -1) { if (!maybeImg || maybeImg.imageId === undefined || maybeImg.imageId === -1) {
continue; continue;
} }
@@ -461,7 +461,7 @@ export class ImageStorage implements IDisposable {
* Also check _extendedAttrs directly for cells where text cleared HAS_EXTENDED. * Also check _extendedAttrs directly for cells where text cleared HAS_EXTENDED.
*/ */
while (++col < cols) { while (++col < cols) {
const nextE = line._extendedAttrs[col] as IExtendedAttrsImage | undefined; const nextE = line._extendedAttrs?.[col] as IExtendedAttrsImage | undefined;
if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) { if (!nextE || nextE.imageId !== imageId || nextE.tileId !== startTile + count) {
break; break;
} }
@@ -515,7 +515,7 @@ export class ImageStorage implements IDisposable {
for (let row = 0; row < rows; ++row) { for (let row = 0; row < rows; ++row) {
const line = buffer.lines.get(row) as IBufferLineExt; const line = buffer.lines.get(row) as IBufferLineExt;
if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) { if (line.getBg(oldCol) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs[oldCol] ?? EMPTY_ATTRS; const e: IExtendedAttrsImage = line._extendedAttrs?.[oldCol] ?? EMPTY_ATTRS;
const imageId = e.imageId; const imageId = e.imageId;
if (imageId === undefined || imageId === -1) { if (imageId === undefined || imageId === -1) {
continue; continue;
@@ -560,7 +560,7 @@ export class ImageStorage implements IDisposable {
const buffer = this._terminal._core.buffer; const buffer = this._terminal._core.buffer;
const line = buffer.lines.get(y) as IBufferLineExt; const line = buffer.lines.get(y) as IBufferLineExt;
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) { if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS; const e: IExtendedAttrsImage = line._extendedAttrs?.[x] ?? EMPTY_ATTRS;
if (e.imageId && e.imageId !== -1) { if (e.imageId && e.imageId !== -1) {
const orig = this._images.get(e.imageId)?.orig; const orig = this._images.get(e.imageId)?.orig;
if (window.ImageBitmap && orig instanceof ImageBitmap) { if (window.ImageBitmap && orig instanceof ImageBitmap) {
@@ -580,7 +580,7 @@ export class ImageStorage implements IDisposable {
const buffer = this._terminal._core.buffer; const buffer = this._terminal._core.buffer;
const line = buffer.lines.get(y) as IBufferLineExt; const line = buffer.lines.get(y) as IBufferLineExt;
if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) { if (line && line.getBg(x) & BgFlags.HAS_EXTENDED) {
const e: IExtendedAttrsImage = line._extendedAttrs[x] ?? EMPTY_ATTRS; const e: IExtendedAttrsImage = line._extendedAttrs?.[x] ?? EMPTY_ATTRS;
if (e.imageId && e.imageId !== -1 && e.tileId !== -1) { if (e.imageId && e.imageId !== -1 && e.tileId !== -1) {
const spec = this._images.get(e.imageId); const spec = this._images.get(e.imageId);
if (spec) { if (spec) {
@@ -611,7 +611,7 @@ export class ImageStorage implements IDisposable {
private _writeToCell(line: IBufferLineExt, x: number, imageId: number, tileId: number): void { private _writeToCell(line: IBufferLineExt, x: number, imageId: number, tileId: number): void {
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) { if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
const old = line._extendedAttrs[x]; const old = line._extendedAttrs?.[x];
if (old) { if (old) {
if (old.imageId !== undefined) { if (old.imageId !== undefined) {
// found an old ExtendedAttrsImage, since we know that // found an old ExtendedAttrsImage, since we know that
@@ -627,13 +627,13 @@ export class ImageStorage implements IDisposable {
return; return;
} }
// found a plain ExtendedAttrs instance, clone it to new entry // found a plain ExtendedAttrs instance, clone it to new entry
line._extendedAttrs[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId); (line._extendedAttrs ??= {})[x] = new ExtendedAttrsImage(old.ext, old.urlId, imageId, tileId);
return; return;
} }
} }
// fall-through: always create new ExtendedAttrsImage entry // fall-through: always create new ExtendedAttrsImage entry
line._data[x * Cell.SIZE + Cell.BG] |= BgFlags.HAS_EXTENDED; line._data[x * Cell.SIZE + Cell.BG] |= BgFlags.HAS_EXTENDED;
line._extendedAttrs[x] = new ExtendedAttrsImage(0, 0, imageId, tileId); (line._extendedAttrs ??= {})[x] = new ExtendedAttrsImage(0, 0, imageId, tileId);
} }
private _evictOnAlternate(): void { private _evictOnAlternate(): void {
@@ -652,7 +652,7 @@ export class ImageStorage implements IDisposable {
} }
for (let x = 0; x < this._terminal.cols; ++x) { for (let x = 0; x < this._terminal.cols; ++x) {
if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) { if (line._data[x * Cell.SIZE + Cell.BG] & BgFlags.HAS_EXTENDED) {
const imgId = line._extendedAttrs[x]?.imageId; const imgId = line._extendedAttrs?.[x]?.imageId;
if (imgId) { if (imgId) {
const spec = this._images.get(imgId); const spec = this._images.get(imgId);
if (spec) { if (spec) {
+1 -1
View File
@@ -64,7 +64,7 @@ export interface IExtendedAttrsImage extends IExtendedAttrs {
/* eslint-disable */ /* eslint-disable */
export interface IBufferLineExt extends IBufferLine { export interface IBufferLineExt extends IBufferLine {
_extendedAttrs: {[index: number]: IExtendedAttrsImage | undefined}; _extendedAttrs: {[index: number]: IExtendedAttrsImage | undefined} | undefined;
_data: Uint32Array; _data: Uint32Array;
} }
+4 -1
View File
@@ -118,7 +118,10 @@ test.describe('ImageAddon', () => {
} }
// bufferline privates // bufferline privates
strictEqual(await ctx.page.evaluate('window.term._core.buffer.lines.get(0)._data instanceof Uint32Array'), true); strictEqual(await ctx.page.evaluate('window.term._core.buffer.lines.get(0)._data instanceof Uint32Array'), true);
strictEqual(await ctx.page.evaluate('window.term._core.buffer.lines.get(0)._extendedAttrs instanceof Object'), true); strictEqual(await ctx.page.evaluate(`(() => {
const ea = window.term._core.buffer.lines.get(0)._extendedAttrs;
return ea === undefined || (typeof ea === 'object' && ea !== null);
})()`), true);
// inputhandler privates // inputhandler privates
strictEqual(await ctx.page.evaluate('window.term._core._inputHandler._curAttrData.constructor.name'), '_AttributeData'); strictEqual(await ctx.page.evaluate('window.term._core._inputHandler._curAttrData.constructor.name'), '_AttributeData');
strictEqual(await ctx.page.evaluate('window.term._core._inputHandler._parser.constructor.name'), 'EscapeSequenceParser'); strictEqual(await ctx.page.evaluate('window.term._core._inputHandler._parser.constructor.name'), 'EscapeSequenceParser');
+6 -8
View File
@@ -142,14 +142,12 @@ export class SearchAddon extends Disposable implements ITerminalAddon, ISearchAp
} }
prevResult = result; prevResult = result;
results.push(prevResult); results.push(prevResult);
const cols = this._terminal.cols; result = this._engine.find(
let nextCol = prevResult.col + prevResult.size; term,
let nextRow = prevResult.row; prevResult.col + prevResult.term.length >= this._terminal.cols ? prevResult.row + 1 : prevResult.row,
if (nextCol >= cols) { prevResult.col + prevResult.term.length >= this._terminal.cols ? 0 : prevResult.col + 1,
nextRow += Math.floor(nextCol / cols); searchOptions
nextCol = nextCol % cols; );
}
result = this._engine.find(term, nextRow, nextCol, searchOptions);
} }
this._resultTracker.updateResults(results, this._highlightLimit); this._resultTracker.updateResults(results, this._highlightLimit);
@@ -414,20 +414,6 @@ test.describe('Search Tests', () => {
}); });
test.describe('Regression 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', () => { test.describe('#2444 wrapped line content not being found', () => {
let fixture: string; let fixture: string;
test.beforeAll(async () => { test.beforeAll(async () => {
+107 -107
View File
@@ -663,9 +663,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz",
"integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -680,9 +680,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz",
"integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -697,9 +697,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz",
"integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -714,9 +714,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz",
"integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -731,9 +731,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz",
"integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -748,9 +748,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz",
"integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -765,9 +765,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz",
"integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -782,9 +782,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz",
"integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -799,9 +799,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz",
"integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@@ -816,9 +816,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz",
"integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -833,9 +833,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz",
"integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -850,9 +850,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz",
"integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@@ -867,9 +867,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz",
"integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@@ -884,9 +884,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz",
"integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@@ -901,9 +901,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz",
"integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@@ -918,9 +918,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz",
"integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@@ -935,9 +935,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz",
"integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -952,9 +952,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-arm64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz",
"integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -969,9 +969,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz",
"integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -986,9 +986,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz",
"integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1003,9 +1003,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz",
"integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1020,9 +1020,9 @@
} }
}, },
"node_modules/@esbuild/openharmony-arm64": { "node_modules/@esbuild/openharmony-arm64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz",
"integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1037,9 +1037,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz",
"integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -1054,9 +1054,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz",
"integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -1071,9 +1071,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz",
"integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@@ -1088,9 +1088,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz",
"integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -3869,9 +3869,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.28.1", "version": "0.28.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz",
"integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
@@ -3882,32 +3882,32 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.28.1", "@esbuild/aix-ppc64": "0.28.0",
"@esbuild/android-arm": "0.28.1", "@esbuild/android-arm": "0.28.0",
"@esbuild/android-arm64": "0.28.1", "@esbuild/android-arm64": "0.28.0",
"@esbuild/android-x64": "0.28.1", "@esbuild/android-x64": "0.28.0",
"@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-arm64": "0.28.0",
"@esbuild/darwin-x64": "0.28.1", "@esbuild/darwin-x64": "0.28.0",
"@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.0",
"@esbuild/freebsd-x64": "0.28.1", "@esbuild/freebsd-x64": "0.28.0",
"@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm": "0.28.0",
"@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-arm64": "0.28.0",
"@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-ia32": "0.28.0",
"@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-loong64": "0.28.0",
"@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-mips64el": "0.28.0",
"@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-ppc64": "0.28.0",
"@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-riscv64": "0.28.0",
"@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-s390x": "0.28.0",
"@esbuild/linux-x64": "0.28.1", "@esbuild/linux-x64": "0.28.0",
"@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.0",
"@esbuild/netbsd-x64": "0.28.1", "@esbuild/netbsd-x64": "0.28.0",
"@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.0",
"@esbuild/openbsd-x64": "0.28.1", "@esbuild/openbsd-x64": "0.28.0",
"@esbuild/openharmony-arm64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.0",
"@esbuild/sunos-x64": "0.28.1", "@esbuild/sunos-x64": "0.28.0",
"@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-arm64": "0.28.0",
"@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-ia32": "0.28.0",
"@esbuild/win32-x64": "0.28.1" "@esbuild/win32-x64": "0.28.0"
} }
}, },
"node_modules/escalade": { "node_modules/escalade": {
-93
View File
@@ -351,96 +351,3 @@ describe('MouseService mouseEventsRequireAlt', () => {
assert.isFalse(element.classList.contains(MouseEventCssClasses.ENABLE_MOUSE_EVENTS)); 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, []);
});
});
+10 -18
View File
@@ -21,7 +21,6 @@ interface IMouseBindContext {
readonly target: IMouseServiceTarget; readonly target: IMouseServiceTarget;
readonly focus: () => void; readonly focus: () => void;
readonly requestedEvents: RequestedMouseEvents; readonly requestedEvents: RequestedMouseEvents;
listenerDocument: Document | null;
} }
export class MouseService implements IMouseService { export class MouseService implements IMouseService {
@@ -62,7 +61,7 @@ export class MouseService implements IMouseService {
mousedrag: null, mousedrag: null,
mousemove: null mousemove: null
}; };
const ctx: IMouseBindContext = { target, focus, requestedEvents, listenerDocument: null }; const ctx: IMouseBindContext = { target, focus, requestedEvents };
const eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener> = { const eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener> = {
mouseup: (ev: Event) => this._handleMouseUp(ctx, ev as MouseEvent), mouseup: (ev: Event) => this._handleMouseUp(ctx, ev as MouseEvent),
wheel: (ev: Event) => this._handleWheel(ctx, ev as WheelEvent), wheel: (ev: Event) => this._handleWheel(ctx, ev as WheelEvent),
@@ -88,12 +87,11 @@ export class MouseService implements IMouseService {
// Ensure document-level listeners are removed on dispose // Ensure document-level listeners are removed on dispose
register(toDisposable(() => { register(toDisposable(() => {
const listenerDocument = ctx.listenerDocument ?? document;
if (requestedEvents.mouseup) { if (requestedEvents.mouseup) {
listenerDocument.removeEventListener('mouseup', requestedEvents.mouseup); document.removeEventListener('mouseup', requestedEvents.mouseup);
} }
if (requestedEvents.mousedrag) { if (requestedEvents.mousedrag) {
listenerDocument.removeEventListener('mousemove', requestedEvents.mousedrag); document.removeEventListener('mousemove', requestedEvents.mousedrag);
} }
})); }));
@@ -201,14 +199,12 @@ export class MouseService implements IMouseService {
this._sendEvent(ctx, ev); this._sendEvent(ctx, ev);
if (!ev.buttons) { if (!ev.buttons) {
// if no other button is held remove global handlers // if no other button is held remove global handlers
const listenerDocument = ctx.listenerDocument ?? ctx.target.document;
if (ctx.requestedEvents.mouseup) { if (ctx.requestedEvents.mouseup) {
listenerDocument.removeEventListener('mouseup', ctx.requestedEvents.mouseup); ctx.target.document.removeEventListener('mouseup', ctx.requestedEvents.mouseup);
} }
if (ctx.requestedEvents.mousedrag) { if (ctx.requestedEvents.mousedrag) {
listenerDocument.removeEventListener('mousemove', ctx.requestedEvents.mousedrag); ctx.target.document.removeEventListener('mousemove', ctx.requestedEvents.mousedrag);
} }
ctx.listenerDocument = null;
} }
} }
@@ -250,15 +246,12 @@ export class MouseService implements IMouseService {
// of the terminal element. // of the terminal element.
// Note: Other emulators also do this for 'mousedown' while a button // Note: Other emulators also do this for 'mousedown' while a button
// is held, we currently limit 'mousedown' to the terminal only. // 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) { if (ctx.requestedEvents.mouseup) {
listenerDocument.addEventListener('mouseup', ctx.requestedEvents.mouseup); ctx.target.document.addEventListener('mouseup', ctx.requestedEvents.mouseup);
} }
if (ctx.requestedEvents.mousedrag) { if (ctx.requestedEvents.mousedrag) {
listenerDocument.addEventListener('mousemove', ctx.requestedEvents.mousedrag); ctx.target.document.addEventListener('mousemove', ctx.requestedEvents.mousedrag);
} }
ctx.listenerDocument = listenerDocument;
} }
private _handlePassiveWheel(ctx: IMouseBindContext, ev: WheelEvent): false | void { private _handlePassiveWheel(ctx: IMouseBindContext, ev: WheelEvent): false | void {
@@ -405,9 +398,8 @@ export class MouseService implements IMouseService {
} }
private _handleProtocolChange(ctx: IMouseBindContext, eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener>, events: CoreMouseEventType): void { private _handleProtocolChange(ctx: IMouseBindContext, eventListeners: Record<'mouseup' | 'wheel' | 'mousedrag' | 'mousemove', EventListener>, events: CoreMouseEventType): void {
const { element } = ctx.target; const { element, document } = ctx.target;
const { requestedEvents } = ctx; const { requestedEvents } = ctx;
const listenerDocument = ctx.listenerDocument ?? ctx.target.document;
// apply global changes on events // apply global changes on events
if (events) { if (events) {
if (this._optionsService.rawOptions.logLevel === 'debug') { if (this._optionsService.rawOptions.logLevel === 'debug') {
@@ -442,7 +434,7 @@ export class MouseService implements IMouseService {
if (!(events & CoreMouseEventType.UP)) { if (!(events & CoreMouseEventType.UP)) {
if (requestedEvents.mouseup) { if (requestedEvents.mouseup) {
listenerDocument.removeEventListener('mouseup', requestedEvents.mouseup); document.removeEventListener('mouseup', requestedEvents.mouseup);
} }
requestedEvents.mouseup = null; requestedEvents.mouseup = null;
} else { } else {
@@ -451,7 +443,7 @@ export class MouseService implements IMouseService {
if (!(events & CoreMouseEventType.DRAG)) { if (!(events & CoreMouseEventType.DRAG)) {
if (requestedEvents.mousedrag) { if (requestedEvents.mousedrag) {
listenerDocument.removeEventListener('mousemove', requestedEvents.mousedrag); document.removeEventListener('mousemove', requestedEvents.mousedrag);
} }
requestedEvents.mousedrag = null; requestedEvents.mousedrag = null;
} else { } else {
+67 -15
View File
@@ -71,12 +71,20 @@ export interface IBufferLineStringCache {
* (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop * (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop
* memory allocs / GC pressure can be greatly reduced by reusing the CellData object. * memory allocs / GC pressure can be greatly reduced by reusing the CellData object.
*/ */
/**
* Shared empty sparse map for lines with no combining data.
* Uses a sentinel + copy-on-write so `copyFrom` can detect empty `_combined` with
* reference equality. `_extendedAttrs` uses `undefined` instead (addon-image writes
* the map directly without BufferLine APIs).
*/
const EMPTY_SPARSE_MAP: {[index: number]: string} = Object.create(null);
export class BufferLine implements IBufferLine { export class BufferLine implements IBufferLine {
protected _data: Uint32Array; protected _data: Uint32Array;
/** Sparse cache; only read when `IS_COMBINED_MASK` is set in `_data`. */ /** Sparse cache; only read when `IS_COMBINED_MASK` is set in `_data`. */
protected _combined: {[index: number]: string} = {}; protected _combined: {[index: number]: string} = EMPTY_SPARSE_MAP;
/** Sparse cache; only read when `HAS_EXTENDED` is set in `_data`. */ /** Sparse cache; only read when `HAS_EXTENDED` is set in `_data`. */
protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {}; protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} | undefined;
protected _stringCacheEntryRef: WeakRef<IBufferLineStringCacheEntry> | undefined; protected _stringCacheEntryRef: WeakRef<IBufferLineStringCacheEntry> | undefined;
public length: number; public length: number;
@@ -121,6 +129,7 @@ export class BufferLine implements IBufferLine {
this._invalidateStringCache(); this._invalidateStringCache();
this._data[index * Constants.CELL_INDICIES + Cell.FG] = value[CHAR_DATA_ATTR_INDEX]; this._data[index * Constants.CELL_INDICIES + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
if (value[CHAR_DATA_CHAR_INDEX].length > 1) { if (value[CHAR_DATA_CHAR_INDEX].length > 1) {
this._ensureCombinedMap();
this._combined[index] = value[1]; this._combined[index] = value[1];
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT); this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
} else { } else {
@@ -211,7 +220,7 @@ export class BufferLine implements IBufferLine {
cell.combinedData = ''; cell.combinedData = '';
} }
if (cell.bg & BgFlags.HAS_EXTENDED) { if (cell.bg & BgFlags.HAS_EXTENDED) {
cell.extended = this._extendedAttrs[index]!; cell.extended = this._extendedAttrs![index]!;
} else { } else {
// Do not mutate cell.extended in place: it may still reference this line's map entry from a // 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). // prior loadCell into a reused CellData (e.g. $workCell during insert/delete).
@@ -226,10 +235,11 @@ export class BufferLine implements IBufferLine {
public setCell(index: number, cell: ICellData): void { public setCell(index: number, cell: ICellData): void {
this._invalidateStringCache(); this._invalidateStringCache();
if (cell.content & Content.IS_COMBINED_MASK) { if (cell.content & Content.IS_COMBINED_MASK) {
this._ensureCombinedMap();
this._combined[index] = cell.combinedData; this._combined[index] = cell.combinedData;
} }
if (cell.bg & BgFlags.HAS_EXTENDED) { if (cell.bg & BgFlags.HAS_EXTENDED) {
this._extendedAttrs[index] = cell.extended; this._ensureExtendedAttrsMap()[index] = cell.extended;
} }
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = cell.content; this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = cell.content;
this._data[index * Constants.CELL_INDICIES + Cell.FG] = cell.fg; this._data[index * Constants.CELL_INDICIES + Cell.FG] = cell.fg;
@@ -244,7 +254,7 @@ export class BufferLine implements IBufferLine {
public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void { public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void {
this._invalidateStringCache(); this._invalidateStringCache();
if (attrs.bg & BgFlags.HAS_EXTENDED) { if (attrs.bg & BgFlags.HAS_EXTENDED) {
this._extendedAttrs[index] = attrs.extended; this._ensureExtendedAttrsMap()[index] = attrs.extended;
} }
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT); this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);
this._data[index * Constants.CELL_INDICIES + Cell.FG] = attrs.fg; this._data[index * Constants.CELL_INDICIES + Cell.FG] = attrs.fg;
@@ -262,12 +272,14 @@ export class BufferLine implements IBufferLine {
let content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT]; let content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
if (content & Content.IS_COMBINED_MASK) { if (content & Content.IS_COMBINED_MASK) {
// we already have a combined string, simply add // we already have a combined string, simply add
this._ensureCombinedMap();
this._combined[index] += stringFromCodePoint(codePoint); this._combined[index] += stringFromCodePoint(codePoint);
} else { } else {
if (content & Content.CODEPOINT_MASK) { if (content & Content.CODEPOINT_MASK) {
// normal case for combining chars: // normal case for combining chars:
// - move current leading char + new one into combined string // - move current leading char + new one into combined string
// - set combined flag // - set combined flag
this._ensureCombinedMap();
this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint); this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint);
content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0 content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0
content |= Content.IS_COMBINED_MASK; content |= Content.IS_COMBINED_MASK;
@@ -410,11 +422,13 @@ export class BufferLine implements IBufferLine {
} }
} }
// remove any cut off extended attributes // remove any cut off extended attributes
const extKeys = Object.keys(this._extendedAttrs); if (this._extendedAttrs) {
for (let i = 0; i < extKeys.length; i++) { const extKeys = Object.keys(this._extendedAttrs);
const key = parseInt(extKeys[i], 10); for (let i = 0; i < extKeys.length; i++) {
if (key >= cols) { const key = parseInt(extKeys[i], 10);
delete this._extendedAttrs[key]; if (key >= cols) {
delete this._extendedAttrs[key];
}
} }
} }
} }
@@ -450,8 +464,8 @@ export class BufferLine implements IBufferLine {
} }
return; return;
} }
this._combined = {}; this._combined = EMPTY_SPARSE_MAP;
this._extendedAttrs = {}; this._extendedAttrs = undefined;
for (let i = 0; i < this.length; ++i) { for (let i = 0; i < this.length; ++i) {
this.setCell(i, fillCellData); this.setCell(i, fillCellData);
} }
@@ -608,19 +622,57 @@ export class BufferLine implements IBufferLine {
private _copyCellMapsFrom(src: BufferLine, srcCol: number, destCol: number): void { private _copyCellMapsFrom(src: BufferLine, srcCol: number, destCol: number): void {
const srcStart = srcCol * Constants.CELL_INDICIES; const srcStart = srcCol * Constants.CELL_INDICIES;
if (src._data[srcStart + Cell.CONTENT] & Content.IS_COMBINED_MASK) { if (src._data[srcStart + Cell.CONTENT] & Content.IS_COMBINED_MASK) {
this._ensureCombinedMap();
this._combined[destCol] = src._combined[srcCol]; this._combined[destCol] = src._combined[srcCol];
} }
if (src._data[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) { if (src._data[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) {
this._extendedAttrs[destCol] = src._extendedAttrs[srcCol]; this._ensureExtendedAttrsMap()[destCol] = src._extendedAttrs![srcCol]!;
} }
} }
/** Rebuild sparse maps from another line, keyed only by `_data` flags. */ /** Rebuild sparse maps from another line, keyed only by `_data` flags. */
private _copySparseMapsFrom(line: BufferLine): void { private _copySparseMapsFrom(line: BufferLine): void {
const srcCombined = line._combined;
const srcExtended = line._extendedAttrs;
// Fast path: blank / scroll-recycle source (empty `_combined` sentinel, no extended map).
if (srcCombined === EMPTY_SPARSE_MAP && !srcExtended) {
this._combined = EMPTY_SPARSE_MAP;
this._extendedAttrs = undefined;
return;
}
this._copySparseMapsFromDense(line);
}
/** Copy sparse maps by scanning `_data` flags (inlined, no per-cell function calls). */
private _copySparseMapsFromDense(line: BufferLine): void {
this._combined = {}; this._combined = {};
this._extendedAttrs = {}; this._extendedAttrs = {};
for (let i = 0; i < line.length; i++) { const srcData = line._data;
this._copyCellMapsFrom(line, i, i); const srcCombined = line._combined;
const srcExtended = line._extendedAttrs;
const len = line.length;
const indices = Constants.CELL_INDICIES;
const combined = this._combined;
const extendedAttrs = this._extendedAttrs;
for (let i = 0; i < len; i++) {
const start = i * indices;
if (srcData[start + Cell.CONTENT] & Content.IS_COMBINED_MASK) {
combined[i] = srcCombined[i];
}
if (srcData[start + Cell.BG] & BgFlags.HAS_EXTENDED) {
extendedAttrs[i] = srcExtended![i];
}
} }
} }
private _ensureCombinedMap(): void {
if (this._combined === EMPTY_SPARSE_MAP) {
this._combined = {};
}
}
private _ensureExtendedAttrsMap(): {[index: number]: IExtendedAttrs | undefined} {
return this._extendedAttrs ??= {};
}
} }
+113
View File
@@ -0,0 +1,113 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { perfContext, before, RuntimeCase } from 'xterm-benchmark';
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
import { CellData } from 'common/buffer/CellData';
import { BgFlags, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, UnderlineStyle } from 'common/buffer/Constants';
const ITERATIONS = 500_000;
const stringCache = new BufferLineStringCache();
const nullCell = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
const yCell = CellData.fromCharData([0, 'y', 1, 'y'.charCodeAt(0)]);
function fillPlainY(line: BufferLine, cols: number): void {
for (let i = 0; i < cols; i++) {
line.setCellFromCodepoint(i, 'y'.charCodeAt(0), 1, DEFAULT_ATTR_DATA);
}
}
function setupScrollRecyclePair(cols: number): { recycled: BufferLine, blank: BufferLine } {
const blank = new BufferLine(stringCache, cols, nullCell, false);
const recycled = new BufferLine(stringCache, cols, yCell, false);
fillPlainY(recycled, cols);
return { recycled, blank };
}
/** Source line with combining chars and extended attrs spread across columns. */
function buildSparseSourceLine(cols: number): BufferLine {
const line = new BufferLine(stringCache, cols, nullCell, false);
for (let i = 0; i < cols; i++) {
if (i % 10 === 0) {
const cell = CellData.fromCharData([0, 'a', 1, 'a'.charCodeAt(0)]);
cell.extended.underlineStyle = UnderlineStyle.CURLY;
cell.bg |= BgFlags.HAS_EXTENDED;
line.setCell(i, cell);
} else if (i % 17 === 0) {
line.setCell(i, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
} else {
line.setCellFromCodepoint(i, 'y'.charCodeAt(0), 1, DEFAULT_ATTR_DATA);
}
}
return line;
}
function setupCopyFromSparsePair(cols: number): { dest: BufferLine, src: BufferLine } {
const src = buildSparseSourceLine(cols);
const dest = new BufferLine(stringCache, cols, yCell, false);
fillPlainY(dest, cols);
return { dest, src };
}
perfContext('BufferLine.copyFrom (scroll recycle)', () => {
perfContext('cols=80', () => {
let recycled: BufferLine;
let blank: BufferLine;
before(() => {
({ recycled, blank } = setupScrollRecyclePair(80));
});
new RuntimeCase('', () => {
for (let i = 0; i < ITERATIONS; i++) {
recycled.copyFrom(blank);
}
return { payloadSize: ITERATIONS };
}, { fork: false }).showAverageRuntime();
});
perfContext('cols=279', () => {
let recycled: BufferLine;
let blank: BufferLine;
before(() => {
({ recycled, blank } = setupScrollRecyclePair(279));
});
new RuntimeCase('', () => {
for (let i = 0; i < ITERATIONS; i++) {
recycled.copyFrom(blank);
}
return { payloadSize: ITERATIONS };
}, { fork: false }).showAverageRuntime();
});
});
perfContext('BufferLine.copyFrom (sparse source: combined + extended attrs)', () => {
perfContext('cols=80', () => {
let dest: BufferLine;
let src: BufferLine;
before(() => {
({ dest, src } = setupCopyFromSparsePair(80));
});
new RuntimeCase('', () => {
for (let i = 0; i < ITERATIONS; i++) {
dest.copyFrom(src);
}
return { payloadSize: ITERATIONS };
}, { fork: false }).showAverageRuntime();
});
perfContext('cols=279', () => {
let dest: BufferLine;
let src: BufferLine;
before(() => {
({ dest, src } = setupCopyFromSparsePair(279));
});
new RuntimeCase('', () => {
for (let i = 0; i < ITERATIONS; i++) {
dest.copyFrom(src);
}
return { payloadSize: ITERATIONS };
}, { fork: false }).showAverageRuntime();
});
});