Compare commits

...
Author SHA1 Message Date
Cursor AgentandDaniel Imms 3556f16138 feat(buffer): add logical line storage for wrapped rows
Introduce BufferOverflowLine rows that share cell storage with a parent
BufferLine head, implementing the logical-line model from issue #4800.

- Extend BufferLine with logical capacity and segment-aware indexing
- Add BufferOverflowLine, BufferLineUtils helpers, and unit tests
- Expose attachWrappedRow/clearWrappedRow on Buffer for future integration
- Keep InputHandler on isWrapped flag until reflow/SL paths are updated

Reflow still uses per-row buffers; materializeLogicalLinesForReflow helpers
are included for a follow-up that simplifies resize reflow.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-28 03:42:52 +00:00
Daniel Imms c2ecfe7446 Merge pull request #5895 from xtermjs/dependabot/npm_and_yarn/multi-f792d6d6d9
Bump qs and express
2026-05-27 18:26:29 -07:00
Daniel Imms 5656443163 Merge pull request #5957 from xtermjs/cursor/fix-progress-webkit-flake-5577
fix(addon-progress): stabilize flaky WebKit integration test harness
2026-05-27 17:32:25 -07:00
Cursor AgentandDaniel Imms 38174907f3 fix(addon-progress): stabilize WebKit integration test harness
Remove unnecessary unawaited setViewportSize from ProgressAddon tests,
which was the line reported in flaky CI failures. Use serial mode and
scoped hooks per Playwright shared-page guidance, and guard afterAll
teardown when setup fails.

Fixes #5956

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-28 00:24:06 +00:00
Daniel Imms 91fcb46a1a Merge pull request #5955 from xtermjs/cursor/gitignore-cursor-logs-5a86
Ignore .cursor/*.log files in git
2026-05-27 16:25:02 -07:00
Cursor AgentandDaniel Imms 1d569f873d Ignore .cursor/*.log files in git
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 23:22:32 +00:00
dependabot[bot] e89fbf2979 Bump qs and express
Bumps [qs](https://github.com/ljharb/qs) to 6.15.2 and updates ancestor dependency [express](https://github.com/expressjs/express). These dependencies need to be updated together.


Updates `qs` from 6.14.2 to 6.15.2
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.2...v6.15.2)

Updates `express` from 4.22.1 to 4.22.2
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/v4.22.2/History.md)
- [Commits](https://github.com/expressjs/express/compare/v4.22.1...v4.22.2)

---
updated-dependencies:
- dependency-name: express
  dependency-version: 4.22.2
  dependency-type: direct:development
- dependency-name: qs
  dependency-version: 6.15.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-27 21:53:01 +00:00
Daniel Imms db244ea5a8 Merge pull request #5954 from xtermjs/cursor/align-headless-marker-alt-buffer-2866
Allow markers on alt buffer in headless terminal
2026-05-27 14:51:35 -07:00
Cursor AgentandDaniel Imms fdcd1c2720 Align xterm-headless marker JSDoc with xterm.d.ts
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 21:46:20 +00:00
Cursor AgentandDaniel Imms 57b48f6048 Allow markers on alt buffer in headless terminal
Remove the alt-buffer guard in headless registerMarker so behavior
matches the browser terminal. Update headless public API and typings
to return IMarker instead of IMarker | undefined.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 21:28:36 +00:00
Daniel Imms ec0c93fd5a Merge pull request #5950 from xtermjs/cursor/writebuffer-dispose-timeout-124c
Cancel WriteBuffer innerWrite timers on dispose
2026-05-27 14:14:41 -07:00
Daniel Imms be339bf796 Merge pull request #5947 from xtermjs/cursor/decoration-bucket-merge-loop-7132
Avoid spread when merging decoration line buckets
2026-05-27 13:15:37 -07:00
Daniel Imms 7294124736 Merge pull request #5945 from xtermjs/cursor/bufferline-stringbuilder-5939-79f8
Use StringBuilder in BufferLine.translateToString
2026-05-27 13:11:02 -07:00
Daniel Imms 2c3fad39af Merge pull request #5948 from xtermjs/cursor/hoist-get-current-link-id-e29d
Hoist _getCurrentLinkId() in print() loop
2026-05-27 13:09:09 -07:00
Cursor AgentandDaniel Imms edb4f89dd8 Add WriteBuffer regression tests for dispose and hot paths
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 20:00:06 +00:00
Cursor AgentandDaniel Imms 224c113b64 Move linkId declaration above use site in print loop
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 19:36:45 +00:00
Cursor AgentandDaniel Imms 3e0c7d18e5 Register WriteBuffer cleanup via toDisposable in constructor
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 19:35:58 +00:00
Daniel Imms 95d9224b8c Merge pull request #5946 from xtermjs/cursor/fix-duplicate-buildnumber-guard-cbb8
Fix duplicate buildNumber guard in Windows ConPTY logic
2026-05-27 12:34:08 -07:00
Cursor AgentandDaniel Imms 7499a2181d Use DisposableStore.isDisposed instead of duplicate flag
Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 19:34:08 +00:00
Daniel Imms 7dc820f5fa Merge pull request #5949 from xtermjs/cursor/rename-headless-register-marker-22c3
Rename headless core addMarker → registerMarker
2026-05-27 12:33:02 -07:00
Cursor AgentandDaniel Imms e59a6c1f7b Reuse shared StringBuilder in BufferLine.translateToString
Avoid per-call allocation when translating many lines in sequence by
resetting a module-level work StringBuilder before and after each use.

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 19:31:00 +00:00
Daniel Imms f3e2c08a5d Merge pull request #5944 from xtermjs/cursor/buffer-reflow-single-loop-sum-705e
perf: use single loop for cellsNeeded sum in BufferReflow
2026-05-27 12:28:08 -07:00
Daniel Imms b138bf38d0 Merge pull request #5943 from xtermjs/cursor/remove-duplicate-defineproperty-57b2
Remove duplicate Object.defineProperty loop in headless Terminal
2026-05-27 12:26:36 -07:00
Cursor AgentandDaniel Imms c6e4627b45 Hoist _getCurrentLinkId() in print() loop
Avoid calling _getCurrentLinkId() twice per codepoint when a hyperlink
is active by reading the link id once before the print loop.

Fixes #5940

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 18:18:24 +00:00
Cursor AgentandDaniel Imms d91045dc5f Cancel WriteBuffer innerWrite timers on dispose
Guard _innerWrite and public write methods after disposal so pending
setTimeout continuations cannot run against a disposed terminal.

Fixes #5936

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 18:18:22 +00:00
Cursor AgentandDaniel Imms 41dfdba2cf Avoid spread when merging decoration line buckets
Replace existing.push(...bucket) with an indexed loop in
_mergeLineBucket to avoid intermediate array allocations during
buffer trim/insert/delete re-indexing.

Reuse _mergeLineBucket in _handleBufferLinesTrim for consistency.

Fixes #5941

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 18:18:13 +00:00
Cursor AgentandDaniel Imms ba33decb68 Rename headless core addMarker to registerMarker
Align headless Terminal core API with browser CoreBrowserTerminal
and the public registerMarker naming.

Fixes #5938

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 18:18:01 +00:00
Cursor AgentandDaniel Imms 17e3b84bcc perf: use single loop for cellsNeeded sum in BufferReflow
Replace map().reduce() with one accumulator loop in
reflowSmallerGetNewLineLengths to avoid allocating an intermediate
array when summing wrapped line trimmed lengths.

Fixes #5942

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 18:17:17 +00:00
Cursor AgentandDaniel Imms 182ff2f635 Remove duplicate Object.defineProperty loop in headless Terminal
The constructor defined property getters/setters twice on _publicOptions;
the first block was immediately overwritten by the second. Align with the
browser public Terminal implementation.

Fixes #5935

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 18:17:08 +00:00
Cursor AgentandDaniel Imms bcb4e5298b Fix duplicate buildNumber guard in Windows ConPTY logic
The windowsPty option change handler checked buildNumber twice
instead of requiring both backend and buildNumber to be defined.
This aligns with Buffer.ts and ensures ConPTY wrapping heuristics
are evaluated only when both fields are present.

Fixes #5934

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 18:16:54 +00:00
Cursor AgentandDaniel Imms 42facac2d9 Use StringBuilder in BufferLine.translateToString
Replace O(n²) string concatenation in the translate loop with
StringBuilder.append, matching the pattern used elsewhere in the project.

Fixes #5939

Co-authored-by: Daniel Imms <Tyriar@users.noreply.github.com>
2026-05-27 18:16:51 +00:00
21 changed files with 791 additions and 167 deletions
+1
View File
@@ -24,6 +24,7 @@ test-results/
# Tracing/debug files
.cursor/nal-trace/
.cursor/*.log
# Keep bundled code out of Git
dist/
@@ -9,15 +9,18 @@ import { ITestContext, createTestContext, openTerminal } from '../../../test/pla
let ctx: ITestContext;
test.beforeAll(async ({ browser }) => {
ctx = await createTestContext(browser);
ctx.page.setViewportSize({ width: 1024, height: 768 });
await openTerminal(ctx);
});
test.afterAll(async () => await ctx.page.close());
test.describe('ProgressAddon', () => {
test.describe.configure({ mode: 'serial' });
test.beforeAll(async ({ browser }) => {
ctx = await createTestContext(browser);
await openTerminal(ctx);
});
test.afterAll(async () => {
await ctx?.page?.close();
});
test.beforeEach(async function(): Promise<any> {
await ctx.page.evaluate(`
window.progressStack = [];
+33 -17
View File
@@ -2594,10 +2594,11 @@
}
},
"node_modules/body-parser": {
"version": "1.20.4",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz",
"integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==",
"version": "1.20.5",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz",
"integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==",
"dev": true,
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"content-type": "~1.0.5",
@@ -2607,7 +2608,7 @@
"http-errors": "~2.0.1",
"iconv-lite": "~0.4.24",
"on-finished": "~2.4.1",
"qs": "~6.14.0",
"qs": "~6.15.1",
"raw-body": "~2.5.3",
"type-is": "~1.6.18",
"unpipe": "~1.0.0"
@@ -2622,6 +2623,7 @@
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "2.0.0"
}
@@ -2630,7 +2632,8 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==",
"dev": true
"dev": true,
"license": "MIT"
},
"node_modules/brace-expansion": {
"version": "2.0.2",
@@ -2712,6 +2715,7 @@
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
"integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
@@ -2749,6 +2753,7 @@
"resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
"integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"get-intrinsic": "^1.3.0"
@@ -3144,6 +3149,7 @@
"resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
"integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -3780,14 +3786,15 @@
}
},
"node_modules/express": {
"version": "4.22.1",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
"version": "4.22.2",
"resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz",
"integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"accepts": "~1.3.8",
"array-flatten": "1.1.1",
"body-parser": "~1.20.3",
"body-parser": "~1.20.5",
"content-disposition": "~0.5.4",
"content-type": "~1.0.4",
"cookie": "~0.7.1",
@@ -3806,7 +3813,7 @@
"parseurl": "~1.3.3",
"path-to-regexp": "~0.1.12",
"proxy-addr": "~2.0.7",
"qs": "~6.14.0",
"qs": "~6.15.1",
"range-parser": "~1.2.1",
"safe-buffer": "5.2.1",
"send": "~0.19.0",
@@ -4521,6 +4528,7 @@
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz",
"integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3"
},
@@ -5222,6 +5230,7 @@
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
@@ -5706,6 +5715,7 @@
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
"integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
@@ -6128,9 +6138,9 @@
}
},
"node_modules/qs": {
"version": "6.14.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz",
"integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==",
"version": "6.15.2",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz",
"integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -6166,6 +6176,7 @@
"resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz",
"integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"bytes": "~3.1.2",
"http-errors": "~2.0.1",
@@ -6604,6 +6615,7 @@
"resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
"integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3",
@@ -6619,13 +6631,14 @@
}
},
"node_modules/side-channel-list": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
"integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
"integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
"dev": true,
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"object-inspect": "^1.13.3"
"object-inspect": "^1.13.4"
},
"engines": {
"node": ">= 0.4"
@@ -6639,6 +6652,7 @@
"resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
"integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
@@ -6657,6 +6671,7 @@
"resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
"integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
"dev": true,
"license": "MIT",
"dependencies": {
"call-bound": "^1.0.2",
"es-errors": "^1.3.0",
@@ -7194,6 +7209,7 @@
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
"dev": true,
"license": "MIT",
"dependencies": {
"media-typer": "0.3.0",
"mime-types": "~2.1.24"
+9
View File
@@ -258,6 +258,15 @@ export class MockBuffer implements IBuffer {
public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
return Buffer.prototype.getBlankLine.apply(this, arguments as any);
}
public attachWrappedRow(y: number, fillCellData: ICellData): IBufferLine {
return Buffer.prototype.attachWrappedRow.apply(this, arguments as any);
}
public clearWrappedRow(y: number, fillCellData: ICellData, preserveContent?: boolean): void {
Buffer.prototype.clearWrappedRow.apply(this, arguments as any);
}
public createScrollWrappedLine(lineAbove: IBufferLine | undefined, attr: IAttributeData): IBufferLine {
return Buffer.prototype.createScrollWrappedLine.apply(this, arguments as any);
}
public getNullCell(attr?: IAttributeData): ICellData {
throw new Error('Method not implemented.');
}
+1 -1
View File
@@ -263,7 +263,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
private _handleWindowsPtyOptionChange(): void {
let value = false;
const windowsPty = this.optionsService.rawOptions.windowsPty;
if (windowsPty && windowsPty.buildNumber !== undefined && windowsPty.buildNumber !== undefined) {
if (windowsPty && windowsPty.backend !== undefined && windowsPty.buildNumber !== undefined) {
value = !!(windowsPty.backend === 'conpty' && windowsPty.buildNumber < 21376);
}
if (value) {
+41 -20
View File
@@ -11,6 +11,8 @@ import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
import { Disposable } from 'common/Lifecycle';
import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from 'common/input/TextDecoder';
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { getWrappedRangeEnd } from 'common/buffer/BufferLineUtils';
import { BufferOverflowLine } from 'common/buffer/BufferOverflowLine';
import { IParsingState, IEscapeSequenceParser, IParams, IFunctionIdentifier } from 'common/parser/Types';
import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
@@ -567,8 +569,9 @@ export class InputHandler extends Disposable implements IInputHandler {
if (screenReaderMode) {
this._onA11yChar.fire(stringFromCodePoint(code));
}
if (this._getCurrentLinkId()) {
this._oscLinkService.addLineToLink(this._getCurrentLinkId(), this._activeBuffer.ybase + this._activeBuffer.y);
const linkId = this._getCurrentLinkId();
if (linkId) {
this._oscLinkService.addLineToLink(linkId, this._activeBuffer.ybase + this._activeBuffer.y);
}
// goto next line if ch would overflow
@@ -589,8 +592,7 @@ export class InputHandler extends Disposable implements IInputHandler {
if (this._activeBuffer.y >= this._bufferService.rows) {
this._activeBuffer.y = this._bufferService.rows - 1;
}
// The line already exists (eg. the initial viewport), mark it as a
// wrapped line
// The line already exists (eg. the initial viewport), mark it as a wrapped line
this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = true;
}
// row changed, get it again
@@ -755,7 +757,7 @@ export class InputHandler extends Disposable implements IInputHandler {
// reprint is common, especially on resize. Note that the windowsMode wrapped line heuristics
// can mess with this so windowsMode should be disabled, which is recommended on Windows build
// 21376 and above.
this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false;
this._unwrapBufferRow(this._activeBuffer.ybase + this._activeBuffer.y, false);
}
// If the end of the line is hit, prevent this action from wrapping around to the next line.
if (this._activeBuffer.x >= this._bufferService.cols) {
@@ -819,7 +821,7 @@ export class InputHandler extends Disposable implements IInputHandler {
&& this._activeBuffer.y > this._activeBuffer.scrollTop
&& this._activeBuffer.y <= this._activeBuffer.scrollBottom
&& this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)?.isWrapped) {
this._activeBuffer.lines.get(this._activeBuffer.ybase + this._activeBuffer.y)!.isWrapped = false;
this._unwrapBufferRow(this._activeBuffer.ybase + this._activeBuffer.y, false);
this._activeBuffer.y--;
this._activeBuffer.x = this._bufferService.cols - 1;
// find last taken cell - last cell can have 3 different states:
@@ -1183,21 +1185,43 @@ export class InputHandler extends Disposable implements IInputHandler {
respectProtect
);
if (clearWrap) {
line.isWrapped = false;
this._unwrapBufferRow(this._activeBuffer.ybase + y, true);
}
}
private _unwrapBufferRow(y: number, preserveContent: boolean): void {
const line = this._activeBuffer.lines.get(y);
if (!line?.isWrapped) {
return;
}
if (line instanceof BufferOverflowLine) {
if (preserveContent) {
this._activeBuffer.lines.set(y, line.materializeToStandalone(this._activeBuffer.getNullCell(this._eraseAttrData())));
} else {
this._activeBuffer.clearWrappedRow(y, this._activeBuffer.getNullCell(this._eraseAttrData()), false);
}
return;
}
line.isWrapped = false;
}
/**
* Helper method to reset cells in a terminal row. The cell gets replaced with the eraseChar of
* the terminal and the isWrapped property is set to false.
* @param y row index
*/
private _resetBufferLine(y: number, respectProtect: boolean = false): void {
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y);
const absY = this._activeBuffer.ybase + y;
const nullCell = this._activeBuffer.getNullCell(this._eraseAttrData());
const last = getWrappedRangeEnd(this._activeBuffer.lines, absY);
for (let i = last; i > absY; i--) {
this._activeBuffer.clearWrappedRow(i, nullCell);
}
const line = this._activeBuffer.lines.get(absY);
if (line) {
line.fill(this._activeBuffer.getNullCell(this._eraseAttrData()), respectProtect);
this._bufferService.buffer.clearMarkers(this._activeBuffer.ybase + y);
line.isWrapped = false;
line.fill(nullCell, respectProtect);
this._bufferService.buffer.clearMarkers(absY);
this._activeBuffer.clearWrappedRow(absY, nullCell);
}
}
@@ -1245,10 +1269,7 @@ export class InputHandler extends Disposable implements IInputHandler {
this._eraseInBufferLine(j, 0, this._activeBuffer.x + 1, true, respectProtect);
if (this._activeBuffer.x + 1 >= this._bufferService.cols) {
// Deleted entire previous line. This next line can no longer be wrapped.
const nextLine = this._activeBuffer.lines.get(j + 1);
if (nextLine) {
nextLine.isWrapped = false;
}
this._unwrapBufferRow(this._activeBuffer.ybase + j + 1, true);
}
while (j--) {
this._resetBufferLine(j, respectProtect);
@@ -1512,7 +1533,7 @@ export class InputHandler extends Disposable implements IInputHandler {
for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
line.deleteCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()));
line.isWrapped = false;
this._unwrapBufferRow(this._activeBuffer.ybase + y, true);
}
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
@@ -1545,7 +1566,7 @@ export class InputHandler extends Disposable implements IInputHandler {
for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
line.insertCells(0, param, this._activeBuffer.getNullCell(this._eraseAttrData()));
line.isWrapped = false;
this._unwrapBufferRow(this._activeBuffer.ybase + y, true);
}
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
@@ -1568,7 +1589,7 @@ export class InputHandler extends Disposable implements IInputHandler {
for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
line.insertCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()));
line.isWrapped = false;
this._unwrapBufferRow(this._activeBuffer.ybase + y, true);
}
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
@@ -1591,7 +1612,7 @@ export class InputHandler extends Disposable implements IInputHandler {
for (let y = this._activeBuffer.scrollTop; y <= this._activeBuffer.scrollBottom; ++y) {
const line = this._activeBuffer.lines.get(this._activeBuffer.ybase + y)!;
line.deleteCells(this._activeBuffer.x, param, this._activeBuffer.getNullCell(this._eraseAttrData()));
line.isWrapped = false;
this._unwrapBufferRow(this._activeBuffer.ybase + y, true);
}
this._dirtyRowTracker.markRangeDirty(this._activeBuffer.scrollTop, this._activeBuffer.scrollBottom);
return true;
@@ -3475,7 +3496,7 @@ export class InputHandler extends Disposable implements IInputHandler {
const line = this._activeBuffer.lines.get(row);
if (line) {
line.fill(cell);
line.isWrapped = false;
this._unwrapBufferRow(row, true);
}
}
this._dirtyRowTracker.markAllDirty();
+14 -1
View File
@@ -9,6 +9,7 @@ import { IdleTaskQueue } from 'common/TaskQueue';
import { IAttributeData, IBufferLine, ICellData, ICharset } from 'common/Types';
import { ExtendedAttrs } from 'common/buffer/AttributeData';
import { BufferLine, DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine';
import { attachWrappedRow, clearWrappedRow, createScrollWrappedLine } from 'common/buffer/BufferLineUtils';
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
import { getWrappedLineTrimmedLength, reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths } from 'common/buffer/BufferReflow';
import { CellData } from 'common/buffer/CellData';
@@ -100,7 +101,19 @@ export class Buffer extends Disposable implements IBuffer {
}
public getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine {
return new BufferLine(this._stringCache, this._bufferService.cols, this.getNullCell(attr), isWrapped);
return new BufferLine(this._stringCache, this._bufferService.cols, this.getNullCell(attr), !!isWrapped);
}
public attachWrappedRow(y: number, fillCellData: ICellData): IBufferLine {
return attachWrappedRow(this.lines, y, this._stringCache, this._cols, fillCellData);
}
public clearWrappedRow(y: number, fillCellData: ICellData, preserveContent: boolean = true): void {
clearWrappedRow(this.lines, y, fillCellData, this._stringCache, this._cols, preserveContent);
}
public createScrollWrappedLine(lineAbove: IBufferLine | undefined, attr: IAttributeData): IBufferLine {
return createScrollWrappedLine(lineAbove, this._stringCache, this._cols, this.getNullCell(attr), attr);
}
public get hasScrollback(): boolean {
+276 -75
View File
@@ -8,6 +8,7 @@ import { AttributeData } from 'common/buffer/AttributeData';
import { CellData } from 'common/buffer/CellData';
import { Attributes, BgFlags, CHAR_DATA_ATTR_INDEX, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, Content, NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH, WHITESPACE_CELL_CHAR } from 'common/buffer/Constants';
import { stringFromCodePoint } from 'common/input/TextDecoder';
import { StringBuilder } from 'common/StringBuilder';
// Buffer memory layout:
//
@@ -41,6 +42,7 @@ export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
// Work variables to avoid garbage collection
let $startIndex = 0;
const $workCell = new CellData();
const $translateToStringBuilder = new StringBuilder();
export interface IBufferLineStringCacheEntry {
value: string | undefined;
@@ -74,20 +76,106 @@ export class BufferLine implements IBufferLine {
protected _combined: {[index: number]: string} = {};
protected _extendedAttrs: {[index: number]: IExtendedAttrs | undefined} = {};
protected _stringCacheEntryRef: WeakRef<IBufferLineStringCacheEntry> | undefined;
/** When set, this row is a viewport slice of the logical line owned by this buffer line. */
protected _logicalHead: BufferLine | undefined;
protected _segmentStart: number = 0;
protected _logicalCellCount: number = 0;
protected _overflowRowCount: number = 0;
protected _fillCellData: ICellData;
public length: number;
public isWrapped: boolean = false;
constructor(
protected readonly _stringCache: IBufferLineStringCache,
cols: number,
fillCellData?: ICellData,
public isWrapped: boolean = false
isWrapped: boolean = false,
logicalHead?: BufferLine,
segmentStart?: number
) {
this._data = new Uint32Array(cols * Constants.CELL_INDICIES);
const cell = fillCellData ?? CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
this._fillCellData = cell;
if (logicalHead) {
this._logicalHead = logicalHead;
this._segmentStart = segmentStart ?? 0;
this._data = logicalHead._data;
this._combined = logicalHead._combined;
this._extendedAttrs = logicalHead._extendedAttrs;
this.length = cols;
this.isWrapped = true;
return;
}
this._data = new Uint32Array(cols * Constants.CELL_INDICIES);
this._logicalCellCount = cols;
for (let i = 0; i < cols; ++i) {
this.setCell(i, cell);
}
this.length = cols;
this.isWrapped = isWrapped;
}
public get logicalCellCount(): number {
return this._logicalHead ? this._logicalHead._logicalCellCount : this._logicalCellCount;
}
public registerOverflowRow(): void {
if (this._logicalHead) {
this._logicalHead.registerOverflowRow();
} else {
this._overflowRowCount++;
}
}
public unregisterOverflowRow(): void {
if (this._logicalHead) {
this._logicalHead.unregisterOverflowRow();
} else if (this._overflowRowCount > 0) {
this._overflowRowCount--;
}
}
public clearOverflowRows(): void {
if (!this._logicalHead) {
this._overflowRowCount = 0;
}
}
/** After reflow merges wrapped rows into the head, reset overflow tracking. */
public clearReflowMergedState(): void {
if (!this._logicalHead) {
this._overflowRowCount = 0;
}
}
public ensureLogicalCapacity(minCells: number, fillCellData: ICellData): void {
const head = this._logicalHead ?? this;
if (minCells <= head._logicalCellCount) {
return;
}
head._invalidateStringCache();
const uint32Cells = minCells * Constants.CELL_INDICIES;
if (head._data.buffer.byteLength >= uint32Cells * 4) {
head._data = new Uint32Array(head._data.buffer, 0, uint32Cells);
} else {
const data = new Uint32Array(uint32Cells);
data.set(head._data);
head._data = data;
}
for (let i = head._logicalCellCount; i < minCells; ++i) {
const start = i * Constants.CELL_INDICIES;
head._data[start + Cell.CONTENT] = fillCellData.content;
head._data[start + Cell.FG] = fillCellData.fg;
head._data[start + Cell.BG] = fillCellData.bg;
}
head._logicalCellCount = minCells;
}
protected _absoluteColumn(column: number): number {
return this._segmentStart + column;
}
protected _cellStartIndex(column: number): number {
return this._absoluteColumn(column) * Constants.CELL_INDICIES;
}
/**
@@ -95,16 +183,18 @@ export class BufferLine implements IBufferLine {
* @deprecated
*/
public get(index: number): CharData {
const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
const abs = this._absoluteColumn(index);
const start = this._cellStartIndex(index);
const content = this._data[start + Cell.CONTENT];
const cp = content & Content.CODEPOINT_MASK;
return [
this._data[index * Constants.CELL_INDICIES + Cell.FG],
this._data[start + Cell.FG],
(content & Content.IS_COMBINED_MASK)
? this._combined[index]
? this._combined[abs]
: (cp) ? stringFromCodePoint(cp) : '',
content >> Content.WIDTH_SHIFT,
(content & Content.IS_COMBINED_MASK)
? this._combined[index].charCodeAt(this._combined[index].length - 1)
? this._combined[abs].charCodeAt(this._combined[abs].length - 1)
: cp
];
}
@@ -115,12 +205,14 @@ export class BufferLine implements IBufferLine {
*/
public set(index: number, value: CharData): void {
this._invalidateStringCache();
this._data[index * Constants.CELL_INDICIES + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
const abs = this._absoluteColumn(index);
const start = this._cellStartIndex(index);
this._data[start + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
if (value[CHAR_DATA_CHAR_INDEX].length > 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._combined[abs] = value[1];
this._data[start + Cell.CONTENT] = abs | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
} else {
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
this._data[start + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
}
}
@@ -129,22 +221,22 @@ export class BufferLine implements IBufferLine {
* use these when only one value is needed, otherwise use `loadCell`
*/
public getWidth(index: number): number {
return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT;
return this._data[this._cellStartIndex(index) + Cell.CONTENT] >> Content.WIDTH_SHIFT;
}
/** Test whether content has width. */
public hasWidth(index: number): number {
return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.WIDTH_MASK;
return this._data[this._cellStartIndex(index) + Cell.CONTENT] & Content.WIDTH_MASK;
}
/** Get FG cell component. */
public getFg(index: number): number {
return this._data[index * Constants.CELL_INDICIES + Cell.FG];
return this._data[this._cellStartIndex(index) + Cell.FG];
}
/** Get BG cell component. */
public getBg(index: number): number {
return this._data[index * Constants.CELL_INDICIES + Cell.BG];
return this._data[this._cellStartIndex(index) + Cell.BG];
}
/**
@@ -153,7 +245,7 @@ export class BufferLine implements IBufferLine {
* from real empty cells.
*/
public hasContent(index: number): number {
return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK;
return this._data[this._cellStartIndex(index) + Cell.CONTENT] & Content.HAS_CONTENT_MASK;
}
/**
@@ -162,23 +254,25 @@ export class BufferLine implements IBufferLine {
* a single UTF32 codepoint or the last codepoint of a combined string.
*/
public getCodePoint(index: number): number {
const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
const abs = this._absoluteColumn(index);
const content = this._data[this._cellStartIndex(index) + Cell.CONTENT];
if (content & Content.IS_COMBINED_MASK) {
return this._combined[index].charCodeAt(this._combined[index].length - 1);
return this._combined[abs].charCodeAt(this._combined[abs].length - 1);
}
return content & Content.CODEPOINT_MASK;
}
/** Test whether the cell contains a combined string. */
public isCombined(index: number): number {
return this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] & Content.IS_COMBINED_MASK;
return this._data[this._cellStartIndex(index) + Cell.CONTENT] & Content.IS_COMBINED_MASK;
}
/** Returns the string content of the cell. */
public getString(index: number): string {
const content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
const abs = this._absoluteColumn(index);
const content = this._data[this._cellStartIndex(index) + Cell.CONTENT];
if (content & Content.IS_COMBINED_MASK) {
return this._combined[index];
return this._combined[abs];
}
if (content & Content.CODEPOINT_MASK) {
return stringFromCodePoint(content & Content.CODEPOINT_MASK);
@@ -189,7 +283,7 @@ export class BufferLine implements IBufferLine {
/** Get state of protected flag. */
public isProtected(index: number): number {
return this._data[index * Constants.CELL_INDICIES + Cell.BG] & BgFlags.PROTECTED;
return this._data[this._cellStartIndex(index) + Cell.BG] & BgFlags.PROTECTED;
}
/**
@@ -197,15 +291,16 @@ export class BufferLine implements IBufferLine {
* to GC as it significantly reduced the amount of new objects/references needed.
*/
public loadCell(index: number, cell: ICellData): ICellData {
$startIndex = index * Constants.CELL_INDICIES;
const abs = this._absoluteColumn(index);
$startIndex = this._cellStartIndex(index);
cell.content = this._data[$startIndex + Cell.CONTENT];
cell.fg = this._data[$startIndex + Cell.FG];
cell.bg = this._data[$startIndex + Cell.BG];
if (cell.content & Content.IS_COMBINED_MASK) {
cell.combinedData = this._combined[index];
cell.combinedData = this._combined[abs];
}
if (cell.bg & BgFlags.HAS_EXTENDED) {
cell.extended = this._extendedAttrs[index]!;
cell.extended = this._extendedAttrs[abs]!;
}
return cell;
}
@@ -215,15 +310,17 @@ export class BufferLine implements IBufferLine {
*/
public setCell(index: number, cell: ICellData): void {
this._invalidateStringCache();
const abs = this._absoluteColumn(index);
const start = this._cellStartIndex(index);
if (cell.content & Content.IS_COMBINED_MASK) {
this._combined[index] = cell.combinedData;
this._combined[abs] = cell.combinedData;
}
if (cell.bg & BgFlags.HAS_EXTENDED) {
this._extendedAttrs[index] = cell.extended;
this._extendedAttrs[abs] = cell.extended;
}
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.BG] = cell.bg;
this._data[start + Cell.CONTENT] = cell.content;
this._data[start + Cell.FG] = cell.fg;
this._data[start + Cell.BG] = cell.bg;
}
/**
@@ -233,12 +330,14 @@ export class BufferLine implements IBufferLine {
*/
public setCellFromCodepoint(index: number, codePoint: number, width: number, attrs: IAttributeData): void {
this._invalidateStringCache();
const abs = this._absoluteColumn(index);
const start = this._cellStartIndex(index);
if (attrs.bg & BgFlags.HAS_EXTENDED) {
this._extendedAttrs[index] = attrs.extended;
this._extendedAttrs[abs] = attrs.extended;
}
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.BG] = attrs.bg;
this._data[start + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);
this._data[start + Cell.FG] = attrs.fg;
this._data[start + Cell.BG] = attrs.bg;
}
/**
@@ -249,16 +348,18 @@ export class BufferLine implements IBufferLine {
*/
public addCodepointToCell(index: number, codePoint: number, width: number): void {
this._invalidateStringCache();
let content = this._data[index * Constants.CELL_INDICIES + Cell.CONTENT];
const abs = this._absoluteColumn(index);
const start = this._cellStartIndex(index);
let content = this._data[start + Cell.CONTENT];
if (content & Content.IS_COMBINED_MASK) {
// we already have a combined string, simply add
this._combined[index] += stringFromCodePoint(codePoint);
this._combined[abs] += stringFromCodePoint(codePoint);
} else {
if (content & Content.CODEPOINT_MASK) {
// normal case for combining chars:
// - move current leading char + new one into combined string
// - set combined flag
this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint);
this._combined[abs] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint);
content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0
content |= Content.IS_COMBINED_MASK;
} else {
@@ -271,7 +372,7 @@ export class BufferLine implements IBufferLine {
content &= ~Content.WIDTH_MASK;
content |= width << Content.WIDTH_SHIFT;
}
this._data[index * Constants.CELL_INDICIES + Cell.CONTENT] = content;
this._data[start + Cell.CONTENT] = content;
}
public insertCells(pos: number, n: number, fillCellData: ICellData): void {
@@ -371,20 +472,28 @@ export class BufferLine implements IBufferLine {
*/
public resize(cols: number, fillCellData: ICellData): boolean {
this._invalidateStringCache();
if (this._logicalHead) {
if (cols > this.length) {
this._logicalHead.ensureLogicalCapacity(this._segmentStart + cols, fillCellData);
}
this.length = cols;
return false;
}
if (this._overflowRowCount > 0) {
if (cols > this.length) {
this.ensureLogicalCapacity(Math.max(this._logicalCellCount, this._segmentStart + cols), fillCellData);
}
this.length = cols;
return this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;
}
if (cols === this.length) {
return this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;
}
const uint32Cells = cols * Constants.CELL_INDICIES;
if (cols > this.length) {
if (this._data.buffer.byteLength >= uint32Cells * 4) {
// optimization: avoid alloc and data copy if buffer has enough room
this._data = new Uint32Array(this._data.buffer, 0, uint32Cells);
} else {
// slow path: new alloc and full data copy
const data = new Uint32Array(uint32Cells);
data.set(this._data);
this._data = data;
}
const data = new Uint32Array(uint32Cells);
data.set(this._data.subarray(0, this.length * Constants.CELL_INDICIES));
this._data = data;
for (let i = this.length; i < cols; ++i) {
this.setCell(i, fillCellData);
}
@@ -409,6 +518,7 @@ export class BufferLine implements IBufferLine {
}
}
this.length = cols;
this._logicalCellCount = cols;
return uint32Cells * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength;
}
@@ -419,10 +529,12 @@ export class BufferLine implements IBufferLine {
* Returns 0 or 1 indicating whether a cleanup happened.
*/
public cleanupMemory(): number {
if (this._data.length * 4 * Constants.CLEANUP_THRESHOLD < this._data.buffer.byteLength) {
const data = new Uint32Array(this._data.length);
data.set(this._data);
this._data = data;
const head = this._logicalHead ?? this;
const usedCells = head._logicalCellCount * Constants.CELL_INDICIES;
if (usedCells * 4 * Constants.CLEANUP_THRESHOLD < head._data.buffer.byteLength) {
const data = new Uint32Array(usedCells);
data.set(head._data.subarray(0, usedCells));
head._data = data;
return 1;
}
return 0;
@@ -440,8 +552,10 @@ export class BufferLine implements IBufferLine {
}
return;
}
this._combined = {};
this._extendedAttrs = {};
if (!this._logicalHead) {
this._combined = {};
this._extendedAttrs = {};
}
for (let i = 0; i < this.length; ++i) {
this.setCell(i, fillCellData);
}
@@ -449,30 +563,72 @@ export class BufferLine implements IBufferLine {
/** alter to a full copy of line */
public copyFrom(line: BufferLine): void {
if (this._logicalHead) {
this.materializeFromLogicalHead(this._fillCellData);
}
this._invalidateStringCache();
this._logicalHead = undefined;
this._segmentStart = 0;
this._overflowRowCount = 0;
if (line._logicalHead) {
const standalone = new BufferLine(this._stringCache, line.length, this._fillCellData, false);
standalone.copyCellsFrom(line, 0, 0, line.length, false);
line = standalone;
}
if (this.length !== line.length) {
this._data = new Uint32Array(line._data);
this._data = new Uint32Array(line._data.subarray(0, line.length * Constants.CELL_INDICIES));
} else {
// use high speed copy if lengths are equal
this._data.set(line._data);
this._data.set(line._data.subarray(0, line.length * Constants.CELL_INDICIES));
}
this.length = line.length;
this._logicalCellCount = line._logicalCellCount || line.length;
this._combined = {};
for (const el in line._combined) {
this._combined[el] = line._combined[el];
const key = parseInt(el, 10);
if (key < this._logicalCellCount) {
this._combined[key] = line._combined[el];
}
}
this._extendedAttrs = {};
for (const el in line._extendedAttrs) {
this._extendedAttrs[el] = line._extendedAttrs[el];
const key = parseInt(el, 10);
if (key < this._logicalCellCount) {
this._extendedAttrs[key] = line._extendedAttrs[el];
}
}
this.isWrapped = line.isWrapped;
}
public materializeFromLogicalHead(fillCellData: ICellData): BufferLine {
if (!this._logicalHead) {
return this;
}
const standalone = new BufferLine(this._stringCache, this.length, fillCellData, false);
standalone.copyCellsFrom(this._logicalHead, this._segmentStart, 0, this.length, false);
this._logicalHead.unregisterOverflowRow();
this._data = standalone._data;
this._combined = standalone._combined;
this._extendedAttrs = standalone._extendedAttrs;
this._logicalHead = undefined;
this._segmentStart = 0;
this._logicalCellCount = this.length;
this.isWrapped = false;
return this;
}
/** create a new clone */
public clone(): IBufferLine {
if (this._logicalHead) {
const newLine = new BufferLine(this._stringCache, 0, undefined, true, this._logicalHead, this._segmentStart);
newLine.length = this.length;
return newLine;
}
const newLine = new BufferLine(this._stringCache, 0, undefined, false);
newLine._data = new Uint32Array(this._data);
newLine._data = new Uint32Array(this._data.subarray(0, this._logicalCellCount * Constants.CELL_INDICIES));
newLine.length = this.length;
newLine._logicalCellCount = this._logicalCellCount;
newLine._overflowRowCount = this._overflowRowCount;
for (const el in this._combined) {
newLine._combined[el] = this._combined[el];
}
@@ -485,8 +641,34 @@ export class BufferLine implements IBufferLine {
public getTrimmedLength(): number {
for (let i = this.length - 1; i >= 0; --i) {
if ((this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {
return i + (this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT);
const start = this._cellStartIndex(i);
if ((this._data[start + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {
return i + (this._data[start + Cell.CONTENT] >> Content.WIDTH_SHIFT);
}
}
return 0;
}
/** Creates a standalone line containing the full logical line content. */
public toStandaloneLogicalLine(cols: number, fillCellData: ICellData, stringCache: IBufferLineStringCache): BufferLine {
const head = this._logicalHead ?? this;
const trimmed = head.getLogicalTrimmedLength();
const flat = new BufferLine(stringCache, cols, fillCellData, false);
if (trimmed > 0) {
flat.copyCellsFrom(head, 0, 0, trimmed, false);
flat._logicalCellCount = trimmed;
}
head.clearReflowMergedState();
return flat;
}
/** Trimmed length across the entire logical line (all segments). */
public getLogicalTrimmedLength(): number {
const head = this._logicalHead ?? this;
for (let i = head._logicalCellCount - 1; i >= 0; --i) {
const start = i * Constants.CELL_INDICIES;
if ((head._data[start + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {
return i + (head._data[start + Cell.CONTENT] >> Content.WIDTH_SHIFT);
}
}
return 0;
@@ -494,8 +676,9 @@ export class BufferLine implements IBufferLine {
public getNoBgTrimmedLength(): number {
for (let i = this.length - 1; i >= 0; --i) {
if ((this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this._data[i * Constants.CELL_INDICIES + Cell.BG] & Attributes.CM_MASK)) {
return i + (this._data[i * Constants.CELL_INDICIES + Cell.CONTENT] >> Content.WIDTH_SHIFT);
const start = this._cellStartIndex(i);
if ((this._data[start + Cell.CONTENT] & Content.HAS_CONTENT_MASK) || (this._data[start + Cell.BG] & Attributes.CM_MASK)) {
return i + (this._data[start + Cell.CONTENT] >> Content.WIDTH_SHIFT);
}
}
return 0;
@@ -504,22 +687,32 @@ export class BufferLine implements IBufferLine {
public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {
this._invalidateStringCache();
const srcData = src._data;
const srcOffset = src._segmentStart;
const destOffset = this._segmentStart;
if (applyInReverse) {
for (let cell = length - 1; cell >= 0; cell--) {
const srcStart = (srcOffset + srcCol + cell) * Constants.CELL_INDICIES;
const destStart = (destOffset + destCol + cell) * Constants.CELL_INDICIES;
for (let i = 0; i < Constants.CELL_INDICIES; i++) {
this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];
this._data[destStart + i] = srcData[srcStart + i];
}
if (srcData[(srcCol + cell) * Constants.CELL_INDICIES + Cell.BG] & BgFlags.HAS_EXTENDED) {
this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell];
if (srcData[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) {
const absSrc = srcOffset + srcCol + cell;
const absDest = destOffset + destCol + cell;
this._extendedAttrs[absDest] = src._extendedAttrs[absSrc];
}
}
} else {
for (let cell = 0; cell < length; cell++) {
const srcStart = (srcOffset + srcCol + cell) * Constants.CELL_INDICIES;
const destStart = (destOffset + destCol + cell) * Constants.CELL_INDICIES;
for (let i = 0; i < Constants.CELL_INDICIES; i++) {
this._data[(destCol + cell) * Constants.CELL_INDICIES + i] = srcData[(srcCol + cell) * Constants.CELL_INDICIES + i];
this._data[destStart + i] = srcData[srcStart + i];
}
if (srcData[(srcCol + cell) * Constants.CELL_INDICIES + Cell.BG] & BgFlags.HAS_EXTENDED) {
this._extendedAttrs[destCol + cell] = src._extendedAttrs[srcCol + cell];
if (srcData[srcStart + Cell.BG] & BgFlags.HAS_EXTENDED) {
const absSrc = srcOffset + srcCol + cell;
const absDest = destOffset + destCol + cell;
this._extendedAttrs[absDest] = src._extendedAttrs[absSrc];
}
}
}
@@ -528,8 +721,9 @@ export class BufferLine implements IBufferLine {
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];
const srcAbsCol = srcOffset + srcCol;
if (key >= srcAbsCol && key < srcAbsCol + length) {
this._combined[key - srcCol + destOffset + destCol] = src._combined[key];
}
}
}
@@ -570,12 +764,16 @@ export class BufferLine implements IBufferLine {
if (outColumns) {
outColumns.length = 0;
}
let result = '';
$translateToStringBuilder.reset();
while (startCol < endCol) {
const content = this._data[startCol * Constants.CELL_INDICIES + Cell.CONTENT];
const abs = this._absoluteColumn(startCol);
const start = this._cellStartIndex(startCol);
const content = this._data[start + Cell.CONTENT];
const cp = content & Content.CODEPOINT_MASK;
const chars = (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;
result += chars;
const chars = (content & Content.IS_COMBINED_MASK)
? (this._combined[abs] ?? '')
: (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;
$translateToStringBuilder.append(chars);
if (outColumns) {
for (let i = 0; i < chars.length; ++i) {
outColumns.push(startCol);
@@ -586,6 +784,8 @@ export class BufferLine implements IBufferLine {
if (outColumns) {
outColumns.push(startCol);
}
const result = $translateToStringBuilder.toString();
$translateToStringBuilder.reset();
if (isCanonicalRequest) {
const cacheEntry = this._getStringCacheEntry(true)!;
cacheEntry.value = result;
@@ -609,8 +809,9 @@ export class BufferLine implements IBufferLine {
return cacheEntry;
}
private _invalidateStringCache(): void {
const cacheEntry = this._getStringCacheEntry(false);
protected _invalidateStringCache(): void {
const head = this._logicalHead ?? this;
const cacheEntry = head._getStringCacheEntry(false);
if (cacheEntry) {
cacheEntry.value = undefined;
cacheEntry.isTrimmed = false;
+171
View File
@@ -0,0 +1,171 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { BufferLine } from 'common/buffer/BufferLine';
import { BufferOverflowLine } from 'common/buffer/BufferOverflowLine';
import { IAttributeData, IBufferLine, ICellData } from 'common/Types';
import { IBufferLineStringCache } from 'common/buffer/BufferLine';
export function getLogicalHead(line: IBufferLine): BufferLine {
if (line instanceof BufferOverflowLine) {
return line.head;
}
return line as BufferLine;
}
export function isBufferOverflowLine(line: IBufferLine): line is BufferOverflowLine {
return line instanceof BufferOverflowLine;
}
/**
* Returns the logical cell offset for the start of a buffer row.
*/
export function getSegmentStart(line: IBufferLine): number {
if (line instanceof BufferOverflowLine) {
return line.segmentStart;
}
return 0;
}
/**
* Marks the row at `y` as a wrapped continuation of the logical line above.
* Extends the logical line storage and replaces the row with a {@link BufferOverflowLine}.
*/
export function attachWrappedRow(
lines: { get(index: number): IBufferLine | undefined; set(index: number, line: IBufferLine): void },
y: number,
stringCache: IBufferLineStringCache,
cols: number,
fillCellData: ICellData
): IBufferLine {
const headY = y > 0 ? getWrappedRangeStart(lines, y - 1) : y;
const head = getLogicalHead(lines.get(headY)!);
const segmentStart = head.logicalCellCount;
head.ensureLogicalCapacity(segmentStart + cols, fillCellData);
const existingLine = lines.get(y);
if (existingLine && !(existingLine instanceof BufferOverflowLine) && existingLine.getTrimmedLength() > 0) {
head.copyCellsFrom(existingLine as BufferLine, 0, segmentStart, existingLine.length, false);
}
const overflowLine = new BufferOverflowLine(head, segmentStart, stringCache, cols, fillCellData);
lines.set(y, overflowLine);
head.registerOverflowRow();
return overflowLine;
}
/**
* Clears the wrapped state for a row, detaching overflow rows from their logical line.
*/
export function clearWrappedRow(
lines: { get(index: number): IBufferLine | undefined; set(index: number, line: IBufferLine): void },
y: number,
fillCellData: ICellData,
stringCache?: IBufferLineStringCache,
cols?: number,
preserveContent: boolean = true
): void {
const line = lines.get(y);
if (!line) {
return;
}
if (line.isWrapped) {
if (line instanceof BufferOverflowLine) {
if (preserveContent) {
lines.set(y, line.materializeToStandalone(fillCellData));
} else if (stringCache !== undefined && cols !== undefined) {
line.head.unregisterOverflowRow();
lines.set(y, new BufferLine(stringCache, cols, fillCellData, false));
} else {
lines.set(y, line.materializeToStandalone(fillCellData));
}
} else if (line instanceof BufferLine) {
(line as BufferLine).materializeFromLogicalHead(fillCellData);
}
return;
}
const head = line as BufferLine;
head.clearOverflowRows();
}
export function getWrappedRangeStart(lines: { get(index: number): IBufferLine | undefined }, y: number): number {
let first = y;
while (first > 0 && lines.get(first)?.isWrapped) {
first--;
}
return first;
}
export function getWrappedRangeEnd(lines: { get(index: number): IBufferLine | undefined; length: number }, y: number): number {
let last = y;
while (last + 1 < lines.length && lines.get(last + 1)?.isWrapped) {
last++;
}
return last;
}
/**
* Copies a logical line group into a single standalone buffer row and removes overflow rows.
* Used before reflow so existing reflow algorithms can operate on independent lines.
*/
export function flattenLogicalGroup(
lines: { get(index: number): IBufferLine | undefined; set(index: number, line: IBufferLine): void; length: number },
headY: number,
cols: number,
fillCellData: ICellData,
stringCache: IBufferLineStringCache
): void {
const last = getWrappedRangeEnd(lines, headY);
if (last <= headY) {
return;
}
const head = getLogicalHead(lines.get(headY)!);
lines.set(headY, head.toStandaloneLogicalLine(cols, fillCellData, stringCache));
for (let i = headY + 1; i <= last; i++) {
const line = lines.get(i);
if (line instanceof BufferOverflowLine) {
const standalone = line.materializeToStandalone(fillCellData);
standalone.isWrapped = true;
lines.set(i, standalone);
}
}
}
export function materializeLogicalLinesForReflow(
lines: { get(index: number): IBufferLine | undefined; set(index: number, line: IBufferLine): void; length: number },
cols: number,
fillCellData: ICellData,
stringCache: IBufferLineStringCache
): void {
let y = 0;
while (y < lines.length) {
const line = lines.get(y);
const next = lines.get(y + 1);
if (line && next instanceof BufferOverflowLine && next.head === getLogicalHead(line)) {
flattenLogicalGroup(lines, y, cols, fillCellData, stringCache);
y++;
continue;
}
y++;
}
}
export function createScrollWrappedLine(
lineAbove: IBufferLine | undefined,
stringCache: IBufferLineStringCache,
cols: number,
fillCellData: ICellData,
attr: IAttributeData
): IBufferLine {
if (!lineAbove) {
return new BufferLine(stringCache, cols, fillCellData, true);
}
const head = getLogicalHead(lineAbove);
const segmentStart = head.logicalCellCount;
head.ensureLogicalCapacity(segmentStart + cols, fillCellData);
const overflowLine = new BufferOverflowLine(head, segmentStart, stringCache, cols, fillCellData);
head.registerOverflowRow();
return overflowLine;
}
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { BufferLine } from 'common/buffer/BufferLine';
import { BufferLineStringCache } from 'common/buffer/BufferLineStringCache';
import { attachWrappedRow } from 'common/buffer/BufferLineUtils';
import { BufferOverflowLine } from 'common/buffer/BufferOverflowLine';
import { CellData } from 'common/buffer/CellData';
import { NULL_CELL_CHAR, NULL_CELL_CODE, NULL_CELL_WIDTH } from 'common/buffer/Constants';
import { IBufferLine } from 'common/Types';
const TEST_STRING_CACHE = new BufferLineStringCache();
const NULL_CELL = CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
class LineList {
private _lines: IBufferLine[] = [];
public get(index: number): IBufferLine | undefined { return this._lines[index]; }
public set(index: number, line: IBufferLine): void { this._lines[index] = line; }
public get length(): number { return this._lines.length; }
}
describe('BufferLogicalLine', () => {
it('should share storage across wrapped rows', () => {
const cols = 5;
const lines = new LineList();
const head = new BufferLine(TEST_STRING_CACHE, cols, NULL_CELL, false);
lines.set(0, head);
for (let i = 0; i < cols; i++) {
head.setCellFromCodepoint(i, 'a'.charCodeAt(0) + i, 1, NULL_CELL);
}
attachWrappedRow(lines, 1, TEST_STRING_CACHE, cols, NULL_CELL);
const overflow = lines.get(1)!;
assert.instanceOf(overflow, BufferOverflowLine);
for (let i = 0; i < cols; i++) {
(overflow as BufferLine).setCellFromCodepoint(i, 'f'.charCodeAt(0) + i, 1, NULL_CELL);
}
assert.equal(head.translateToString(true), 'abcde');
assert.equal(overflow.translateToString(true), 'fghij');
assert.equal(head.getLogicalTrimmedLength(), 10);
});
it('should reflow unwrap without copying when logical content fits', () => {
const lines = new LineList();
const head = new BufferLine(TEST_STRING_CACHE, 5, NULL_CELL, false);
lines.set(0, head);
for (let i = 0; i < 5; i++) {
head.setCellFromCodepoint(i, 'f'.charCodeAt(0) + i, 1, NULL_CELL);
}
attachWrappedRow(lines, 1, TEST_STRING_CACHE, 2, NULL_CELL);
const overflow = lines.get(1)! as BufferOverflowLine;
for (let i = 0; i < 2; i++) {
overflow.setCellFromCodepoint(i, 'k'.charCodeAt(0) + i, 1, NULL_CELL);
}
assert.equal(head.getLogicalTrimmedLength(), 7);
assert.equal(overflow.translateToString(true), 'kl');
});
});
+34
View File
@@ -0,0 +1,34 @@
/**
* Copyright (c) 2026 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICellData } from 'common/Types';
import { BufferLine, IBufferLineStringCache } from 'common/buffer/BufferLine';
/**
* A viewport row that is a view into a slice of a parent {@link BufferLine}'s logical storage.
*/
export class BufferOverflowLine extends BufferLine {
public readonly head: BufferLine;
public readonly segmentStart: number;
constructor(
head: BufferLine,
segmentStart: number,
stringCache: IBufferLineStringCache,
cols: number,
fillCellData: ICellData
) {
super(stringCache, cols, fillCellData, true, head, segmentStart);
this.head = head;
this.segmentStart = segmentStart;
}
/**
* Copies this row's segment into a standalone {@link BufferLine}.
*/
public materializeToStandalone(fillCellData: ICellData): BufferLine {
return this.materializeFromLogicalHead(fillCellData);
}
}
+4 -1
View File
@@ -178,7 +178,10 @@ export function reflowLargerApplyNewLayout(lines: CircularList<IBufferLine>, new
*/
export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] {
const newLineLengths: number[] = [];
const cellsNeeded = wrappedLines.map((l, i) => getWrappedLineTrimmedLength(wrappedLines, i, oldCols)).reduce((p, c) => p + c);
let cellsNeeded = 0;
for (let i = 0; i < wrappedLines.length; i++) {
cellsNeeded += getWrappedLineTrimmedLength(wrappedLines, i, oldCols);
}
// Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and
// linesNeeded
+3
View File
@@ -34,6 +34,9 @@ export interface IBuffer {
nextStop(x?: number): number;
prevStop(x?: number): number;
getBlankLine(attr: IAttributeData, isWrapped?: boolean): IBufferLine;
attachWrappedRow(y: number, fillCellData: ICellData): IBufferLine;
clearWrappedRow(y: number, fillCellData: ICellData, preserveContent?: boolean): void;
createScrollWrappedLine(lineAbove: IBufferLine | undefined, attr: IAttributeData): IBufferLine;
getNullCell(attr?: IAttributeData): ICellData;
getWhitespaceCell(attr?: IAttributeData): ICellData;
addMarker(y: number): IMarker;
+66
View File
@@ -140,5 +140,71 @@ describe('WriteBuffer', () => {
wb.flushSync();
assert.equal(parsed, 0);
});
it('dispose cancels scheduled innerWrite', done => {
wb.write('a');
wb.dispose();
setTimeout(() => {
assert.deepEqual(stack, []);
done();
}, 20);
});
it('dispose does not fire onWriteParsed for pending writes', done => {
let parsed = 0;
wb.onWriteParsed(() => parsed++);
wb.write('a');
wb.dispose();
setTimeout(() => {
assert.equal(parsed, 0);
done();
}, 20);
});
it('write after dispose is a no-op', done => {
wb.dispose();
wb.write('a');
setTimeout(() => {
assert.deepEqual(stack, []);
done();
}, 20);
});
it('dispose is idempotent', done => {
wb.write('a');
wb.dispose();
wb.dispose();
setTimeout(() => {
assert.deepEqual(stack, []);
done();
}, 20);
});
it('async handler continuation is skipped after dispose', done => {
let resolve!: (value: boolean) => void;
const pending = new Promise<boolean>(r => { resolve = r; });
wb = new WriteBuffer(() => pending);
wb.write('a');
wb.dispose();
resolve(true);
setTimeout(() => {
assert.deepEqual(stack, []);
done();
}, 20);
});
it('handleUserInput still processes first chunk synchronously', () => {
wb.handleUserInput();
wb.write('a');
assert.deepEqual(stack, ['a']);
});
it('flushSync after dispose is a no-op', done => {
wb.write('a');
wb.dispose();
wb.flushSync();
setTimeout(() => {
assert.deepEqual(stack, []);
done();
}, 20);
});
it('writeSync after dispose is a no-op', () => {
wb.dispose();
wb.writeSync('a');
assert.deepEqual(stack, []);
});
});
});
+40 -8
View File
@@ -4,11 +4,10 @@
* @license MIT
*/
import { Disposable } from 'common/Lifecycle';
import { TimeoutTimer } from 'common/Async';
import { Disposable, toDisposable } from 'common/Lifecycle';
import { Emitter } from 'common/Event';
declare const setTimeout: (handler: () => void, timeout?: number) => void;
const enum Constants {
/**
* Safety watermark to avoid memory exhaustion and browser engine crash on fast data input.
@@ -43,11 +42,18 @@ export class WriteBuffer extends Disposable {
private _syncCalls = 0;
private _didUserInput = false;
private readonly _innerWriteTimer = this._register(new TimeoutTimer());
private readonly _onWriteParsed = this._register(new Emitter<void>());
public readonly onWriteParsed = this._onWriteParsed.event;
constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise<boolean>) {
super();
this._register(toDisposable(() => {
this._writeBuffer.length = 0;
this._callbacks.length = 0;
this._pendingData = 0;
this._bufferOffset = 0;
}));
}
public handleUserInput(): void {
@@ -63,6 +69,9 @@ export class WriteBuffer extends Disposable {
* promises to resolve.
*/
public flushSync(): void {
if (this._store.isDisposed) {
return;
}
// exit early if another sync write loop is active
if (this._isSyncWriting) {
return;
@@ -95,6 +104,9 @@ export class WriteBuffer extends Disposable {
* @deprecated Unreliable, to be removed soon.
*/
public writeSync(data: string | Uint8Array, maxSubsequentCalls?: number): void {
if (this._store.isDisposed) {
return;
}
// stop writeSync recursions with maxSubsequentCalls argument
// This is dangerous to use as it will lose the current data chunk
// and return immediately.
@@ -138,6 +150,9 @@ export class WriteBuffer extends Disposable {
}
public write(data: string | Uint8Array, callback?: () => void): void {
if (this._store.isDisposed) {
return;
}
if (this._pendingData > Constants.DISCARD_WATERMARK) {
throw new Error('write data discarded, use flow control to avoid losing data');
}
@@ -158,7 +173,7 @@ export class WriteBuffer extends Disposable {
return;
}
setTimeout(() => this._innerWrite());
this._scheduleInnerWrite();
}
this._pendingData += data.length;
@@ -194,7 +209,17 @@ export class WriteBuffer extends Disposable {
*
* Note, for pure sync code `lastTime` and `promiseResult` have no meaning.
*/
private _scheduleInnerWrite(lastTime: number = 0, promiseResult: boolean = true): void {
if (this._store.isDisposed) {
return;
}
this._innerWriteTimer.cancelAndSet(() => this._innerWrite(lastTime, promiseResult), 0);
}
protected _innerWrite(lastTime: number = 0, promiseResult: boolean = true): void {
if (this._store.isDisposed) {
return;
}
const startTime = lastTime || performance.now();
while (this._writeBuffer.length > this._bufferOffset) {
const data = this._writeBuffer[this._bufferOffset];
@@ -223,9 +248,16 @@ export class WriteBuffer extends Disposable {
* responsibility to slice hard work), but we can at least schedule a screen update as we
* gain control.
*/
const continuation: (r: boolean) => void = (r: boolean) => performance.now() - startTime >= Constants.WRITE_TIMEOUT_MS
? setTimeout(() => this._innerWrite(0, r))
: this._innerWrite(startTime, r);
const continuation: (r: boolean) => void = (r: boolean) => {
if (this._store.isDisposed) {
return;
}
if (performance.now() - startTime >= Constants.WRITE_TIMEOUT_MS) {
this._scheduleInnerWrite(0, r);
} else {
this._innerWrite(startTime, r);
}
};
/**
* Optimization considerations:
@@ -272,7 +304,7 @@ export class WriteBuffer extends Disposable {
this._callbacks = this._callbacks.slice(this._bufferOffset);
this._bufferOffset = 0;
}
setTimeout(() => this._innerWrite());
this._scheduleInnerWrite();
} else {
this._writeBuffer.length = 0;
this._callbacks.length = 0;
+5 -5
View File
@@ -69,17 +69,17 @@ export class BufferService extends Disposable implements IBufferService {
public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void {
const buffer = this.buffer;
let newLine: IBufferLine | undefined;
newLine = this._cachedBlankLine;
const topRow = buffer.ybase + buffer.scrollTop;
const bottomRow = buffer.ybase + buffer.scrollBottom;
let newLine: IBufferLine;
newLine = this._cachedBlankLine!;
if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) {
newLine = buffer.getBlankLine(eraseAttr, isWrapped);
this._cachedBlankLine = newLine;
}
newLine.isWrapped = isWrapped;
const topRow = buffer.ybase + buffer.scrollTop;
const bottomRow = buffer.ybase + buffer.scrollBottom;
if (buffer.scrollTop === 0) {
// Determine whether the buffer is going to be trimmed after insertion.
const willBufferBeTrimmed = buffer.lines.isFull;
+4 -7
View File
@@ -225,12 +225,7 @@ export class DecorationLineCache extends Disposable {
if (newLine < 0) {
continue;
}
const existing = newMap.get(newLine);
if (existing) {
existing.push(...bucket);
} else {
newMap.set(newLine, bucket.slice());
}
this._mergeLineBucket(newMap, newLine, bucket);
}
this._decorationsByLine.clear();
for (const [line, bucket] of newMap) {
@@ -254,7 +249,9 @@ export class DecorationLineCache extends Disposable {
private _mergeLineBucket(newMap: Map<number, IInternalDecoration[]>, line: number, bucket: IInternalDecoration[]): void {
const existing = newMap.get(line);
if (existing) {
existing.push(...bucket);
for (let i = 0, len = bucket.length; i < len; i++) {
existing.push(bucket[i]);
}
} else {
newMap.set(line, bucket.slice());
}
+1 -6
View File
@@ -69,12 +69,7 @@ export class Terminal extends CoreTerminal {
return this.buffer.markers;
}
public addMarker(cursorYOffset: number): IMarker | undefined {
// Disallow markers on the alt buffer
if (this.buffer !== this.buffers.normal) {
return;
}
public registerMarker(cursorYOffset: number): IMarker {
return this.buffer.addMarker(this.buffer.ybase + this.buffer.y + cursorYOffset);
}
+13 -4
View File
@@ -120,10 +120,10 @@ describe('Headless API Tests', function (): void {
await writeSync('\n\rtest' + i);
}
const markers = [
term.registerMarker(1)!,
term.registerMarker(2)!,
term.registerMarker(3)!,
term.registerMarker(4)!
term.registerMarker(1),
term.registerMarker(2),
term.registerMarker(3),
term.registerMarker(4)
];
let disposeCount = 0;
for (const marker of markers) {
@@ -419,6 +419,15 @@ describe('Headless API Tests', function (): void {
strictEqual(term.buffer.normal.getLine(0)!.translateToString(), 'norm ');
strictEqual(term.buffer.alternate.getLine(0), undefined);
});
it('registerMarker on alternate buffer', async () => {
term = new Terminal({ cols: 5, allowProposedApi: true });
await writeSync('\x1b[?47h');
const marker = term.registerMarker(0);
strictEqual(term.buffer.active.type, 'alternate');
strictEqual(term.markers.length, 1);
strictEqual(term.markers[0], marker);
});
});
describe('modes', () => {
+3 -12
View File
@@ -40,15 +40,6 @@ export class Terminal extends Disposable implements ITerminalApi {
};
for (const propName in this._core.options) {
Object.defineProperty(this._publicOptions, propName, {
get: () => {
return this._core.options[propName];
},
set: (value: any) => {
this._checkReadonlyOptions(propName);
this._core.options[propName] = value;
}
});
const desc = {
get: getter.bind(this, propName),
set: setter.bind(this, propName)
@@ -139,11 +130,11 @@ export class Terminal extends Disposable implements ITerminalApi {
this._verifyIntegers(columns, rows);
this._core.resize(columns, rows);
}
public registerMarker(cursorYOffset: number = 0): IMarker | undefined {
public registerMarker(cursorYOffset: number = 0): IMarker {
this._verifyIntegers(cursorYOffset);
return this._core.addMarker(cursorYOffset);
return this._core.registerMarker(cursorYOffset);
}
public addMarker(cursorYOffset: number): IMarker | undefined {
public addMarker(cursorYOffset: number): IMarker {
return this.registerMarker(cursorYOffset);
}
public dispose(): void {
+2 -3
View File
@@ -825,12 +825,11 @@ declare module '@xterm/headless' {
resize(columns: number, rows: number): void;
/**
* Adds a marker to the normal buffer and returns it. If the alt buffer is
* active, undefined is returned.
* Adds a marker to the normal buffer and returns it.
* @param cursorYOffset The y position offset of the marker from the cursor.
* @returns The new marker or undefined.
*/
registerMarker(cursorYOffset?: number): IMarker | undefined;
registerMarker(cursorYOffset?: number): IMarker;
/*
* Disposes of the terminal, detaching it from the DOM and removing any