From b4fa7c6c06f23d932926b5db0cbe85e78824b13e Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sat, 4 Nov 2023 14:35:36 +0100 Subject: [PATCH 01/76] Remove unit tests with Node 16 --- .github/workflows/ci.yml | 34 ---------------------------------- 1 file changed, 34 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a64b6a7..ffaf853f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,40 +148,6 @@ jobs: - name: Unit tests run: yarn test-unit --forbid-only - test-unit: - needs: build - timeout-minutes: 20 - strategy: - matrix: - node-version: [16] - runs-on: [ubuntu, macos, windows] - runs-on: ${{ matrix.runs-on }}-latest - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }}.x - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }}.x - cache: 'yarn' - - name: Install dependencies - run: | - yarn --frozen-lockfile - yarn install-addons - - uses: actions/download-artifact@v3 - with: - name: build-artifacts - - name: Unzip artifacts - shell: bash - run: | - if [ "$RUNNER_OS" == "Windows" ]; then - pwsh -Command "7z x compressed-build.zip -aoa -o${{ github.workspace }}" - else - unzip -o compressed-build.zip - fi - ls -R - - name: Unit tests - run: yarn test-unit --forbid-only - test-api-parallel: timeout-minutes: 20 strategy: From 2861a4dd392e9a451c08d84b1dc47755eb925ad9 Mon Sep 17 00:00:00 2001 From: tisilent Date: Wed, 8 Nov 2023 17:58:38 +0800 Subject: [PATCH 02/76] Draw dashed lines through ratios --- src/browser/renderer/shared/TextureAtlas.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index d7f65d00..f3f67b8b 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -613,7 +613,14 @@ export class TextureAtlas implements ITextureAtlas { nextOffset = computeNextVariantOffset(xChRight - xChLeft, lineWidth, nextOffset); break; case UnderlineStyle.DASHED: - this._tmpCtx.setLineDash([this._config.devicePixelRatio * 4, this._config.devicePixelRatio * 3]); + const lineRatio = 0.6; + const gapRatio = 0.3; + // End line ratio is approximately equal to 0.1 + const xChWidth = xChRight - xChLeft; + const line = Math.floor(lineRatio * xChWidth); + const gap = Math.floor(gapRatio * xChWidth); + const end = xChWidth - line - gap; + this._tmpCtx.setLineDash([line, gap, end]); this._tmpCtx.moveTo(xChLeft, yTop); this._tmpCtx.lineTo(xChRight, yTop); break; From b595940bcfbae34babccb10b7e587fe747d96b43 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 9 Nov 2023 09:43:14 -0800 Subject: [PATCH 03/76] Add range API to serialize addon Fixes #4876 Fixes #4871 --- .../src/SerializeAddon.test.ts | 31 ++++- addons/addon-serialize/src/SerializeAddon.ts | 108 +++++++++--------- .../typings/addon-serialize.d.ts | 21 +++- 3 files changed, 102 insertions(+), 58 deletions(-) diff --git a/addons/addon-serialize/src/SerializeAddon.test.ts b/addons/addon-serialize/src/SerializeAddon.test.ts index d41cfa12..ac485e54 100644 --- a/addons/addon-serialize/src/SerializeAddon.test.ts +++ b/addons/addon-serialize/src/SerializeAddon.test.ts @@ -9,7 +9,6 @@ import { SerializeAddon } from './SerializeAddon'; import { Terminal } from 'browser/public/Terminal'; import { SelectionModel } from 'browser/selection/SelectionModel'; import { IBufferService } from 'common/services/Services'; -import { OptionsService } from 'common/services/OptionsService'; import { ThemeService } from 'browser/services/ThemeService'; function sgr(...seq: string[]): string { @@ -83,6 +82,36 @@ describe('SerializeAddon', () => { await writeP(terminal, sgr('32') + '> ' + sgr('0')); assert.equal(serializeAddon.serialize(), '\u001b[32m> \u001b[0m'); }); + + describe('ISerializeOptions.range', () => { + it('should serialize the top line', async () => { + await writeP(terminal, 'hello\r\nworld'); + assert.equal(serializeAddon.serialize({ + range: { + start: 0, + end: 0 + } + }), 'hello'); + }); + it('should serialize multiple lines from the top', async () => { + await writeP(terminal, 'hello\r\nworld'); + assert.equal(serializeAddon.serialize({ + range: { + start: 0, + end: 1 + } + }), 'hello\r\nworld'); + }); + it('should serialize lines in the middle', async () => { + await writeP(terminal, 'hello\r\nworld'); + assert.equal(serializeAddon.serialize({ + range: { + start: 1, + end: 1 + } + }), 'world'); + }); + }); }); describe('html', () => { diff --git a/addons/addon-serialize/src/SerializeAddon.ts b/addons/addon-serialize/src/SerializeAddon.ts index 961d8546..e654eddb 100644 --- a/addons/addon-serialize/src/SerializeAddon.ts +++ b/addons/addon-serialize/src/SerializeAddon.ts @@ -6,7 +6,7 @@ */ import type { IBuffer, IBufferCell, IBufferRange, ITerminalAddon, Terminal } from '@xterm/xterm'; -import type { SerializeAddon as ISerializeApi } from '@xterm/addon-serialize'; +import type { IHTMLSerializeOptions, SerializeAddon as ISerializeApi, ISerializeOptions, ISerializeRange } from '@xterm/addon-serialize'; import { DEFAULT_ANSI_COLORS } from 'browser/services/ThemeService'; import { IAttributeData, IColor } from 'common/Types'; @@ -21,24 +21,24 @@ abstract class BaseSerializeHandler { ) { } - public serialize(range: IBufferRange): string { + public serialize(range: IBufferRange, excludeFinalCursorPosition?: boolean): string { // we need two of them to flip between old and new cell const cell1 = this._buffer.getNullCell(); const cell2 = this._buffer.getNullCell(); let oldCell = cell1; - const startRow = range.start.x; - const endRow = range.end.x; - const startColumn = range.start.y; - const endColumn = range.end.y; + const startRow = range.start.y; + const endRow = range.end.y; + const startColumn = range.start.x; + const endColumn = range.end.x; this._beforeSerialize(endRow - startRow, startRow, endRow); for (let row = startRow; row <= endRow; row++) { const line = this._buffer.getLine(row); if (line) { - const startLineColumn = row !== range.start.x ? 0 : startColumn; - const endLineColumn = row !== range.end.x ? line.length : endColumn; + const startLineColumn = row === range.start.y ? startColumn : 0; + const endLineColumn = row === range.end.y ? endColumn: line.length; for (let col = startLineColumn; col < endLineColumn; col++) { const c = line.getCell(col, oldCell === cell1 ? cell2 : cell1); if (!c) { @@ -54,14 +54,14 @@ abstract class BaseSerializeHandler { this._afterSerialize(); - return this._serializeString(); + return this._serializeString(excludeFinalCursorPosition); } protected _nextCell(cell: IBufferCell, oldCell: IBufferCell, row: number, col: number): void { } protected _rowEnd(row: number, isLastRow: boolean): void { } protected _beforeSerialize(rows: number, startRow: number, endRow: number): void { } protected _afterSerialize(): void { } - protected _serializeString(): string { return ''; } + protected _serializeString(excludeFinalCursorPosition?: boolean): string { return ''; } } function equalFg(cell1: IBufferCell | IAttributeData, cell2: IBufferCell): boolean { @@ -353,7 +353,7 @@ class StringSerializeHandler extends BaseSerializeHandler { } } - protected _serializeString(): string { + protected _serializeString(excludeFinalCursorPosition: boolean): string { let rowEnd = this._allRows.length; // the fixup is only required for data without scrollback @@ -374,29 +374,31 @@ class StringSerializeHandler extends BaseSerializeHandler { } // restore the cursor - const realCursorRow = this._buffer.baseY + this._buffer.cursorY; - const realCursorCol = this._buffer.cursorX; + if (!excludeFinalCursorPosition) { + const realCursorRow = this._buffer.baseY + this._buffer.cursorY; + const realCursorCol = this._buffer.cursorX; - const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol); + const cursorMoved = (realCursorRow !== this._lastCursorRow || realCursorCol !== this._lastCursorCol); - const moveRight = (offset: number): void => { - if (offset > 0) { - content += `\u001b[${offset}C`; - } else if (offset < 0) { - content += `\u001b[${-offset}D`; + const moveRight = (offset: number): void => { + if (offset > 0) { + content += `\u001b[${offset}C`; + } else if (offset < 0) { + content += `\u001b[${-offset}D`; + } + }; + const moveDown = (offset: number): void => { + if (offset > 0) { + content += `\u001b[${offset}B`; + } else if (offset < 0) { + content += `\u001b[${-offset}A`; + } + }; + + if (cursorMoved) { + moveDown(realCursorRow - this._lastCursorRow); + moveRight(realCursorCol - this._lastCursorCol); } - }; - const moveDown = (offset: number): void => { - if (offset > 0) { - content += `\u001b[${offset}B`; - } else if (offset < 0) { - content += `\u001b[${-offset}A`; - } - }; - - if (cursorMoved) { - moveDown(realCursorRow - this._lastCursorRow); - moveRight(realCursorCol - this._lastCursorCol); } // Restore the cursor's current style, see https://github.com/xtermjs/xterm.js/issues/3677 @@ -419,14 +421,21 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi { this._terminal = terminal; } - private _serializeBuffer(terminal: Terminal, buffer: IBuffer, scrollback?: number): string { + private _serializeBufferByScrollback(terminal: Terminal, buffer: IBuffer, scrollback?: number): string { const maxRows = buffer.length; - const handler = new StringSerializeHandler(buffer, terminal); const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); + return this._serializeBufferByRange(terminal, buffer, { + start: maxRows - correctRows, + end: maxRows - 1 + }, false); + } + + private _serializeBufferByRange(terminal: Terminal, buffer: IBuffer, range: ISerializeRange, excludeFinalCursorPosition: boolean): string { + const handler = new StringSerializeHandler(buffer, terminal); return handler.serialize({ - start: { x: maxRows - correctRows, y: 0 }, - end: { x: maxRows - 1, y: terminal.cols } - }); + start: { x: 0, y: typeof range.start === 'number' ? range.start : range.start.line }, + end: { x: terminal.cols, y: typeof range.end === 'number' ? range.end : range.end.line } + }, excludeFinalCursorPosition); } private _serializeBufferAsHTML(terminal: Terminal, options: Partial): string { @@ -438,16 +447,16 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi { const scrollback = options.scrollback; const correctRows = (scrollback === undefined) ? maxRows : constrain(scrollback + terminal.rows, 0, maxRows); return handler.serialize({ - start: { x: maxRows - correctRows, y: 0 }, - end: { x: maxRows - 1, y: terminal.cols } + start: { x: 0, y: maxRows - correctRows }, + end: { x: terminal.cols, y: maxRows - 1 } }); } const selection = this._terminal?.getSelectionPosition(); if (selection !== undefined) { return handler.serialize({ - start: { x: selection.start.y, y: selection.start.x }, - end: { x: selection.end.y, y: selection.end.x } + start: { x: selection.start.x, y: selection.start.y }, + end: { x: selection.end.x, y: selection.end.y } }); } @@ -490,12 +499,14 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi { } // Normal buffer - let content = this._serializeBuffer(this._terminal, this._terminal.buffer.normal, options?.scrollback); + let content = options?.range + ? this._serializeBufferByRange(this._terminal, this._terminal.buffer.normal, options.range, true) + : this._serializeBufferByScrollback(this._terminal, this._terminal.buffer.normal, options?.scrollback); // Alternate buffer if (!options?.excludeAltBuffer) { if (this._terminal.buffer.active.type === 'alternate') { - const alternativeScreenContent = this._serializeBuffer(this._terminal, this._terminal.buffer.alternate, undefined); + const alternativeScreenContent = this._serializeBufferByScrollback(this._terminal, this._terminal.buffer.alternate, undefined); content += `\u001b[?1049h\u001b[H${alternativeScreenContent}`; } } @@ -519,19 +530,6 @@ export class SerializeAddon implements ITerminalAddon , ISerializeApi { public dispose(): void { } } - -interface ISerializeOptions { - scrollback?: number; - excludeModes?: boolean; - excludeAltBuffer?: boolean; -} - -interface IHTMLSerializeOptions { - scrollback: number; - onlySelection: boolean; - includeGlobalBackground: boolean; -} - export class HTMLSerializeHandler extends BaseSerializeHandler { private _currentRow: string = ''; diff --git a/addons/addon-serialize/typings/addon-serialize.d.ts b/addons/addon-serialize/typings/addon-serialize.d.ts index 0b127b50..90b8b428 100644 --- a/addons/addon-serialize/typings/addon-serialize.d.ts +++ b/addons/addon-serialize/typings/addon-serialize.d.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ITerminalAddon } from '@xterm/xterm'; +import { Terminal, ITerminalAddon, IMarker, IBufferRange } from '@xterm/xterm'; declare module '@xterm/addon-serialize' { /** @@ -48,10 +48,16 @@ declare module '@xterm/addon-serialize' { } export interface ISerializeOptions { + /** + * The row range to serialize. The an explicit range is specified, the cursor will get its final + * repositioning. + */ + range?: ISerializeRange; + /** * The number of rows in the scrollback buffer to serialize, starting from the bottom of the * scrollback buffer. When not specified, all available rows in the scrollback buffer will be - * serialized. + * serialized. This will be ignored if {@link range} is specified. */ scrollback?: number; @@ -85,4 +91,15 @@ declare module '@xterm/addon-serialize' { */ includeGlobalBackground: boolean; } + + export interface ISerializeRange { + /** + * The line to start serializing (inclusive). + */ + start: IMarker | number; + /** + * The line to end serializing (inclusive). + */ + end: IMarker | number; + } } From d218bff2c4e79ac40599d5b3842df2e4aac97037 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 10 Nov 2023 16:12:08 +0000 Subject: [PATCH 04/76] Bump axios from 0.21.2 to 1.6.0 in /addons/addon-ligatures Bumps [axios](https://github.com/axios/axios) from 0.21.2 to 1.6.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v0.21.2...v1.6.0) --- updated-dependencies: - dependency-name: axios dependency-type: direct:development ... Signed-off-by: dependabot[bot] --- addons/addon-ligatures/package.json | 2 +- addons/addon-ligatures/yarn.lock | 63 ++++++++++++++++++++++++----- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/addons/addon-ligatures/package.json b/addons/addon-ligatures/package.json index 30251888..e5c6b972 100644 --- a/addons/addon-ligatures/package.json +++ b/addons/addon-ligatures/package.json @@ -36,7 +36,7 @@ }, "devDependencies": { "@types/sinon": "^5.0.1", - "axios": "^0.21.2", + "axios": "^1.6.0", "mkdirp": "0.5.5", "sinon": "6.3.5", "yauzl": "^2.10.0" diff --git a/addons/addon-ligatures/yarn.lock b/addons/addon-ligatures/yarn.lock index 6ba3eccb..966fc113 100644 --- a/addons/addon-ligatures/yarn.lock +++ b/addons/addon-ligatures/yarn.lock @@ -45,17 +45,36 @@ array-from@^2.1.1: resolved "https://registry.yarnpkg.com/array-from/-/array-from-2.1.1.tgz#cfe9d8c26628b9dc5aecc62a9f5d8f1f352c1195" integrity sha1-z+nYwmYoudxa7MYqn12PHzUsEZU= -axios@^0.21.2: - version "0.21.2" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017" - integrity sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg== +asynckit@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79" + integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q== + +axios@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-1.6.0.tgz#f1e5292f26b2fd5c2e66876adc5b06cdbd7d2102" + integrity sha512-EZ1DYihju9pwVB+jg67ogm+Tmqc6JmhamRN6I4Zt8DfZu5lbcQGw3ozH9lFejSJgs/ibaef3A9PMXPLeefFGJg== dependencies: - follow-redirects "^1.14.0" + follow-redirects "^1.15.0" + form-data "^4.0.0" + proxy-from-env "^1.1.0" buffer-crc32@~0.2.3: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" +combined-stream@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f" + integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg== + dependencies: + delayed-stream "~1.0.0" + +delayed-stream@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619" + integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ== + diff@^3.5.0: version "3.5.0" resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12" @@ -66,10 +85,10 @@ fd-slicer@~1.1.0: dependencies: pend "~1.2.0" -follow-redirects@^1.14.0: - version "1.14.8" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.8.tgz#016996fb9a11a100566398b1c6839337d7bfa8fc" - integrity sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA== +follow-redirects@^1.15.0: + version "1.15.3" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.3.tgz#fe2f3ef2690afce7e82ed0b44db08165b207123a" + integrity sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q== font-finder@^1.0.3: version "1.0.4" @@ -95,6 +114,15 @@ font-ligatures@^1.4.1: lru-cache "^6.0.0" opentype.js "^0.8.0" +form-data@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-4.0.0.tgz#93919daeaf361ee529584b9b31664dc12c9fa452" + integrity sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww== + dependencies: + asynckit "^0.4.0" + combined-stream "^1.0.8" + mime-types "^2.1.12" + get-system-fonts@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/get-system-fonts/-/get-system-fonts-2.0.0.tgz#a43b9a33f05c0715a60176d2aad5ce6e98f0a3c6" @@ -140,6 +168,18 @@ lru-cache@^6.0.0: dependencies: yallist "^4.0.0" +mime-db@1.52.0: + version "1.52.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70" + integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg== + +mime-types@^2.1.12: + version "2.1.35" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a" + integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw== + dependencies: + mime-db "1.52.0" + minimist@^1.2.5: version "1.2.6" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" @@ -183,6 +223,11 @@ promise-stream-reader@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/promise-stream-reader/-/promise-stream-reader-1.0.1.tgz#4e793a79c9d49a73ccd947c6da9c127f12923649" +proxy-from-env@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2" + integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg== + sinon@6.3.5: version "6.3.5" resolved "https://registry.yarnpkg.com/sinon/-/sinon-6.3.5.tgz#0f6d6a5b4ebaad1f6e8e019395542d1d02c144a0" From 1943636f023b0496c2b95aeb31f2d08d7c0e4dce Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 8 Dec 2023 12:14:22 -0800 Subject: [PATCH 05/76] Help embedders avoid memory leaks by clearing options Related microsoft/vscode#192838 --- src/browser/Linkifier2.ts | 3 +++ src/common/services/OptionsService.ts | 9 ++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 28002e04..f7b1f605 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -39,6 +39,9 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { this.register(getDisposeArrayDisposable(this._linkCacheDisposables)); this.register(toDisposable(() => { this._lastMouseEvent = undefined; + // Clear out link providers as they could easily cause an embedder memory leak + this._linkProviders.length = 0; + this._activeProviderReplies?.clear(); })); // Listen to resize to catch the case where it's resized and the cursor is out of the viewport. this.register(this._bufferService.onResize(() => { diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index eb9dbfa8..ba92992e 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -4,7 +4,7 @@ */ import { EventEmitter } from 'common/EventEmitter'; -import { Disposable } from 'common/Lifecycle'; +import { Disposable, toDisposable } from 'common/Lifecycle'; import { isMac } from 'common/Platform'; import { CursorStyle, IDisposable } from 'common/Types'; import { FontWeight, IOptionsService, ITerminalOptions } from 'common/services/Services'; @@ -86,6 +86,13 @@ export class OptionsService extends Disposable implements IOptionsService { this.rawOptions = defaultOptions; this.options = { ... defaultOptions }; this._setupOptions(); + + // Clear out options that could link outside xterm.js as they could easily cause an embedder + // memory leak + this.register(toDisposable(() => { + this.rawOptions.linkHandler = null; + this.rawOptions.documentOverride = null; + })); } // eslint-disable-next-line @typescript-eslint/naming-convention From 54608bf38d1d7e9d067d9700a9dcd536a48473e5 Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Tue, 12 Dec 2023 04:16:28 -0500 Subject: [PATCH 06/76] Fixes https://github.com/microsoft/vscode/issues/200469 --- src/browser/RenderDebouncer.ts | 11 ++++++----- src/browser/services/RenderService.ts | 5 ++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/browser/RenderDebouncer.ts b/src/browser/RenderDebouncer.ts index b3118d5f..4877a140 100644 --- a/src/browser/RenderDebouncer.ts +++ b/src/browser/RenderDebouncer.ts @@ -4,6 +4,7 @@ */ import { IRenderDebouncerWithCallback } from 'browser/Types'; +import { ICoreBrowserService } from 'browser/services/Services'; /** * Debounces calls to render terminal rows using animation frames. @@ -16,14 +17,14 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback { private _refreshCallbacks: FrameRequestCallback[] = []; constructor( - private _parentWindow: Window, - private _renderCallback: (start: number, end: number) => void + private _renderCallback: (start: number, end: number) => void, + private readonly _coreBrowserService: ICoreBrowserService, ) { } public dispose(): void { if (this._animationFrame) { - this._parentWindow.cancelAnimationFrame(this._animationFrame); + this._coreBrowserService.window.cancelAnimationFrame(this._animationFrame); this._animationFrame = undefined; } } @@ -31,7 +32,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback { public addRefreshCallback(callback: FrameRequestCallback): number { this._refreshCallbacks.push(callback); if (!this._animationFrame) { - this._animationFrame = this._parentWindow.requestAnimationFrame(() => this._innerRefresh()); + this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh()); } return this._animationFrame; } @@ -49,7 +50,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback { return; } - this._animationFrame = this._parentWindow.requestAnimationFrame(() => this._innerRefresh()); + this._animationFrame = this._coreBrowserService.window.requestAnimationFrame(() => this._innerRefresh()); } private _innerRefresh(): void { diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index 9fa8d234..dddd5185 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -10,7 +10,7 @@ import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } import { EventEmitter } from 'common/EventEmitter'; import { Disposable, MutableDisposable } from 'common/Lifecycle'; import { DebouncedIdleTask } from 'common/TaskQueue'; -import { IBufferService, IDecorationService, IInstantiationService, IOptionsService } from 'common/services/Services'; +import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; interface ISelectionState { start: [number, number] | undefined; @@ -56,12 +56,11 @@ export class RenderService extends Disposable implements IRenderService { @IDecorationService decorationService: IDecorationService, @IBufferService bufferService: IBufferService, @ICoreBrowserService coreBrowserService: ICoreBrowserService, - @IInstantiationService instantiationService: IInstantiationService, @IThemeService themeService: IThemeService ) { super(); - this._renderDebouncer = new RenderDebouncer(coreBrowserService.window, (start, end) => this._renderRows(start, end)); + this._renderDebouncer = new RenderDebouncer((start, end) => this._renderRows(start, end), coreBrowserService); this.register(this._renderDebouncer); this.register(coreBrowserService.onDprChange(() => this.handleDevicePixelRatioChange())); From 97750f672f2446974c70a3ad9efe3de7fd779f2f Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Tue, 12 Dec 2023 04:41:20 -0500 Subject: [PATCH 07/76] :lipstick: --- src/browser/RenderDebouncer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/RenderDebouncer.ts b/src/browser/RenderDebouncer.ts index 4877a140..dd3b97a6 100644 --- a/src/browser/RenderDebouncer.ts +++ b/src/browser/RenderDebouncer.ts @@ -18,7 +18,7 @@ export class RenderDebouncer implements IRenderDebouncerWithCallback { constructor( private _renderCallback: (start: number, end: number) => void, - private readonly _coreBrowserService: ICoreBrowserService, + private readonly _coreBrowserService: ICoreBrowserService ) { } From 2592e1e84d174d060a7b81243dfe70355bda78db Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Tue, 12 Dec 2023 05:55:30 -0500 Subject: [PATCH 08/76] re register intersection observer on window change --- src/browser/services/RenderService.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index dddd5185..f1d25fd2 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -3,12 +3,13 @@ * @license MIT */ +import { IDisposable } from 'common/Types'; import { RenderDebouncer } from 'browser/RenderDebouncer'; import { IRenderDebouncerWithCallback } from 'browser/Types'; import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types'; import { ICharSizeService, ICoreBrowserService, IRenderService, IThemeService } from 'browser/services/Services'; import { EventEmitter } from 'common/EventEmitter'; -import { Disposable, MutableDisposable } from 'common/Lifecycle'; +import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; import { DebouncedIdleTask } from 'common/TaskQueue'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; @@ -24,6 +25,7 @@ export class RenderService extends Disposable implements IRenderService { private _renderer: MutableDisposable = this.register(new MutableDisposable()); private _renderDebouncer: IRenderDebouncerWithCallback; private _pausedResizeTask = new DebouncedIdleTask(); + private _observerDisposable: IDisposable | undefined; private _isPaused: boolean = false; private _needsFullRefresh: boolean = false; @@ -38,7 +40,7 @@ export class RenderService extends Disposable implements IRenderService { }; private readonly _onDimensionsChange = this.register(new EventEmitter()); - public readonly onDimensionsChange = this._onDimensionsChange.event; + public readonly onDimensionsChange = this._onDimensionsChange.event; private readonly _onRenderedViewportChange = this.register(new EventEmitter<{ start: number, end: number }>()); public readonly onRenderedViewportChange = this._onRenderedViewportChange.event; private readonly _onRender = this.register(new EventEmitter<{ start: number, end: number }>()); @@ -101,12 +103,18 @@ export class RenderService extends Disposable implements IRenderService { this.register(themeService.onChangeColors(() => this._fullRefresh())); + this._registerIntersectionObserver(coreBrowserService.window, screenElement); + this.register(coreBrowserService.onWindowChange((w) => this._registerIntersectionObserver(w, screenElement))); + } + + private _registerIntersectionObserver(w: Window & typeof globalThis, screenElement: HTMLElement): void { // Detect whether IntersectionObserver is detected and enable renderer pause // and resume based on terminal visibility if so - if ('IntersectionObserver' in coreBrowserService.window) { - const observer = new coreBrowserService.window.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 }); + this._observerDisposable?.dispose(); + if ('IntersectionObserver' in w) { + const observer = new w.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 }); observer.observe(screenElement); - this.register({ dispose: () => observer.disconnect() }); + this._observerDisposable = toDisposable(() => observer.disconnect()); } } From 88ba66d00bb848569e867fea894d444defee69b5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 12 Dec 2023 05:17:24 -0800 Subject: [PATCH 09/76] addCustomWheelEventHandler API I opted for consistency with the key event handler over our more modern approach, we can migrate them at the same time if we end up returning a disposable. See microsoft/vscode#76381 --- src/browser/Terminal.ts | 23 +++++++++++++---------- src/browser/TestUtils.test.ts | 3 +++ src/browser/Types.d.ts | 1 + src/browser/public/Terminal.ts | 3 +++ test/playwright/TestUtils.ts | 1 + typings/xterm.d.ts | 22 ++++++++++++++++++++++ 6 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index fad2d80b..7e1f9014 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -26,7 +26,7 @@ import { addDisposableDomListener } from 'browser/Lifecycle'; import { Linkifier2 } from 'browser/Linkifier2'; import * as Strings from 'browser/LocalizableStrings'; import { OscLinkProvider } from 'browser/OscLinkProvider'; -import { CharacterJoinerHandler, CustomKeyEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal, IViewport } from 'browser/Types'; +import { CharacterJoinerHandler, CustomKeyEventHandler, CustomWheelEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal, IViewport } from 'browser/Types'; import { Viewport } from 'browser/Viewport'; import { BufferDecorationRenderer } from 'browser/decorations/BufferDecorationRenderer'; import { OverviewRulerRenderer } from 'browser/decorations/OverviewRulerRenderer'; @@ -74,6 +74,7 @@ export class Terminal extends CoreTerminal implements ITerminal { public browser: IBrowser = Browser as any; private _customKeyEventHandler: CustomKeyEventHandler | undefined; + private _customWheelEventHandler: CustomWheelEventHandler | undefined; // browser services private _decorationService: DecorationService; @@ -633,6 +634,9 @@ export class Terminal extends CoreTerminal implements ITerminal { but = ev.button < 3 ? ev.button : CoreMouseButton.NONE; break; case 'wheel': + if (self._customWheelEventHandler && self._customWheelEventHandler(ev as WheelEvent) === false) { + return false; + } const amount = self.viewport!.getLinesScrolled(ev as WheelEvent); if (amount === 0) { @@ -792,6 +796,10 @@ export class Terminal extends CoreTerminal implements ITerminal { // do nothing, if app side handles wheel itself if (requestedEvents.wheel) return; + if (this._customWheelEventHandler && this._customWheelEventHandler(ev) === false) { + return false; + } + if (!this.buffer.hasScrollback) { // Convert wheel events into up/down events when the buffer does not have scrollback, this // enables scrolling in apps hosted in the alt buffer such as vim or tmux. @@ -878,19 +886,14 @@ export class Terminal extends CoreTerminal implements ITerminal { paste(data, this.textarea!, this.coreService, this.optionsService); } - /** - * Attaches a custom key event handler which is run before keys are processed, - * giving consumers of xterm.js ultimate control as to what keys should be - * processed by the terminal and what keys should not. - * @param customKeyEventHandler The custom KeyboardEvent handler to attach. - * This is a function that takes a KeyboardEvent, allowing consumers to stop - * propagation and/or prevent the default action. The function returns whether - * the event should be processed by xterm.js. - */ public attachCustomKeyEventHandler(customKeyEventHandler: CustomKeyEventHandler): void { this._customKeyEventHandler = customKeyEventHandler; } + public attachCustomWheelEventHandler(customWheelEventHandler: CustomWheelEventHandler): void { + this._customWheelEventHandler = customWheelEventHandler; + } + public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { return this.linkifier2.registerLinkProvider(linkProvider); } diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 7e43017a..a969ec2c 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -86,6 +86,9 @@ export class MockTerminal implements ITerminal { public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { throw new Error('Method not implemented.'); } + public attachCustomWheelEventHandler(customWheelEventHandler: (event: WheelEvent) => boolean): void { + throw new Error('Method not implemented.'); + } public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise): IDisposable { throw new Error('Method not implemented.'); } diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index b1f31b20..d1ad5f87 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -32,6 +32,7 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal { } export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean; +export type CustomWheelEventHandler = (event: WheelEvent) => boolean; export type LineData = CharData[]; diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 6f009f74..ade46fa4 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -148,6 +148,9 @@ export class Terminal extends Disposable implements ITerminalApi { public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { this._core.attachCustomKeyEventHandler(customKeyEventHandler); } + public attachCustomWheelEventHandler(customWheelEventHandler: (event: WheelEvent) => boolean): void { + this._core.attachCustomWheelEventHandler(customWheelEventHandler); + } public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { return this._core.registerLinkProvider(linkProvider); } diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index 3e925f79..4d4112f0 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -75,6 +75,7 @@ type TerminalProxyCustomOverrides = 'buffer' | ( 'options' | 'open' | 'attachCustomKeyEventHandler' | + 'attachCustomWheelEventHandler' | 'registerLinkProvider' | 'registerCharacterJoiner' | 'deregisterCharacterJoiner' | diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 70b0c6d7..211fce09 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1010,6 +1010,28 @@ declare module '@xterm/xterm' { */ attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; + /** + * Attaches a custom wheel event handler which is run before keys are + * processed, giving consumers of xterm.js control over whether to proceed + * or cancel terminal wheel events. + * @param customMouseEventHandler The custom WheelEvent handler to attach. + * This is a function that takes a WheelEvent, allowing consumers to stop + * propagation and/or prevent the default action. The function returns + * whether the event should be processed by xterm.js. + * + * @example A handler that prevents all wheel events while ctrl is held from + * being processed. + * ```ts + * term.attachCustomKeyEventHandler(ev => { + * if (ev.ctrlKey) { + * return false; + * } + * return true; + * }); + * ``` + */ + attachCustomWheelEventHandler(customWheelEventHandler: (event: WheelEvent) => boolean): void; + /** * Registers a link provider, allowing a custom parser to be used to match * and handle links. Multiple link providers can be used, they will be asked From 7ac95fab28ddb62b30d13b15238848fa1b72e9a2 Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Tue, 12 Dec 2023 09:19:53 -0500 Subject: [PATCH 10/76] More fixes --- addons/addon-canvas/src/CanvasRenderer.ts | 15 +++++++++++++-- addons/addon-webgl/src/WebglRenderer.ts | 14 ++++++++++++-- src/browser/services/RenderService.ts | 5 +++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/addons/addon-canvas/src/CanvasRenderer.ts b/addons/addon-canvas/src/CanvasRenderer.ts index 40b89546..a8e01b1a 100644 --- a/addons/addon-canvas/src/CanvasRenderer.ts +++ b/addons/addon-canvas/src/CanvasRenderer.ts @@ -12,7 +12,7 @@ import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeS import { EventEmitter, forwardEvent } from 'common/EventEmitter'; import { Disposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; -import { Terminal } from '@xterm/xterm'; +import { IDisposable, Terminal } from '@xterm/xterm'; import { CursorRenderLayer } from './CursorRenderLayer'; import { LinkRenderLayer } from './LinkRenderLayer'; import { SelectionRenderLayer } from './SelectionRenderLayer'; @@ -22,6 +22,7 @@ import { IRenderLayer } from './Types'; export class CanvasRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; private _devicePixelRatio: number; + private _observerDisposable : IDisposable| undefined; public dimensions: IRenderDimensions; @@ -60,7 +61,12 @@ export class CanvasRenderer extends Disposable implements IRenderer { this._devicePixelRatio = this._coreBrowserService.dpr; this._updateDimensions(); - this.register(observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h))); + this._observerDisposable = observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); + this.register(this._coreBrowserService.onWindowChange(w => { + this._observerDisposable?.dispose(); + this._observerDisposable = observeDevicePixelDimensions(this._renderLayers[0].canvas, w, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); + })); + this.register(toDisposable(() => { for (const l of this._renderLayers) { l.dispose(); @@ -183,4 +189,9 @@ export class CanvasRenderer extends Disposable implements IRenderer { private _requestRedrawViewport(): void { this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 }); } + + public override dispose(): void { + this._observerDisposable?.dispose(); + super.dispose(); + } } diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index 2bccc9b7..db3c28c8 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -19,7 +19,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { CellData } from 'common/buffer/CellData'; import { Attributes, Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; -import { Terminal } from '@xterm/xterm'; +import { IDisposable, Terminal } from '@xterm/xterm'; import { GlyphRenderer } from './GlyphRenderer'; import { RectangleRenderer } from './RectangleRenderer'; import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL, RenderModel } from './RenderModel'; @@ -33,6 +33,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _charAtlasDisposable = this.register(new MutableDisposable()); private _charAtlas: ITextureAtlas | undefined; private _devicePixelRatio: number; + private _observerDisposable : IDisposable| undefined; private _model: RenderModel = new RenderModel(); private _workCell: CellData = new CellData(); @@ -123,7 +124,11 @@ export class WebglRenderer extends Disposable implements IRenderer { this._requestRedrawViewport(); })); - this.register(observeDevicePixelDimensions(this._canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h))); + this._observerDisposable = observeDevicePixelDimensions(this._canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); + this.register(this._coreBrowserService.onWindowChange(w => { + this._observerDisposable?.dispose(); + this._observerDisposable = observeDevicePixelDimensions(this._canvas, w, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); + })); this._core.screenElement!.appendChild(this._canvas); @@ -594,6 +599,11 @@ export class WebglRenderer extends Disposable implements IRenderer { const cursorY = this._terminal.buffer.active.cursorY; this._onRequestRedraw.fire({ start: cursorY, end: cursorY }); } + + public override dispose(): void { + this._observerDisposable?.dispose(); + super.dispose(); + } } // TODO: Share impl with core diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index f1d25fd2..e8d59f7e 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -283,4 +283,9 @@ export class RenderService extends Disposable implements IRenderService { public clear(): void { this._renderer.value?.clear(); } + + public override dispose(): void { + this._observerDisposable?.dispose(); + super.dispose(); + } } From 81a4d860c5d4622c68f9ea829d0b0298a3e605be Mon Sep 17 00:00:00 2001 From: jeanp413 Date: Tue, 12 Dec 2023 09:21:55 -0500 Subject: [PATCH 11/76] :lipstick: --- addons/addon-canvas/src/CanvasRenderer.ts | 2 +- addons/addon-webgl/src/WebglRenderer.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/addon-canvas/src/CanvasRenderer.ts b/addons/addon-canvas/src/CanvasRenderer.ts index a8e01b1a..947213d4 100644 --- a/addons/addon-canvas/src/CanvasRenderer.ts +++ b/addons/addon-canvas/src/CanvasRenderer.ts @@ -22,7 +22,7 @@ import { IRenderLayer } from './Types'; export class CanvasRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; private _devicePixelRatio: number; - private _observerDisposable : IDisposable| undefined; + private _observerDisposable: IDisposable | undefined; public dimensions: IRenderDimensions; diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index db3c28c8..b5f378c2 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -33,7 +33,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _charAtlasDisposable = this.register(new MutableDisposable()); private _charAtlas: ITextureAtlas | undefined; private _devicePixelRatio: number; - private _observerDisposable : IDisposable| undefined; + private _observerDisposable: IDisposable | undefined; private _model: RenderModel = new RenderModel(); private _workCell: CellData = new CellData(); From 427b4f6c6d35c5e5807928801b1b8f8638017b9b Mon Sep 17 00:00:00 2001 From: tisilent Date: Thu, 14 Dec 2023 16:55:49 +0800 Subject: [PATCH 12/76] Update attachCustomWheelEventHandler comments --- typings/xterm.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 211fce09..39a9c91a 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -1014,7 +1014,7 @@ declare module '@xterm/xterm' { * Attaches a custom wheel event handler which is run before keys are * processed, giving consumers of xterm.js control over whether to proceed * or cancel terminal wheel events. - * @param customMouseEventHandler The custom WheelEvent handler to attach. + * @param customWheelEventHandler The custom WheelEvent handler to attach. * This is a function that takes a WheelEvent, allowing consumers to stop * propagation and/or prevent the default action. The function returns * whether the event should be processed by xterm.js. @@ -1022,7 +1022,7 @@ declare module '@xterm/xterm' { * @example A handler that prevents all wheel events while ctrl is held from * being processed. * ```ts - * term.attachCustomKeyEventHandler(ev => { + * term.attachCustomWheelEventHandler(ev => { * if (ev.ctrlKey) { * return false; * } From 9f995c6ff8cff1203c099db8b5b38ef3097d8540 Mon Sep 17 00:00:00 2001 From: Jean Pierre Date: Fri, 15 Dec 2023 16:03:37 +0000 Subject: [PATCH 13/76] :lipstick: --- addons/addon-canvas/src/CanvasRenderer.ts | 16 +++++----------- addons/addon-webgl/src/WebglRenderer.ts | 14 ++++---------- src/browser/services/RenderService.ts | 11 ++--------- 3 files changed, 11 insertions(+), 30 deletions(-) diff --git a/addons/addon-canvas/src/CanvasRenderer.ts b/addons/addon-canvas/src/CanvasRenderer.ts index 947213d4..148ae736 100644 --- a/addons/addon-canvas/src/CanvasRenderer.ts +++ b/addons/addon-canvas/src/CanvasRenderer.ts @@ -10,9 +10,9 @@ import { createRenderDimensions } from 'browser/renderer/shared/RendererUtils'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/shared/Types'; import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { EventEmitter, forwardEvent } from 'common/EventEmitter'; -import { Disposable, toDisposable } from 'common/Lifecycle'; +import { Disposable, MutableDisposable, toDisposable } from 'common/Lifecycle'; import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; -import { IDisposable, Terminal } from '@xterm/xterm'; +import { Terminal } from '@xterm/xterm'; import { CursorRenderLayer } from './CursorRenderLayer'; import { LinkRenderLayer } from './LinkRenderLayer'; import { SelectionRenderLayer } from './SelectionRenderLayer'; @@ -22,7 +22,7 @@ import { IRenderLayer } from './Types'; export class CanvasRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; private _devicePixelRatio: number; - private _observerDisposable: IDisposable | undefined; + private _observerDisposable = this.register(new MutableDisposable()); public dimensions: IRenderDimensions; @@ -61,10 +61,9 @@ export class CanvasRenderer extends Disposable implements IRenderer { this._devicePixelRatio = this._coreBrowserService.dpr; this._updateDimensions(); - this._observerDisposable = observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); + this._observerDisposable.value = observeDevicePixelDimensions(this._renderLayers[0].canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); this.register(this._coreBrowserService.onWindowChange(w => { - this._observerDisposable?.dispose(); - this._observerDisposable = observeDevicePixelDimensions(this._renderLayers[0].canvas, w, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); + this._observerDisposable.value = observeDevicePixelDimensions(this._renderLayers[0].canvas, w, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); })); this.register(toDisposable(() => { @@ -189,9 +188,4 @@ export class CanvasRenderer extends Disposable implements IRenderer { private _requestRedrawViewport(): void { this._onRequestRedraw.fire({ start: 0, end: this._bufferService.rows - 1 }); } - - public override dispose(): void { - this._observerDisposable?.dispose(); - super.dispose(); - } } diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index b5f378c2..f2f2c83e 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -19,7 +19,7 @@ import { AttributeData } from 'common/buffer/AttributeData'; import { CellData } from 'common/buffer/CellData'; import { Attributes, Content, NULL_CELL_CHAR, NULL_CELL_CODE } from 'common/buffer/Constants'; import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; -import { IDisposable, Terminal } from '@xterm/xterm'; +import { Terminal } from '@xterm/xterm'; import { GlyphRenderer } from './GlyphRenderer'; import { RectangleRenderer } from './RectangleRenderer'; import { COMBINED_CHAR_BIT_MASK, RENDER_MODEL_BG_OFFSET, RENDER_MODEL_EXT_OFFSET, RENDER_MODEL_FG_OFFSET, RENDER_MODEL_INDICIES_PER_CELL, RenderModel } from './RenderModel'; @@ -33,7 +33,7 @@ export class WebglRenderer extends Disposable implements IRenderer { private _charAtlasDisposable = this.register(new MutableDisposable()); private _charAtlas: ITextureAtlas | undefined; private _devicePixelRatio: number; - private _observerDisposable: IDisposable | undefined; + private _observerDisposable = this.register(new MutableDisposable()); private _model: RenderModel = new RenderModel(); private _workCell: CellData = new CellData(); @@ -124,10 +124,9 @@ export class WebglRenderer extends Disposable implements IRenderer { this._requestRedrawViewport(); })); - this._observerDisposable = observeDevicePixelDimensions(this._canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); + this._observerDisposable.value = observeDevicePixelDimensions(this._canvas, this._coreBrowserService.window, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); this.register(this._coreBrowserService.onWindowChange(w => { - this._observerDisposable?.dispose(); - this._observerDisposable = observeDevicePixelDimensions(this._canvas, w, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); + this._observerDisposable.value = observeDevicePixelDimensions(this._canvas, w, (w, h) => this._setCanvasDevicePixelDimensions(w, h)); })); this._core.screenElement!.appendChild(this._canvas); @@ -599,11 +598,6 @@ export class WebglRenderer extends Disposable implements IRenderer { const cursorY = this._terminal.buffer.active.cursorY; this._onRequestRedraw.fire({ start: cursorY, end: cursorY }); } - - public override dispose(): void { - this._observerDisposable?.dispose(); - super.dispose(); - } } // TODO: Share impl with core diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index e8d59f7e..c2cb9a05 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -3,7 +3,6 @@ * @license MIT */ -import { IDisposable } from 'common/Types'; import { RenderDebouncer } from 'browser/RenderDebouncer'; import { IRenderDebouncerWithCallback } from 'browser/Types'; import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types'; @@ -25,7 +24,7 @@ export class RenderService extends Disposable implements IRenderService { private _renderer: MutableDisposable = this.register(new MutableDisposable()); private _renderDebouncer: IRenderDebouncerWithCallback; private _pausedResizeTask = new DebouncedIdleTask(); - private _observerDisposable: IDisposable | undefined; + private _observerDisposable = this.register(new MutableDisposable()); private _isPaused: boolean = false; private _needsFullRefresh: boolean = false; @@ -110,11 +109,10 @@ export class RenderService extends Disposable implements IRenderService { private _registerIntersectionObserver(w: Window & typeof globalThis, screenElement: HTMLElement): void { // Detect whether IntersectionObserver is detected and enable renderer pause // and resume based on terminal visibility if so - this._observerDisposable?.dispose(); if ('IntersectionObserver' in w) { const observer = new w.IntersectionObserver(e => this._handleIntersectionChange(e[e.length - 1]), { threshold: 0 }); observer.observe(screenElement); - this._observerDisposable = toDisposable(() => observer.disconnect()); + this._observerDisposable.value = toDisposable(() => observer.disconnect()); } } @@ -283,9 +281,4 @@ export class RenderService extends Disposable implements IRenderService { public clear(): void { this._renderer.value?.clear(); } - - public override dispose(): void { - this._observerDisposable?.dispose(); - super.dispose(); - } } From 7c5ad6b7a9839315f68ca47b718e8b0dcd337ace Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Dec 2023 05:44:55 -0800 Subject: [PATCH 14/76] Remove tracing drawToCache call This was a little noisy and didn't end up being that useful --- src/browser/renderer/shared/TextureAtlas.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index f3f67b8b..4af8685e 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -15,7 +15,6 @@ import { IdleTaskQueue } from 'common/TaskQueue'; import { IColor } from 'common/Types'; import { AttributeData } from 'common/buffer/AttributeData'; import { Attributes, DEFAULT_COLOR, DEFAULT_EXT, UnderlineStyle } from 'common/buffer/Constants'; -import { traceCall } from 'common/services/LogService'; import { IUnicodeService } from 'common/services/Services'; /** @@ -424,7 +423,6 @@ export class TextureAtlas implements ITextureAtlas { return this._config.colors.contrastCache; } - @traceCall private _drawToCache(codeOrChars: number | string, bg: number, fg: number, ext: number, restrictToCellHeight: boolean = false): IRasterizedGlyph { const chars = typeof codeOrChars === 'number' ? String.fromCharCode(codeOrChars) : codeOrChars; From 22f1ce4a115795f2ef12f49e0013eece2b633122 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Dec 2023 08:16:48 -0800 Subject: [PATCH 15/76] Add bg+powerline background blend behavior When a with bg is selected, the cell's regular background will be blended with 50% opacity selectionBackground in order to retain the bg color info as well as getting the color to change and getting decent contrast. This commit also fixes a terrible, nasty bug where the standalone shared renderer tests were causing the terminal to open multiple times and not get the WebGL addon activated the second time. So some tests were actually testing the DOM renderer :o Fixes #4918 --- .../renderer/dom/DomRendererRowFactory.ts | 4 +- .../renderer/shared/CellColorResolver.ts | 102 ++++++++++++++++-- src/browser/renderer/shared/RendererUtils.ts | 2 +- src/browser/renderer/shared/TextureAtlas.ts | 4 +- src/common/Color.test.ts | 21 ++++ src/common/Color.ts | 17 +++ test/playwright/Renderer.test.ts | 5 +- test/playwright/SharedRendererTests.ts | 93 +++++++++++----- 8 files changed, 208 insertions(+), 40 deletions(-) diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 6ab68e7d..50d3eb49 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -11,7 +11,7 @@ import { ICoreService, IDecorationService, IOptionsService } from 'common/servic import { color, rgba } from 'common/Color'; import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; -import { excludeFromContrastRatioDemands } from 'browser/renderer/shared/RendererUtils'; +import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils'; import { AttributeData } from 'common/buffer/AttributeData'; import { WidthCache } from 'browser/renderer/dom/WidthCache'; import { IColorContrastCache } from 'browser/Types'; @@ -458,7 +458,7 @@ export class DomRendererRowFactory { } private _applyMinimumContrast(element: HTMLElement, bg: IColor, fg: IColor, cell: ICellData, bgOverride: IColor | undefined, fgOverride: IColor | undefined): boolean { - if (this._optionsService.rawOptions.minimumContrastRatio === 1 || excludeFromContrastRatioDemands(cell.getCode())) { + if (this._optionsService.rawOptions.minimumContrastRatio === 1 || treatGlyphAsBackgroundColor(cell.getCode())) { return false; } diff --git a/src/browser/renderer/shared/CellColorResolver.ts b/src/browser/renderer/shared/CellColorResolver.ts index 5837a675..50725108 100644 --- a/src/browser/renderer/shared/CellColorResolver.ts +++ b/src/browser/renderer/shared/CellColorResolver.ts @@ -5,6 +5,8 @@ import { Attributes, BgFlags, ExtFlags, FgFlags, NULL_CELL_CODE, UnderlineStyle import { IDecorationService, IOptionsService } from 'common/services/Services'; import { ICellData } from 'common/Types'; import { Terminal } from '@xterm/xterm'; +import { rgba } from 'common/Color'; +import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils'; // Work variables to avoid garbage collection let $fg = 0; @@ -65,11 +67,11 @@ export class CellColorResolver { // Apply decorations on the bottom layer this._decorationService.forEachDecorationAtCell(x, y, 'bottom', d => { if (d.backgroundColorRGB) { - $bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; + $bg = d.backgroundColorRGB.rgba >> 8 & Attributes.RGB_MASK; $hasBg = true; } if (d.foregroundColorRGB) { - $fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; + $fg = d.foregroundColorRGB.rgba >> 8 & Attributes.RGB_MASK; $hasFg = true; } }); @@ -77,10 +79,94 @@ export class CellColorResolver { // Apply the selection color if needed $isSelected = this._selectionRenderModel.isCellSelected(this._terminal, x, y); if ($isSelected) { - $bg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & 0xFFFFFF; + // If the cell has a bg color, retain the color by blending it with the selection color + if ( + (this.result.fg & FgFlags.INVERSE) || + (this.result.bg & Attributes.CM_MASK) !== Attributes.CM_DEFAULT + ) { + // Resolve the standard bg color + if (this.result.fg & FgFlags.INVERSE) { + switch (this.result.fg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: + $bg = this._themeService.colors.ansi[this.result.fg & Attributes.PCOLOR_MASK].rgba; + break; + case Attributes.CM_RGB: + $bg = (this.result.fg & Attributes.RGB_MASK) << 8 | 0xFF; + break; + case Attributes.CM_DEFAULT: + default: + $bg = this._themeService.colors.foreground.rgba; + } + } else { + switch (this.result.bg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: + $bg = this._themeService.colors.ansi[this.result.bg & Attributes.PCOLOR_MASK].rgba; + break; + case Attributes.CM_RGB: + $bg = this.result.bg & Attributes.RGB_MASK << 8 | 0xFF; + break; + // No need to consider default bg color here as it's not possible + } + } + // Blend with selection bg color + $bg = rgba.blend( + $bg, + ((this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba & 0xFFFFFF00) | 0x80 + ) >> 8 & Attributes.RGB_MASK; + } else { + $bg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & Attributes.RGB_MASK; + } $hasBg = true; + + // Apply explicit selection foreground if present if ($colors.selectionForeground) { - $fg = $colors.selectionForeground.rgba >> 8 & 0xFFFFFF; + $fg = $colors.selectionForeground.rgba >> 8 & Attributes.RGB_MASK; + $hasFg = true; + } + + // Overwrite fg as bg if it's a special decorative glyph (eg. powerline) + if (treatGlyphAsBackgroundColor(cell.getCode())) { + // Inverse default background should be treated as transparent + if ( + (this.result.fg & FgFlags.INVERSE) && + (this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT + ) { + $fg = (this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba >> 8 & Attributes.RGB_MASK; + } else { + + if (this.result.fg & FgFlags.INVERSE) { + switch (this.result.bg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: + $fg = this._themeService.colors.ansi[this.result.bg & Attributes.PCOLOR_MASK].rgba; + break; + case Attributes.CM_RGB: + $fg = this.result.bg & Attributes.RGB_MASK << 8 | 0xFF; + break; + // No need to consider default bg color here as it's not possible + } + } else { + switch (this.result.fg & Attributes.CM_MASK) { + case Attributes.CM_P16: + case Attributes.CM_P256: + $fg = this._themeService.colors.ansi[this.result.fg & Attributes.PCOLOR_MASK].rgba; + break; + case Attributes.CM_RGB: + $fg = (this.result.fg & Attributes.RGB_MASK) << 8 | 0xFF; + break; + case Attributes.CM_DEFAULT: + default: + $fg = this._themeService.colors.foreground.rgba; + } + } + + $fg = rgba.blend( + $fg, + ((this._coreBrowserService.isFocused ? $colors.selectionBackgroundOpaque : $colors.selectionInactiveBackgroundOpaque).rgba & 0xFFFFFF00) | 0x80 + ) >> 8 & Attributes.RGB_MASK; + } $hasFg = true; } } @@ -88,11 +174,11 @@ export class CellColorResolver { // Apply decorations on the top layer this._decorationService.forEachDecorationAtCell(x, y, 'top', d => { if (d.backgroundColorRGB) { - $bg = d.backgroundColorRGB.rgba >> 8 & 0xFFFFFF; + $bg = d.backgroundColorRGB.rgba >> 8 & Attributes.RGB_MASK; $hasBg = true; } if (d.foregroundColorRGB) { - $fg = d.foregroundColorRGB.rgba >> 8 & 0xFFFFFF; + $fg = d.foregroundColorRGB.rgba >> 8 & Attributes.RGB_MASK; $hasFg = true; } }); @@ -119,7 +205,7 @@ export class CellColorResolver { if ($hasBg && !$hasFg) { // Resolve bg color type (default color has a different meaning in fg vs bg) if ((this.result.bg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) { - $fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | (($colors.background.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; + $fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | (($colors.background.rgba >> 8 & Attributes.RGB_MASK) & Attributes.RGB_MASK) | Attributes.CM_RGB; } else { $fg = (this.result.fg & ~(Attributes.RGB_MASK | FgFlags.INVERSE | Attributes.CM_MASK)) | this.result.bg & (Attributes.RGB_MASK | Attributes.CM_MASK); } @@ -128,7 +214,7 @@ export class CellColorResolver { if (!$hasBg && $hasFg) { // Resolve bg color type (default color has a different meaning in fg vs bg) if ((this.result.fg & Attributes.CM_MASK) === Attributes.CM_DEFAULT) { - $bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | (($colors.foreground.rgba >> 8 & 0xFFFFFF) & Attributes.RGB_MASK) | Attributes.CM_RGB; + $bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | (($colors.foreground.rgba >> 8 & Attributes.RGB_MASK) & Attributes.RGB_MASK) | Attributes.CM_RGB; } else { $bg = (this.result.bg & ~(Attributes.RGB_MASK | Attributes.CM_MASK)) | this.result.fg & (Attributes.RGB_MASK | Attributes.CM_MASK); } diff --git a/src/browser/renderer/shared/RendererUtils.ts b/src/browser/renderer/shared/RendererUtils.ts index 59b87b0e..9a4bffe0 100644 --- a/src/browser/renderer/shared/RendererUtils.ts +++ b/src/browser/renderer/shared/RendererUtils.ts @@ -27,7 +27,7 @@ function isBoxOrBlockGlyph(codepoint: number): boolean { return 0x2500 <= codepoint && codepoint <= 0x259F; } -export function excludeFromContrastRatioDemands(codepoint: number): boolean { +export function treatGlyphAsBackgroundColor(codepoint: number): boolean { return isPowerlineGlyph(codepoint) || isBoxOrBlockGlyph(codepoint); } diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index f3f67b8b..7ed28b3d 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -6,7 +6,7 @@ import { IColorContrastCache } from 'browser/Types'; import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/shared/Constants'; import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs'; -import { computeNextVariantOffset, excludeFromContrastRatioDemands, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; +import { computeNextVariantOffset, treatGlyphAsBackgroundColor, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types'; import { NULL_COLOR, color, rgba } from 'common/Color'; import { EventEmitter } from 'common/EventEmitter'; @@ -492,7 +492,7 @@ export class TextureAtlas implements ITextureAtlas { const powerlineGlyph = chars.length === 1 && isPowerlineGlyph(chars.charCodeAt(0)); const restrictedPowerlineGlyph = chars.length === 1 && isRestrictedPowerlineGlyph(chars.charCodeAt(0)); - const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, dim, bold, excludeFromContrastRatioDemands(chars.charCodeAt(0))); + const foregroundColor = this._getForegroundColor(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, dim, bold, treatGlyphAsBackgroundColor(chars.charCodeAt(0))); this._tmpCtx.fillStyle = foregroundColor.css; // For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129) diff --git a/src/common/Color.test.ts b/src/common/Color.test.ts index 082c81c3..e250950d 100644 --- a/src/common/Color.test.ts +++ b/src/common/Color.test.ts @@ -271,6 +271,27 @@ describe('Color', () => { }); describe('rgba', () => { + describe('blend', () => { + it('should blend colors based on the alpha channel', () => { + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF00), 0x000000FF); + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF10), 0x101010FF); + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF20), 0x202020FF); + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF30), 0x303030FF); + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF40), 0x404040FF); + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF50), 0x505050FF); + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF60), 0x606060FF); + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF70), 0x707070FF); + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF80), 0x808080FF); + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFF90), 0x909090FF); + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFA0), 0xA0A0A0FF); + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFB0), 0xB0B0B0FF); + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFC0), 0xC0C0C0FF); + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFD0), 0xD0D0D0FF); + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFE0), 0xE0E0E0FF); + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFF0), 0xF0F0F0FF); + assert.deepEqual(rgba.blend(0x000000FF, 0xFFFFFFFF), 0xFFFFFFFF); + }); + }); describe('ensureContrastRatio', () => { it('should return undefined if the color already meets the contrast ratio (black bg)', () => { assert.equal(rgba.ensureContrastRatio(0x000000ff, 0x606060ff, 1), undefined); diff --git a/src/common/Color.ts b/src/common/Color.ts index 9bfed4e6..2291b7be 100644 --- a/src/common/Color.ts +++ b/src/common/Color.ts @@ -245,6 +245,23 @@ export namespace rgb { * Helper functions where the source type is "rgba" (number: 0xrrggbbaa). */ export namespace rgba { + export function blend(bg: number, fg: number): number { + $a = (fg & 0xFF) / 0xFF; + if ($a === 1) { + return fg; + } + const fgR = (fg >> 24) & 0xFF; + const fgG = (fg >> 16) & 0xFF; + const fgB = (fg >> 8) & 0xFF; + const bgR = (bg >> 24) & 0xFF; + const bgG = (bg >> 16) & 0xFF; + const bgB = (bg >> 8) & 0xFF; + $r = bgR + Math.round((fgR - bgR) * $a); + $g = bgG + Math.round((fgG - bgG) * $a); + $b = bgB + Math.round((fgB - bgB) * $a); + return channels.toRgba($r, $g, $b); + } + /** * Given a foreground color and a background color, either increase or reduce the luminance of the * foreground color until the specified contrast ratio is met. If pure white or black is hit diff --git a/test/playwright/Renderer.test.ts b/test/playwright/Renderer.test.ts index 77bf7941..1381abea 100644 --- a/test/playwright/Renderer.test.ts +++ b/test/playwright/Renderer.test.ts @@ -8,7 +8,10 @@ import { ITestContext, createTestContext, openTerminal } from './TestUtils'; import { ISharedRendererTestContext, injectSharedRendererTestsStandalone, injectSharedRendererTests } from './SharedRendererTests'; let ctx: ITestContext; -const ctxWrapper: ISharedRendererTestContext = { value: undefined } as any; +const ctxWrapper: ISharedRendererTestContext = { + value: undefined, + skipCanvasExceptions: true +} as any; test.beforeAll(async ({ browser }) => { ctx = await createTestContext(browser); ctxWrapper.value = ctx; diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index aa747277..d566acd9 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -11,6 +11,7 @@ import { ITestContext, MaybeAsync, openTerminal, pollFor, pollForApproximate } f export interface ISharedRendererTestContext { value: ITestContext; skipCanvasExceptions?: boolean; + skipDomExceptions?: boolean; } export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void { @@ -945,7 +946,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 255, 0, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [255, 0, 0, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0, 255, 0, 255]); - await ctx.value.page.evaluate(`window.term.selectAll()`); + await ctx.value.proxy.selectAll(); frameDetails = undefined; // Selection only cell needs to be first to ensure renderer has kicked in await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 255, 255]); @@ -965,7 +966,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void // Check both the cursor line and another line await ctx.value.proxy.writeln('_ '); await ctx.value.proxy.write('_ '); - await ctx.value.page.evaluate(`window.term.selectAll()`); + await ctx.value.proxy.selectAll(); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [128, 0, 0, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [128, 0, 0, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [128, 0, 0, 255]); @@ -980,6 +981,44 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void }); }); + (ctx.skipCanvasExceptions || ctx.skipDomExceptions ? test.describe.skip : test.describe)('selection blending', () => { + test('background', async () => { + const theme: ITheme = { + red: '#CC0000', + selectionBackground: '#FFFFFF' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.focus(); + await ctx.value.proxy.writeln('\x1b[41m red bg'); + await ctx.value.proxy.writeln('\x1b[7m inverse'); + await ctx.value.proxy.writeln('\x1b[31;7m red fg inverse'); + await ctx.value.proxy.selectAll(); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [230,128,128,255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [255,255,255,255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 3), [230,128,128,255]); + }); + test('powerline decorative symbols', async () => { + const theme: ITheme = { + red: '#CC0000', + green: '#00CC00', + selectionBackground: '#FFFFFF' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await ctx.value.proxy.focus(); + await ctx.value.proxy.writeln('\u{E0B4} plain\x1b[0m'); + await ctx.value.proxy.writeln('\x1b[31;42m\u{E0B4} red fg green bg\x1b[0m'); + await ctx.value.proxy.writeln('\x1b[32;41m\u{E0B4} green fg red bg\x1b[0m'); + await ctx.value.proxy.writeln('\x1b[31;42;7m\u{E0B4} red fg green bg inverse\x1b[0m'); + await ctx.value.proxy.writeln('\x1b[32;41;7m\u{E0B4} green fg red bg inverse\x1b[0m'); + await ctx.value.proxy.selectAll(); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255,255,255,255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [230, 128, 128, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 3), [128, 230, 128, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 4), [128, 230, 128, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 5), [230, 128, 128, 255]); + }); + }); + test.describe('allowTransparency', async () => { test.beforeEach(() => ctx.value.page.evaluate(`term.options.allowTransparency = true`)); @@ -1003,7 +1042,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); const data = `\x1b[7m■\x1b[0m`; await ctx.value.proxy.write( data); - await ctx.value.page.evaluate(`window.term.selectAll()`); + await ctx.value.proxy.selectAll(); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [255, 0, 0, 255]); }); }); @@ -1206,30 +1245,32 @@ enum CellColorPosition { * treatment. */ export function injectSharedRendererTestsStandalone(ctx: ISharedRendererTestContext): void { - test.beforeEach(async () => { - // Recreate terminal - await openTerminal(ctx.value); - ctx.value.page.evaluate(` - window.term.options.minimumContrastRatio = 1; - window.term.options.allowTransparency = false; - window.term.options.theme = undefined; - `); - // Clear the cached screenshot before each test - frameDetails = undefined; - }); - test.describe('regression tests', () => { - test('#4790: cursor should not be displayed before focusing', async () => { - const theme: ITheme = { - cursor: '#0000FF' - }; - await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]); - await ctx.value.proxy.focus(); + test.describe('standalone tests', () => { + test.beforeEach(async () => { + // Recreate terminal + await openTerminal(ctx.value); + ctx.value.page.evaluate(` + window.term.options.minimumContrastRatio = 1; + window.term.options.allowTransparency = false; + window.term.options.theme = undefined; + `); + // Clear the cached screenshot before each test frameDetails = undefined; - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 255, 255]); - await ctx.value.proxy.blur(); - frameDetails = undefined; - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]); + }); + test.describe('regression tests', () => { + test('#4790: cursor should not be displayed before focusing', async () => { + const theme: ITheme = { + cursor: '#0000FF' + }; + await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]); + await ctx.value.proxy.focus(); + frameDetails = undefined; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 255, 255]); + await ctx.value.proxy.blur(); + frameDetails = undefined; + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]); + }); }); }); } From 7bee0018684550d1cf8271462b884507a19b56a1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Dec 2023 08:18:44 -0800 Subject: [PATCH 16/76] Ensure addons are loaded in standalone tests --- addons/addon-canvas/test/CanvasRenderer.test.ts | 7 ++++++- test/playwright/Renderer.test.ts | 2 +- test/playwright/SharedRendererTests.ts | 5 +++-- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/addons/addon-canvas/test/CanvasRenderer.test.ts b/addons/addon-canvas/test/CanvasRenderer.test.ts index 2782081c..c8b35c7a 100644 --- a/addons/addon-canvas/test/CanvasRenderer.test.ts +++ b/addons/addon-canvas/test/CanvasRenderer.test.ts @@ -28,5 +28,10 @@ test.describe('Canvas Renderer Integration Tests', () => { test.skip(({ browserName }) => browserName === 'webkit'); injectSharedRendererTests(ctxWrapper); - injectSharedRendererTestsStandalone(ctxWrapper); + injectSharedRendererTestsStandalone(ctxWrapper, async () => { + await ctx.page.evaluate(` + window.addon = new window.CanvasAddon(true); + window.term.loadAddon(window.addon); + `); + }); }); diff --git a/test/playwright/Renderer.test.ts b/test/playwright/Renderer.test.ts index 1381abea..0d0159bd 100644 --- a/test/playwright/Renderer.test.ts +++ b/test/playwright/Renderer.test.ts @@ -21,5 +21,5 @@ test.afterAll(async () => await ctx.page.close()); test.describe('DOM Renderer Integration Tests', () => { injectSharedRendererTests(ctxWrapper); - injectSharedRendererTestsStandalone(ctxWrapper); + injectSharedRendererTestsStandalone(ctxWrapper, () => {}); }); diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index d566acd9..0b7dac1c 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -1244,16 +1244,17 @@ enum CellColorPosition { * This is much slower than just calling `Terminal.reset` but testing some features needs this * treatment. */ -export function injectSharedRendererTestsStandalone(ctx: ISharedRendererTestContext): void { +export function injectSharedRendererTestsStandalone(ctx: ISharedRendererTestContext, setupCb: () => Promise | void): void { test.describe('standalone tests', () => { test.beforeEach(async () => { // Recreate terminal await openTerminal(ctx.value); - ctx.value.page.evaluate(` + await ctx.value.page.evaluate(` window.term.options.minimumContrastRatio = 1; window.term.options.allowTransparency = false; window.term.options.theme = undefined; `); + await setupCb(); // Clear the cached screenshot before each test frameDetails = undefined; }); From 66b426311e2ed25bf7f3dab086794fe48367e832 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Dec 2023 08:21:18 -0800 Subject: [PATCH 17/76] Fix compile --- addons/addon-webgl/test/WebglRenderer.test.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/addons/addon-webgl/test/WebglRenderer.test.ts b/addons/addon-webgl/test/WebglRenderer.test.ts index 4b73c08b..ad0e7e73 100644 --- a/addons/addon-webgl/test/WebglRenderer.test.ts +++ b/addons/addon-webgl/test/WebglRenderer.test.ts @@ -29,5 +29,10 @@ test.describe('WebGL Renderer Integration Tests', async () => { } injectSharedRendererTests(ctxWrapper); - injectSharedRendererTestsStandalone(ctxWrapper); + injectSharedRendererTestsStandalone(ctxWrapper, async () => { + await ctx.page.evaluate(` + window.addon = new window.WebglAddon(true); + window.term.loadAddon(window.addon); + `); + }); }); From 9b62849bb8566e5ee796869048a06775c38719e7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Dec 2023 08:23:21 -0800 Subject: [PATCH 18/76] Fix lint --- addons/addon-webgl/test/WebglRenderer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/addon-webgl/test/WebglRenderer.test.ts b/addons/addon-webgl/test/WebglRenderer.test.ts index ad0e7e73..d5f62fda 100644 --- a/addons/addon-webgl/test/WebglRenderer.test.ts +++ b/addons/addon-webgl/test/WebglRenderer.test.ts @@ -30,7 +30,7 @@ test.describe('WebGL Renderer Integration Tests', async () => { injectSharedRendererTests(ctxWrapper); injectSharedRendererTestsStandalone(ctxWrapper, async () => { - await ctx.page.evaluate(` + await ctx.page.evaluate(` window.addon = new window.WebglAddon(true); window.term.loadAddon(window.addon); `); From 78a945ae9efc527c6b517c2419e7a349cfe25ecd Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Dec 2023 08:34:16 -0800 Subject: [PATCH 19/76] Suppress test failure --- test/playwright/SharedRendererTests.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index 0b7dac1c..7291f42f 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -1180,7 +1180,8 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 0, 0, 255]); }); - (ctx.skipCanvasExceptions ? test.skip : test)('#4759: minimum contrast ratio should be respected on selected inverse text', async () => { + // HACK: It's not clear why DOM is failing here + (ctx.skipCanvasExceptions || ctx.skipDomExceptions ? test.skip : test)('#4759: minimum contrast ratio should be respected on selected inverse text', async () => { const theme: ITheme = { foreground: '#777777', background: '#555555', From c0fac5e5f1c386024427ca7bdd7eb940e2291d04 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Dec 2023 08:38:55 -0800 Subject: [PATCH 20/76] Fix test suppression --- test/playwright/SharedRendererTests.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index 7291f42f..005f11da 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -1165,7 +1165,8 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 0, 0, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 3, 1), [0, 0, 0, 255]); }); - test('#4759: minimum contrast ratio should be respected on inverse text', async () => { + // HACK: It's not clear why DOM is failing here + (ctx.skipDomExceptions ? test.skip : test)('#4759: minimum contrast ratio should be respected on inverse text', async () => { const theme: ITheme = { foreground: '#aaaaaa', background: '#333333' @@ -1180,8 +1181,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]); await pollFor(ctx.value.page, () => getCellColor(ctx.value, 2, 1), [0, 0, 0, 255]); }); - // HACK: It's not clear why DOM is failing here - (ctx.skipCanvasExceptions || ctx.skipDomExceptions ? test.skip : test)('#4759: minimum contrast ratio should be respected on selected inverse text', async () => { + (ctx.skipCanvasExceptions ? test.skip : test)('#4759: minimum contrast ratio should be respected on selected inverse text', async () => { const theme: ITheme = { foreground: '#777777', background: '#555555', From b843e0fc8e5f7e0e01e526cc7100fa6a9ba9a190 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Dec 2023 08:44:14 -0800 Subject: [PATCH 21/76] Pass through dom skip exceptions flag --- test/playwright/Renderer.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/playwright/Renderer.test.ts b/test/playwright/Renderer.test.ts index 0d0159bd..119832d6 100644 --- a/test/playwright/Renderer.test.ts +++ b/test/playwright/Renderer.test.ts @@ -10,7 +10,7 @@ import { ISharedRendererTestContext, injectSharedRendererTestsStandalone, inject let ctx: ITestContext; const ctxWrapper: ISharedRendererTestContext = { value: undefined, - skipCanvasExceptions: true + skipDomExceptions: true } as any; test.beforeAll(async ({ browser }) => { ctx = await createTestContext(browser); From a4505ed724f5a0041d69d86067a0d32b85e6d094 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Dec 2023 08:47:54 -0800 Subject: [PATCH 22/76] Skip another test on canvas --- test/playwright/SharedRendererTests.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index 005f11da..657fba44 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -1131,7 +1131,7 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void }); test.describe('regression tests', () => { - test('#4736: inactive selection background should replace regular cell background color', async () => { + (ctx.skipCanvasExceptions ? test.skip : test)('#4736: inactive selection background should replace regular cell background color', async () => { const theme: ITheme = { selectionBackground: '#FF0000', selectionInactiveBackground: '#0000FF' From 56a6a010ab3007043cc6b8cf0839cbfd81b275b8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Dec 2023 07:28:26 -0800 Subject: [PATCH 23/76] Fix crosshair cursor now working in some embedders This was not happening in the demo because the xterm instance is almost always focused. See microsoft/vscode#199848 --- src/browser/Terminal.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 7e1f9014..01dfbc89 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -261,11 +261,10 @@ export class Terminal extends CoreTerminal implements ITerminal { /** * Binds the desired focus behavior on a given terminal object. */ - private _handleTextAreaFocus(ev: KeyboardEvent): void { + private _handleTextAreaFocus(ev: FocusEvent): void { if (this.coreService.decPrivateModes.sendFocus) { this.coreService.triggerDataEvent(C0.ESC + '[I'); } - this.updateCursorStyle(ev); this.element!.classList.add('focus'); this._showCursor(); this._onFocus.fire(); @@ -429,6 +428,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this.screenElement = this._document.createElement('div'); this.screenElement.classList.add('xterm-screen'); + this.register(addDisposableDomListener(this.screenElement, 'mousemove', (ev: MouseEvent) => this.updateCursorStyle(ev))); // Create the container that will hold helpers like the textarea for // capturing DOM Events. Then produce the helpers. this._helperContainer = this._document.createElement('div'); @@ -459,11 +459,10 @@ export class Terminal extends CoreTerminal implements ITerminal { )); this._instantiationService.setService(ICoreBrowserService, this._coreBrowserService); - this.register(addDisposableDomListener(this.textarea, 'focus', (ev: KeyboardEvent) => this._handleTextAreaFocus(ev))); + this.register(addDisposableDomListener(this.textarea, 'focus', (ev: FocusEvent) => this._handleTextAreaFocus(ev))); this.register(addDisposableDomListener(this.textarea, 'blur', () => this._handleTextAreaBlur())); this._helperContainer.appendChild(this.textarea); - this._charSizeService = this._instantiationService.createInstance(CharSizeService, this._document, this._helperContainer); this._instantiationService.setService(ICharSizeService, this._charSizeService); @@ -855,7 +854,7 @@ export class Terminal extends CoreTerminal implements ITerminal { /** * Change the cursor style for different selection modes */ - public updateCursorStyle(ev: KeyboardEvent): void { + public updateCursorStyle(ev: KeyboardEvent | MouseEvent): void { if (this._selectionService?.shouldColumnSelect(ev)) { this.element!.classList.add('column-select'); } else { From 8733d2cfa24d5d81d3f3b398d413be54537cbe1a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Dec 2023 08:21:35 -0800 Subject: [PATCH 24/76] Separate link provider service from linkifier --- addons/addon-canvas/src/CanvasAddon.ts | 2 +- addons/addon-webgl/src/WebglRenderer.ts | 2 +- src/browser/Linkifier2.test.ts | 3 +- src/browser/Linkifier2.ts | 29 +++++-------------- src/browser/OscLinkProvider.ts | 3 +- src/browser/Terminal.ts | 32 +++++++++++++-------- src/browser/TestUtils.test.ts | 2 +- src/browser/Types.d.ts | 9 ++---- src/browser/services/LinkProviderService.ts | 28 ++++++++++++++++++ src/browser/services/Services.ts | 13 ++++++++- src/common/CoreTerminal.ts | 1 + 11 files changed, 77 insertions(+), 47 deletions(-) create mode 100644 src/browser/services/LinkProviderService.ts diff --git a/addons/addon-canvas/src/CanvasAddon.ts b/addons/addon-canvas/src/CanvasAddon.ts index 7f7f679b..4d32b021 100644 --- a/addons/addon-canvas/src/CanvasAddon.ts +++ b/addons/addon-canvas/src/CanvasAddon.ts @@ -37,7 +37,7 @@ export class CanvasAddon extends Disposable implements ITerminalAddon , ICanvasA const coreService = core.coreService; const optionsService = core.optionsService; const screenElement = core.screenElement!; - const linkifier = core.linkifier2; + const linkifier = core.linkifier!; const unsafeCore = core as any; const bufferService: IBufferService = unsafeCore._bufferService; diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index f2f2c83e..3a01e244 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -81,7 +81,7 @@ export class WebglRenderer extends Disposable implements IRenderer { this._core = (this._terminal as any)._core; this._renderLayers = [ - new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier2, this._coreBrowserService, _optionsService, this._themeService) + new LinkRenderLayer(this._core.screenElement!, 2, this._terminal, this._core.linkifier!, this._coreBrowserService, _optionsService, this._themeService) ]; this.dimensions = createRenderDimensions(); this._devicePixelRatio = this._coreBrowserService.dpr; diff --git a/src/browser/Linkifier2.test.ts b/src/browser/Linkifier2.test.ts index 0af74c28..569aba84 100644 --- a/src/browser/Linkifier2.test.ts +++ b/src/browser/Linkifier2.test.ts @@ -8,6 +8,7 @@ import { IBufferService } from 'common/services/Services'; import { Linkifier2 } from 'browser/Linkifier2'; import { MockBufferService } from 'common/TestUtils.test'; import { ILink } from 'browser/Types'; +import { LinkProviderService } from 'browser/services/LinkProviderService'; class TestLinkifier2 extends Linkifier2 { public set currentLink(link: any) { @@ -44,7 +45,7 @@ describe('Linkifier2', () => { beforeEach(() => { bufferService = new MockBufferService(100, 10); - linkifier = new TestLinkifier2(bufferService); + linkifier = new TestLinkifier2(bufferService, new LinkProviderService()); linkifier.currentLink = { link, state: { diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index f7b1f605..3bc38f8b 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -4,18 +4,17 @@ */ import { addDisposableDomListener } from 'browser/Lifecycle'; -import { IBufferCellPosition, ILink, ILinkDecorations, ILinkProvider, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types'; +import { IBufferCellPosition, ILink, ILinkDecorations, ILinkWithState, ILinkifier2, ILinkifierEvent } from 'browser/Types'; import { EventEmitter } from 'common/EventEmitter'; import { Disposable, disposeArray, getDisposeArrayDisposable, toDisposable } from 'common/Lifecycle'; import { IDisposable } from 'common/Types'; import { IBufferService } from 'common/services/Services'; -import { IMouseService, IRenderService } from './services/Services'; +import { ILinkProviderService, IMouseService, IRenderService } from './services/Services'; export class Linkifier2 extends Disposable implements ILinkifier2 { private _element: HTMLElement | undefined; private _mouseService: IMouseService | undefined; private _renderService: IRenderService | undefined; - private _linkProviders: ILinkProvider[] = []; public get currentLink(): ILinkWithState | undefined { return this._currentLink; } protected _currentLink: ILinkWithState | undefined; private _mouseDownLink: ILinkWithState | undefined; @@ -33,14 +32,14 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { public readonly onHideLinkUnderline = this._onHideLinkUnderline.event; constructor( - @IBufferService private readonly _bufferService: IBufferService + @IBufferService private readonly _bufferService: IBufferService, + @ILinkProviderService private readonly _linkProviderService: ILinkProviderService ) { super(); this.register(getDisposeArrayDisposable(this._linkCacheDisposables)); this.register(toDisposable(() => { this._lastMouseEvent = undefined; // Clear out link providers as they could easily cause an embedder memory leak - this._linkProviders.length = 0; this._activeProviderReplies?.clear(); })); // Listen to resize to catch the case where it's resized and the cursor is out of the viewport. @@ -50,20 +49,6 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { })); } - public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { - this._linkProviders.push(linkProvider); - return { - dispose: () => { - // Remove the link provider from the list - const providerIndex = this._linkProviders.indexOf(linkProvider); - - if (providerIndex !== -1) { - this._linkProviders.splice(providerIndex, 1); - } - } - }; - } - public attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void { this._element = element; this._mouseService = mouseService; @@ -145,7 +130,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { let linkProvided = false; // There is no link cached, so ask for one - for (const [i, linkProvider] of this._linkProviders.entries()) { + for (const [i, linkProvider] of this._linkProviderService.linkProviders.entries()) { if (useLineCache) { const existingReply = this._activeProviderReplies?.get(i); // If there isn't a reply, the provider hasn't responded yet. @@ -167,7 +152,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { // If all providers have responded, remove lower priority links that intersect ranges of // higher priority links - if (this._activeProviderReplies?.size === this._linkProviders.length) { + if (this._activeProviderReplies?.size === this._linkProviderService.linkProviders.length) { this._removeIntersectingLinks(position.y, this._activeProviderReplies); } }); @@ -223,7 +208,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { } // Check if all the providers have responded - if (this._activeProviderReplies.size === this._linkProviders.length && !linkProvided) { + if (this._activeProviderReplies.size === this._linkProviderService.linkProviders.length && !linkProvided) { // Respect the order of the link providers for (let j = 0; j < this._activeProviderReplies.size; j++) { const currentLink = this._activeProviderReplies.get(j)?.find(link => this._linkAtPosition(link.link, position)); diff --git a/src/browser/OscLinkProvider.ts b/src/browser/OscLinkProvider.ts index fee1ae7c..a079fe67 100644 --- a/src/browser/OscLinkProvider.ts +++ b/src/browser/OscLinkProvider.ts @@ -3,7 +3,8 @@ * @license MIT */ -import { IBufferRange, ILink, ILinkProvider } from 'browser/Types'; +import { IBufferRange, ILink } from 'browser/Types'; +import { ILinkProvider } from 'browser/services/Services'; import { CellData } from 'common/buffer/CellData'; import { IBufferService, IOptionsService, IOscLinkService } from 'common/services/Services'; diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 01dfbc89..24600131 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -39,7 +39,7 @@ import { CoreBrowserService } from 'browser/services/CoreBrowserService'; import { MouseService } from 'browser/services/MouseService'; import { RenderService } from 'browser/services/RenderService'; import { SelectionService } from 'browser/services/SelectionService'; -import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services'; +import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services'; import { ThemeService } from 'browser/services/ThemeService'; import { color, rgba } from 'common/Color'; import { CoreTerminal } from 'common/CoreTerminal'; @@ -57,6 +57,7 @@ import { IDecorationService } from 'common/services/Services'; import { IDecoration, IDecorationOptions, IDisposable, ILinkProvider, IMarker } from '@xterm/xterm'; import { WindowsOptionsReportType } from '../common/InputHandler'; import { AccessibilityManager } from './AccessibilityManager'; +import { LinkProviderService } from 'browser/services/LinkProviderService'; export class Terminal extends CoreTerminal implements ITerminal { public textarea: HTMLTextAreaElement | undefined; @@ -69,6 +70,7 @@ export class Terminal extends CoreTerminal implements ITerminal { private _helperContainer: HTMLElement | undefined; private _compositionView: HTMLElement | undefined; + public linkifier: ILinkifier2 | undefined; private _overviewRulerRenderer: OverviewRulerRenderer | undefined; public browser: IBrowser = Browser as any; @@ -76,8 +78,11 @@ export class Terminal extends CoreTerminal implements ITerminal { private _customKeyEventHandler: CustomKeyEventHandler | undefined; private _customWheelEventHandler: CustomWheelEventHandler | undefined; - // browser services + // Browser services private _decorationService: DecorationService; + private _linkProviderService: ILinkProviderService; + + // Optional browser services private _charSizeService: ICharSizeService | undefined; private _coreBrowserService: ICoreBrowserService | undefined; private _mouseService: IMouseService | undefined; @@ -113,7 +118,6 @@ export class Terminal extends CoreTerminal implements ITerminal { */ private _unprocessedDeadKey: boolean = false; - public linkifier2: ILinkifier2; public viewport: IViewport | undefined; private _compositionHelper: ICompositionHelper | undefined; private _accessibilityManager: MutableDisposable = this.register(new MutableDisposable()); @@ -149,10 +153,11 @@ export class Terminal extends CoreTerminal implements ITerminal { this._setup(); - this.linkifier2 = this.register(this._instantiationService.createInstance(Linkifier2)); - this.linkifier2.registerLinkProvider(this._instantiationService.createInstance(OscLinkProvider)); this._decorationService = this._instantiationService.createInstance(DecorationService); this._instantiationService.setService(IDecorationService, this._decorationService); + this._linkProviderService = this._instantiationService.createInstance(LinkProviderService); + this._instantiationService.setService(ILinkProviderService, this._linkProviderService); + this._linkProviderService.registerLinkProvider(this._instantiationService.createInstance(OscLinkProvider)); // Setup InputHandler listeners this.register(this._inputHandler.onRequestBell(() => this._onBell.fire())); @@ -482,6 +487,13 @@ export class Terminal extends CoreTerminal implements ITerminal { this._compositionHelper = this._instantiationService.createInstance(CompositionHelper, this.textarea, this._compositionView); this._helperContainer.appendChild(this._compositionView); + this._mouseService = this._instantiationService.createInstance(MouseService); + this._instantiationService.setService(IMouseService, this._mouseService); + + this.linkifier = this.register(this._instantiationService.createInstance(Linkifier2)); + // TODO: Move into ctor + this.linkifier.attachToDom(this.screenElement, this._mouseService, this._renderService); + // Performance: Add viewport and helper elements from the fragment this.element.appendChild(fragment); @@ -493,9 +505,6 @@ export class Terminal extends CoreTerminal implements ITerminal { this._renderService.setRenderer(this._createRenderer()); } - this._mouseService = this._instantiationService.createInstance(MouseService); - this._instantiationService.setService(IMouseService, this._mouseService); - this.viewport = this._instantiationService.createInstance(Viewport, this._viewportElement, this._viewportScrollArea); this.viewport.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent, ScrollSource.VIEWPORT)), this.register(this._inputHandler.onRequestSyncScrollBar(() => this.viewport!.syncScrollArea())); @@ -513,7 +522,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this._selectionService = this.register(this._instantiationService.createInstance(SelectionService, this.element, this.screenElement, - this.linkifier2 + this.linkifier )); this._instantiationService.setService(ISelectionService, this._selectionService); this.register(this._selectionService.onRequestScrollLines(e => this.scrollLines(e.amount, e.suppressScrollEvent))); @@ -533,7 +542,6 @@ export class Terminal extends CoreTerminal implements ITerminal { })); this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService!.refresh())); - this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService); this.register(this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement)); this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.handleMouseDown(e))); @@ -575,7 +583,7 @@ export class Terminal extends CoreTerminal implements ITerminal { } private _createRenderer(): IRenderer { - return this._instantiationService.createInstance(DomRenderer, this, this._document!, this.element!, this.screenElement!, this._viewportElement!, this._helperContainer!, this.linkifier2); + return this._instantiationService.createInstance(DomRenderer, this, this._document!, this.element!, this.screenElement!, this._viewportElement!, this._helperContainer!, this.linkifier!); } /** @@ -894,7 +902,7 @@ export class Terminal extends CoreTerminal implements ITerminal { } public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { - return this.linkifier2.registerLinkProvider(linkProvider); + return this._linkProviderService.registerLinkProvider(linkProvider); } public registerCharacterJoiner(handler: CharacterJoinerHandler): number { diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index a969ec2c..59cc773a 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -48,6 +48,7 @@ export class MockTerminal implements ITerminal { public onRender!: IEvent<{ start: number, end: number }>; public onResize!: IEvent<{ cols: number, rows: number }>; public markers!: IMarker[]; + public linkifier: ILinkifier2 | undefined; public coreMouseService!: ICoreMouseService; public coreService!: ICoreService; public optionsService!: IOptionsService; @@ -151,7 +152,6 @@ export class MockTerminal implements ITerminal { } public bracketedPasteMode!: boolean; public renderer!: IRenderer; - public linkifier2!: ILinkifier2; public isFocused!: boolean; public options!: Required; public element!: HTMLElement; diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index d1ad5f87..d27c0ee7 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -7,7 +7,7 @@ import { IEvent } from 'common/EventEmitter'; import { CharData, IColor, ICoreTerminal, ITerminalOptions } from 'common/Types'; import { IBuffer } from 'common/buffer/Types'; import { IDisposable, Terminal as ITerminalApi } from '@xterm/xterm'; -import { IMouseService, IRenderService } from './services/Services'; +import { IMouseService, IRenderService } from 'browser/services/Services'; /** * A portion of the public API that are implemented identially internally and simply passed through. @@ -18,9 +18,9 @@ export interface ITerminal extends InternalPassthroughApis, ICoreTerminal { screenElement: HTMLElement | undefined; browser: IBrowser; buffer: IBuffer; + linkifier: ILinkifier2 | undefined; viewport: IViewport | undefined; options: Required; - linkifier2: ILinkifier2; onBlur: IEvent; onFocus: IEvent; @@ -130,11 +130,6 @@ export interface ILinkifier2 extends IDisposable { readonly currentLink: ILinkWithState | undefined; attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void; - registerLinkProvider(linkProvider: ILinkProvider): IDisposable; -} - -interface ILinkProvider { - provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void; } interface ILink { diff --git a/src/browser/services/LinkProviderService.ts b/src/browser/services/LinkProviderService.ts new file mode 100644 index 00000000..2590f24b --- /dev/null +++ b/src/browser/services/LinkProviderService.ts @@ -0,0 +1,28 @@ +import { ILinkProvider, ILinkProviderService } from 'browser/services/Services'; +import { Disposable, toDisposable } from 'common/Lifecycle'; +import { IDisposable } from 'common/Types'; + +export class LinkProviderService extends Disposable implements ILinkProviderService { + declare public serviceBrand: undefined; + + public readonly linkProviders: ILinkProvider[] = []; + + constructor() { + super(); + this.register(toDisposable(() => this.linkProviders.length = 0)); + } + + public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { + this.linkProviders.push(linkProvider); + return { + dispose: () => { + // Remove the link provider from the list + const providerIndex = this.linkProviders.indexOf(linkProvider); + + if (providerIndex !== -1) { + this.linkProviders.splice(providerIndex, 1); + } + } + }; + } +} diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index 5c14fa8a..a82eabd0 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent } from 'common/EventEmitter'; import { IRenderDimensions, IRenderer } from 'browser/renderer/shared/Types'; -import { IColorSet, ReadonlyColorSet } from 'browser/Types'; +import { IColorSet, ILink, ReadonlyColorSet } from 'browser/Types'; import { ISelectionRedrawRequestEvent as ISelectionRequestRedrawEvent, ISelectionRequestScrollLinesEvent } from 'browser/selection/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; import { AllColorIndex, IDisposable } from 'common/Types'; @@ -145,3 +145,14 @@ export interface IThemeService { */ modifyColors(callback: (colors: IColorSet) => void): void; } + + +export const ILinkProviderService = createDecorator('LinkProviderService'); +export interface ILinkProviderService extends IDisposable { + serviceBrand: undefined; + readonly linkProviders: ReadonlyArray; + registerLinkProvider(linkProvider: ILinkProvider): IDisposable; +} +export interface ILinkProvider { + provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void; +} diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 47f77406..1789daf8 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -120,6 +120,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._oscLinkService = this._instantiationService.createInstance(OscLinkService); this._instantiationService.setService(IOscLinkService, this._oscLinkService); + // Register input handler and handle/forward events this._inputHandler = this.register(new InputHandler(this._bufferService, this._charsetService, this.coreService, this._logService, this.optionsService, this._oscLinkService, this.coreMouseService, this.unicodeService)); this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed)); From de41c9f306ffcc4180855764c31e7086b90879b8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Dec 2023 08:29:28 -0800 Subject: [PATCH 25/76] Make linkifier always attached to dom --- src/browser/Linkifier2.test.ts | 4 +-- src/browser/Linkifier2.ts | 65 ++++++++++++++-------------------- src/browser/Terminal.ts | 6 ++-- src/browser/Types.d.ts | 3 -- 4 files changed, 30 insertions(+), 48 deletions(-) diff --git a/src/browser/Linkifier2.test.ts b/src/browser/Linkifier2.test.ts index 569aba84..07680a23 100644 --- a/src/browser/Linkifier2.test.ts +++ b/src/browser/Linkifier2.test.ts @@ -5,7 +5,7 @@ import { assert } from 'chai'; import { IBufferService } from 'common/services/Services'; -import { Linkifier2 } from 'browser/Linkifier2'; +import { Linkifier2 } from './Linkifier2'; import { MockBufferService } from 'common/TestUtils.test'; import { ILink } from 'browser/Types'; import { LinkProviderService } from 'browser/services/LinkProviderService'; @@ -45,7 +45,7 @@ describe('Linkifier2', () => { beforeEach(() => { bufferService = new MockBufferService(100, 10); - linkifier = new TestLinkifier2(bufferService, new LinkProviderService()); + linkifier = new TestLinkifier2(null!, null!, null!, bufferService, new LinkProviderService()); linkifier.currentLink = { link, state: { diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier2.ts index 3bc38f8b..4c0fc9dc 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier2.ts @@ -12,9 +12,6 @@ import { IBufferService } from 'common/services/Services'; import { ILinkProviderService, IMouseService, IRenderService } from './services/Services'; export class Linkifier2 extends Disposable implements ILinkifier2 { - private _element: HTMLElement | undefined; - private _mouseService: IMouseService | undefined; - private _renderService: IRenderService | undefined; public get currentLink(): ILinkWithState | undefined { return this._currentLink; } protected _currentLink: ILinkWithState | undefined; private _mouseDownLink: ILinkWithState | undefined; @@ -32,6 +29,9 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { public readonly onHideLinkUnderline = this._onHideLinkUnderline.event; constructor( + private readonly _element: HTMLElement, + @IMouseService private readonly _mouseService: IMouseService, + @IRenderService private readonly _renderService: IRenderService, @IBufferService private readonly _bufferService: IBufferService, @ILinkProviderService private readonly _linkProviderService: ILinkProviderService ) { @@ -47,13 +47,6 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { this._clearCurrentLink(); this._wasResized = true; })); - } - - public attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void { - this._element = element; - this._mouseService = mouseService; - this._renderService = renderService; - this.register(addDisposableDomListener(this._element, 'mouseleave', () => { this._isMouseOut = true; this._clearCurrentLink(); @@ -66,10 +59,6 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { private _handleMouseMove(event: MouseEvent): void { this._lastMouseEvent = event; - if (!this._element || !this._mouseService) { - return; - } - const position = this._positionFromMouseEvent(event, this._element, this._mouseService); if (!position) { return; @@ -228,7 +217,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { } private _handleMouseUp(event: MouseEvent): void { - if (!this._element || !this._mouseService || !this._currentLink) { + if (!this._currentLink) { return; } @@ -243,7 +232,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { } private _clearCurrentLink(startRow?: number, endRow?: number): void { - if (!this._element || !this._currentLink || !this._lastMouseEvent) { + if (!this._currentLink || !this._lastMouseEvent) { return; } @@ -256,7 +245,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { } private _handleNewLink(linkWithState: ILinkWithState): void { - if (!this._element || !this._lastMouseEvent || !this._mouseService) { + if (!this._lastMouseEvent) { return; } @@ -287,7 +276,7 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { if (this._currentLink?.state && this._currentLink.state.decorations.pointerCursor !== v) { this._currentLink.state.decorations.pointerCursor = v; if (this._currentLink.state.isHovered) { - this._element?.classList.toggle('xterm-cursor-pointer', v); + this._element.classList.toggle('xterm-cursor-pointer', v); } } } @@ -307,29 +296,27 @@ export class Linkifier2 extends Disposable implements ILinkifier2 { // Listen to viewport changes to re-render the link under the cursor (only when the line the // link is on changes) - if (this._renderService) { - this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => { - // Sanity check, this shouldn't happen in practice as this listener would be disposed - if (!this._currentLink) { - return; - } - // When start is 0 a scroll most likely occurred, make sure links above the fold also get - // cleared. - const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp; - const end = this._bufferService.buffer.ydisp + 1 + e.end; - // Only clear the link if the viewport change happened on this line - if (this._currentLink.link.range.start.y >= start && this._currentLink.link.range.end.y <= end) { - this._clearCurrentLink(start, end); - if (this._lastMouseEvent && this._element) { - // re-eval previously active link after changes - const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!); - if (position) { - this._askForLink(position, false); - } + this._linkCacheDisposables.push(this._renderService.onRenderedViewportChange(e => { + // Sanity check, this shouldn't happen in practice as this listener would be disposed + if (!this._currentLink) { + return; + } + // When start is 0 a scroll most likely occurred, make sure links above the fold also get + // cleared. + const start = e.start === 0 ? 0 : e.start + 1 + this._bufferService.buffer.ydisp; + const end = this._bufferService.buffer.ydisp + 1 + e.end; + // Only clear the link if the viewport change happened on this line + if (this._currentLink.link.range.start.y >= start && this._currentLink.link.range.end.y <= end) { + this._clearCurrentLink(start, end); + if (this._lastMouseEvent) { + // re-eval previously active link after changes + const position = this._positionFromMouseEvent(this._lastMouseEvent, this._element, this._mouseService!); + if (position) { + this._askForLink(position, false); } } - })); - } + } + })); } } diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 24600131..a92a4244 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -23,7 +23,7 @@ import { copyHandler, handlePasteEvent, moveTextAreaUnderMouseCursor, paste, rightClickHandler } from 'browser/Clipboard'; import { addDisposableDomListener } from 'browser/Lifecycle'; -import { Linkifier2 } from 'browser/Linkifier2'; +import { Linkifier2 } from './Linkifier2'; import * as Strings from 'browser/LocalizableStrings'; import { OscLinkProvider } from 'browser/OscLinkProvider'; import { CharacterJoinerHandler, CustomKeyEventHandler, CustomWheelEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal, IViewport } from 'browser/Types'; @@ -490,9 +490,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this._mouseService = this._instantiationService.createInstance(MouseService); this._instantiationService.setService(IMouseService, this._mouseService); - this.linkifier = this.register(this._instantiationService.createInstance(Linkifier2)); - // TODO: Move into ctor - this.linkifier.attachToDom(this.screenElement, this._mouseService, this._renderService); + this.linkifier = this.register(this._instantiationService.createInstance(Linkifier2, this.screenElement)); // Performance: Add viewport and helper elements from the fragment this.element.appendChild(fragment); diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index d27c0ee7..9ebc55d9 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -7,7 +7,6 @@ import { IEvent } from 'common/EventEmitter'; import { CharData, IColor, ICoreTerminal, ITerminalOptions } from 'common/Types'; import { IBuffer } from 'common/buffer/Types'; import { IDisposable, Terminal as ITerminalApi } from '@xterm/xterm'; -import { IMouseService, IRenderService } from 'browser/services/Services'; /** * A portion of the public API that are implemented identially internally and simply passed through. @@ -128,8 +127,6 @@ export interface ILinkifier2 extends IDisposable { onShowLinkUnderline: IEvent; onHideLinkUnderline: IEvent; readonly currentLink: ILinkWithState | undefined; - - attachToDom(element: HTMLElement, mouseService: IMouseService, renderService: IRenderService): void; } interface ILink { From d7c8d063b6f6fc1a316868a134ad601dbe2a13b3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Dec 2023 08:31:50 -0800 Subject: [PATCH 26/76] Get linkifier test to work --- src/browser/Linkifier2.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/browser/Linkifier2.test.ts b/src/browser/Linkifier2.test.ts index 07680a23..9176170c 100644 --- a/src/browser/Linkifier2.test.ts +++ b/src/browser/Linkifier2.test.ts @@ -9,6 +9,7 @@ import { Linkifier2 } from './Linkifier2'; import { MockBufferService } from 'common/TestUtils.test'; import { ILink } from 'browser/Types'; import { LinkProviderService } from 'browser/services/LinkProviderService'; +import jsdom = require('jsdom'); class TestLinkifier2 extends Linkifier2 { public set currentLink(link: any) { @@ -44,8 +45,9 @@ describe('Linkifier2', () => { }; beforeEach(() => { + const dom = new jsdom.JSDOM(); bufferService = new MockBufferService(100, 10); - linkifier = new TestLinkifier2(null!, null!, null!, bufferService, new LinkProviderService()); + linkifier = new TestLinkifier2(dom.window.document.createElement('div'), null!, null!, bufferService, new LinkProviderService()); linkifier.currentLink = { link, state: { From dbcb43f1d36cc2dd0000232d97cd0a9884a9c4e8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Dec 2023 08:32:39 -0800 Subject: [PATCH 27/76] Rename Linkifier2 -> Linkifier --- src/browser/{Linkifier2.test.ts => Linkifier.test.ts} | 4 ++-- src/browser/{Linkifier2.ts => Linkifier.ts} | 2 +- src/browser/Terminal.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) rename src/browser/{Linkifier2.test.ts => Linkifier.test.ts} (96%) rename src/browser/{Linkifier2.ts => Linkifier.ts} (99%) diff --git a/src/browser/Linkifier2.test.ts b/src/browser/Linkifier.test.ts similarity index 96% rename from src/browser/Linkifier2.test.ts rename to src/browser/Linkifier.test.ts index 9176170c..4294ef10 100644 --- a/src/browser/Linkifier2.test.ts +++ b/src/browser/Linkifier.test.ts @@ -5,13 +5,13 @@ import { assert } from 'chai'; import { IBufferService } from 'common/services/Services'; -import { Linkifier2 } from './Linkifier2'; +import { Linkifier } from './Linkifier'; import { MockBufferService } from 'common/TestUtils.test'; import { ILink } from 'browser/Types'; import { LinkProviderService } from 'browser/services/LinkProviderService'; import jsdom = require('jsdom'); -class TestLinkifier2 extends Linkifier2 { +class TestLinkifier2 extends Linkifier { public set currentLink(link: any) { this._currentLink = link; } diff --git a/src/browser/Linkifier2.ts b/src/browser/Linkifier.ts similarity index 99% rename from src/browser/Linkifier2.ts rename to src/browser/Linkifier.ts index 4c0fc9dc..ac37e42f 100644 --- a/src/browser/Linkifier2.ts +++ b/src/browser/Linkifier.ts @@ -11,7 +11,7 @@ import { IDisposable } from 'common/Types'; import { IBufferService } from 'common/services/Services'; import { ILinkProviderService, IMouseService, IRenderService } from './services/Services'; -export class Linkifier2 extends Disposable implements ILinkifier2 { +export class Linkifier extends Disposable implements ILinkifier2 { public get currentLink(): ILinkWithState | undefined { return this._currentLink; } protected _currentLink: ILinkWithState | undefined; private _mouseDownLink: ILinkWithState | undefined; diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index a92a4244..de8278c6 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -23,7 +23,7 @@ import { copyHandler, handlePasteEvent, moveTextAreaUnderMouseCursor, paste, rightClickHandler } from 'browser/Clipboard'; import { addDisposableDomListener } from 'browser/Lifecycle'; -import { Linkifier2 } from './Linkifier2'; +import { Linkifier } from './Linkifier'; import * as Strings from 'browser/LocalizableStrings'; import { OscLinkProvider } from 'browser/OscLinkProvider'; import { CharacterJoinerHandler, CustomKeyEventHandler, CustomWheelEventHandler, IBrowser, IBufferRange, ICompositionHelper, ILinkifier2, ITerminal, IViewport } from 'browser/Types'; @@ -490,7 +490,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this._mouseService = this._instantiationService.createInstance(MouseService); this._instantiationService.setService(IMouseService, this._mouseService); - this.linkifier = this.register(this._instantiationService.createInstance(Linkifier2, this.screenElement)); + this.linkifier = this.register(this._instantiationService.createInstance(Linkifier, this.screenElement)); // Performance: Add viewport and helper elements from the fragment this.element.appendChild(fragment); From 1b52c97fd01ebbae89995fc71ed019aba479cfe8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Dec 2023 08:46:09 -0800 Subject: [PATCH 28/76] Add wait for builld job to test-unit --- .github/workflows/ci.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a64b6a7..045eb9e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -167,6 +167,11 @@ jobs: run: | yarn --frozen-lockfile yarn install-addons + - name: Wait for build job + uses: NathanFirmo/wait-for-other-job@v1.1.1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + job: build - uses: actions/download-artifact@v3 with: name: build-artifacts From 53604e427ba05af231c08cd1123d223bdf39e128 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 23 Dec 2023 08:49:37 -0800 Subject: [PATCH 29/76] Update test-unit node to 18 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 045eb9e4..c985c0d0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,7 +153,7 @@ jobs: timeout-minutes: 20 strategy: matrix: - node-version: [16] + node-version: [18] runs-on: [ubuntu, macos, windows] runs-on: ${{ matrix.runs-on }}-latest steps: From 94a9ce8566e7b3d3a520038d9a0b6e7993c7f9f9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 28 Dec 2023 09:29:30 -0800 Subject: [PATCH 30/76] Move toColor into channels It operates on rgba channels, not a packed 32 bit int --- src/browser/Terminal.ts | 10 ++++---- .../renderer/dom/DomRendererRowFactory.ts | 6 ++--- src/browser/renderer/shared/TextureAtlas.ts | 8 +++---- src/common/Color.test.ts | 19 +++++++++++++++ src/common/Color.ts | 23 +++++++++---------- src/common/Types.d.ts | 4 ++-- 6 files changed, 44 insertions(+), 26 deletions(-) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index de8278c6..0e945aa9 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -41,7 +41,7 @@ import { RenderService } from 'browser/services/RenderService'; import { SelectionService } from 'browser/services/SelectionService'; import { ICharSizeService, ICharacterJoinerService, ICoreBrowserService, ILinkProviderService, IMouseService, IRenderService, ISelectionService, IThemeService } from 'browser/services/Services'; import { ThemeService } from 'browser/services/ThemeService'; -import { color, rgba } from 'common/Color'; +import { channels, color } from 'common/Color'; import { CoreTerminal } from 'common/CoreTerminal'; import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; import { MutableDisposable, toDisposable } from 'common/Lifecycle'; @@ -211,17 +211,17 @@ export class Terminal extends CoreTerminal implements ITerminal { } switch (req.type) { case ColorRequestType.REPORT: - const channels = color.toColorRGB(acc === 'ansi' + const colorRgb = color.toColorRGB(acc === 'ansi' ? this._themeService.colors.ansi[req.index] : this._themeService.colors[acc]); - this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(channels)}${C1_ESCAPED.ST}`); + this.coreService.triggerDataEvent(`${C0.ESC}]${ident};${toRgbString(colorRgb)}${C1_ESCAPED.ST}`); break; case ColorRequestType.SET: if (acc === 'ansi') { - this._themeService.modifyColors(colors => colors.ansi[req.index] = rgba.toColor(...req.color)); + this._themeService.modifyColors(colors => colors.ansi[req.index] = channels.toColor(...req.color)); } else { const narrowedAcc = acc; - this._themeService.modifyColors(colors => colors[narrowedAcc] = rgba.toColor(...req.color)); + this._themeService.modifyColors(colors => colors[narrowedAcc] = channels.toColor(...req.color)); } break; case ColorRequestType.RESTORE: diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index 50d3eb49..d71edeb9 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -8,7 +8,7 @@ import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/shared/Constants'; import { WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; -import { color, rgba } from 'common/Color'; +import { channels, color } from 'common/Color'; import { ICharacterJoinerService, ICoreBrowserService, IThemeService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; import { treatGlyphAsBackgroundColor } from 'browser/renderer/shared/RendererUtils'; @@ -376,7 +376,7 @@ export class DomRendererRowFactory { classes.push(`xterm-bg-${bg}`); break; case Attributes.CM_RGB: - resolvedBg = rgba.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF); + resolvedBg = channels.toColor(bg >> 16, bg >> 8 & 0xFF, bg & 0xFF); this._addStyle(charElement, `background-color:#${padStart((bg >>> 0).toString(16), '0', 6)}`); break; case Attributes.CM_DEFAULT: @@ -408,7 +408,7 @@ export class DomRendererRowFactory { } break; case Attributes.CM_RGB: - const color = rgba.toColor( + const color = channels.toColor( (fg >> 16) & 0xFF, (fg >> 8) & 0xFF, (fg ) & 0xFF diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index 9cd09cbf..3b3d8809 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -8,7 +8,7 @@ import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/shared/Constants'; import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs'; import { computeNextVariantOffset, treatGlyphAsBackgroundColor, isPowerlineGlyph, isRestrictedPowerlineGlyph, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; import { IBoundingBox, ICharAtlasConfig, IRasterizedGlyph, ITextureAtlas } from 'browser/renderer/shared/Types'; -import { NULL_COLOR, color, rgba } from 'common/Color'; +import { NULL_COLOR, channels, color, rgba } from 'common/Color'; import { EventEmitter } from 'common/EventEmitter'; import { FourKeyMap } from 'common/MultiKeyMap'; import { IdleTaskQueue } from 'common/TaskQueue'; @@ -292,7 +292,7 @@ export class TextureAtlas implements ITextureAtlas { case Attributes.CM_RGB: const arr = AttributeData.toColorRGB(bgColor); // TODO: This object creation is slow - result = rgba.toColor(arr[0], arr[1], arr[2]); + result = channels.toColor(arr[0], arr[1], arr[2]); break; case Attributes.CM_DEFAULT: default: @@ -324,7 +324,7 @@ export class TextureAtlas implements ITextureAtlas { break; case Attributes.CM_RGB: const arr = AttributeData.toColorRGB(fgColor); - result = rgba.toColor(arr[0], arr[1], arr[2]); + result = channels.toColor(arr[0], arr[1], arr[2]); break; case Attributes.CM_DEFAULT: default: @@ -406,7 +406,7 @@ export class TextureAtlas implements ITextureAtlas { return undefined; } - const color = rgba.toColor( + const color = channels.toColor( (result >> 24) & 0xFF, (result >> 16) & 0xFF, (result >> 8) & 0xFF diff --git a/src/common/Color.test.ts b/src/common/Color.test.ts index e250950d..c0947ba3 100644 --- a/src/common/Color.test.ts +++ b/src/common/Color.test.ts @@ -29,6 +29,25 @@ describe('Color', () => { assert.equal(channels.toCss(0xf0, 0xf0, 0xf0), '#f0f0f0'); assert.equal(channels.toCss(0xff, 0xff, 0xff), '#ffffff'); }); + it('should convert an rgba array to css hex string', () => { + assert.equal(channels.toCss(0x00, 0x00, 0x00, 0x00), '0x00000000'); + assert.equal(channels.toCss(0x10, 0x10, 0x10, 0x10), '0x10101010'); + assert.equal(channels.toCss(0x20, 0x20, 0x20, 0x20), '0x20202020'); + assert.equal(channels.toCss(0x30, 0x30, 0x30, 0x30), '0x30303030'); + assert.equal(channels.toCss(0x40, 0x40, 0x40, 0x40), '0x40404040'); + assert.equal(channels.toCss(0x50, 0x50, 0x50, 0x50), '0x50505050'); + assert.equal(channels.toCss(0x60, 0x60, 0x60, 0x60), '0x60606060'); + assert.equal(channels.toCss(0x70, 0x70, 0x70, 0x70), '0x70707070'); + assert.equal(channels.toCss(0x80, 0x80, 0x80, 0x80), '0x80808080'); + assert.equal(channels.toCss(0x90, 0x90, 0x90, 0x90), '0x90909090'); + assert.equal(channels.toCss(0xa0, 0xa0, 0xa0, 0xa0), '0xa0a0a0a0'); + assert.equal(channels.toCss(0xb0, 0xb0, 0xb0, 0xb0), '0xb0b0b0b0'); + assert.equal(channels.toCss(0xc0, 0xc0, 0xc0, 0xc0), '0xc0c0c0c0'); + assert.equal(channels.toCss(0xd0, 0xd0, 0xd0, 0xd0), '0xd0d0d0d0'); + assert.equal(channels.toCss(0xe0, 0xe0, 0xe0, 0xe0), '0xe0e0e0e0'); + assert.equal(channels.toCss(0xf0, 0xf0, 0xf0, 0xf0), '0xf0f0f0f0'); + assert.equal(channels.toCss(0xff, 0xff, 0xff, 0xff), '0xffffffff'); + }); }); describe('toRgba', () => { diff --git a/src/common/Color.ts b/src/common/Color.ts index 2291b7be..5ec2d87d 100644 --- a/src/common/Color.ts +++ b/src/common/Color.ts @@ -33,6 +33,13 @@ export namespace channels { // >>> 0 forces an unsigned int return (r << 24 | g << 16 | b << 8 | a) >>> 0; } + + export function toColor(r: number, g: number, b: number, a?: number): IColor { + return { + css: channels.toCss(r, g, b, a), + rgba: channels.toRgba(r, g, b, a) + }; + } } /** @@ -70,7 +77,7 @@ export namespace color { if (!result) { return undefined; } - return rgba.toColor( + return channels.toColor( (result >> 24 & 0xFF), (result >> 16 & 0xFF), (result >> 8 & 0xFF) @@ -142,14 +149,14 @@ export namespace css { $r = parseInt(css.slice(1, 2).repeat(2), 16); $g = parseInt(css.slice(2, 3).repeat(2), 16); $b = parseInt(css.slice(3, 4).repeat(2), 16); - return rgba.toColor($r, $g, $b); + return channels.toColor($r, $g, $b); } case 5: { // #rgba $r = parseInt(css.slice(1, 2).repeat(2), 16); $g = parseInt(css.slice(2, 3).repeat(2), 16); $b = parseInt(css.slice(3, 4).repeat(2), 16); $a = parseInt(css.slice(4, 5).repeat(2), 16); - return rgba.toColor($r, $g, $b, $a); + return channels.toColor($r, $g, $b, $a); } case 7: // #rrggbb return { @@ -171,7 +178,7 @@ export namespace css { $g = parseInt(rgbaMatch[2]); $b = parseInt(rgbaMatch[3]); $a = Math.round((rgbaMatch[5] === undefined ? 1 : parseFloat(rgbaMatch[5])) * 0xFF); - return rgba.toColor($r, $g, $b, $a); + return channels.toColor($r, $g, $b, $a); } // Validate the context is available for canvas-based color parsing @@ -342,17 +349,9 @@ export namespace rgba { return (fgR << 24 | fgG << 16 | fgB << 8 | 0xFF) >>> 0; } - // FIXME: Move this to channels NS? export function toChannels(value: number): [number, number, number, number] { return [(value >> 24) & 0xFF, (value >> 16) & 0xFF, (value >> 8) & 0xFF, value & 0xFF]; } - - export function toColor(r: number, g: number, b: number, a?: number): IColor { - return { - css: channels.toCss(r, g, b, a), - rgba: channels.toRgba(r, g, b, a) - }; - } } export function toPaddedHex(c: number): string { diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 175c47c3..17c7231a 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -110,8 +110,8 @@ export interface ICharset { export type CharData = [number, string, number, number]; export interface IColor { - css: string; - rgba: number; // 32-bit int with rgba in each byte + readonly css: string; + readonly rgba: number; // 32-bit int with rgba in each byte } export type IColorRGB = [number, number, number]; From 2e03680128306327de4cbb5f701177b13c0f93c6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 28 Dec 2023 09:32:57 -0800 Subject: [PATCH 31/76] Add channels.toColor tests --- src/common/Color.test.ts | 75 +++++++++++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/src/common/Color.test.ts b/src/common/Color.test.ts index c0947ba3..b1683711 100644 --- a/src/common/Color.test.ts +++ b/src/common/Color.test.ts @@ -30,23 +30,23 @@ describe('Color', () => { assert.equal(channels.toCss(0xff, 0xff, 0xff), '#ffffff'); }); it('should convert an rgba array to css hex string', () => { - assert.equal(channels.toCss(0x00, 0x00, 0x00, 0x00), '0x00000000'); - assert.equal(channels.toCss(0x10, 0x10, 0x10, 0x10), '0x10101010'); - assert.equal(channels.toCss(0x20, 0x20, 0x20, 0x20), '0x20202020'); - assert.equal(channels.toCss(0x30, 0x30, 0x30, 0x30), '0x30303030'); - assert.equal(channels.toCss(0x40, 0x40, 0x40, 0x40), '0x40404040'); - assert.equal(channels.toCss(0x50, 0x50, 0x50, 0x50), '0x50505050'); - assert.equal(channels.toCss(0x60, 0x60, 0x60, 0x60), '0x60606060'); - assert.equal(channels.toCss(0x70, 0x70, 0x70, 0x70), '0x70707070'); - assert.equal(channels.toCss(0x80, 0x80, 0x80, 0x80), '0x80808080'); - assert.equal(channels.toCss(0x90, 0x90, 0x90, 0x90), '0x90909090'); - assert.equal(channels.toCss(0xa0, 0xa0, 0xa0, 0xa0), '0xa0a0a0a0'); - assert.equal(channels.toCss(0xb0, 0xb0, 0xb0, 0xb0), '0xb0b0b0b0'); - assert.equal(channels.toCss(0xc0, 0xc0, 0xc0, 0xc0), '0xc0c0c0c0'); - assert.equal(channels.toCss(0xd0, 0xd0, 0xd0, 0xd0), '0xd0d0d0d0'); - assert.equal(channels.toCss(0xe0, 0xe0, 0xe0, 0xe0), '0xe0e0e0e0'); - assert.equal(channels.toCss(0xf0, 0xf0, 0xf0, 0xf0), '0xf0f0f0f0'); - assert.equal(channels.toCss(0xff, 0xff, 0xff, 0xff), '0xffffffff'); + assert.equal(channels.toCss(0x00, 0x00, 0x00, 0x00), '#00000000'); + assert.equal(channels.toCss(0x10, 0x10, 0x10, 0x10), '#10101010'); + assert.equal(channels.toCss(0x20, 0x20, 0x20, 0x20), '#20202020'); + assert.equal(channels.toCss(0x30, 0x30, 0x30, 0x30), '#30303030'); + assert.equal(channels.toCss(0x40, 0x40, 0x40, 0x40), '#40404040'); + assert.equal(channels.toCss(0x50, 0x50, 0x50, 0x50), '#50505050'); + assert.equal(channels.toCss(0x60, 0x60, 0x60, 0x60), '#60606060'); + assert.equal(channels.toCss(0x70, 0x70, 0x70, 0x70), '#70707070'); + assert.equal(channels.toCss(0x80, 0x80, 0x80, 0x80), '#80808080'); + assert.equal(channels.toCss(0x90, 0x90, 0x90, 0x90), '#90909090'); + assert.equal(channels.toCss(0xa0, 0xa0, 0xa0, 0xa0), '#a0a0a0a0'); + assert.equal(channels.toCss(0xb0, 0xb0, 0xb0, 0xb0), '#b0b0b0b0'); + assert.equal(channels.toCss(0xc0, 0xc0, 0xc0, 0xc0), '#c0c0c0c0'); + assert.equal(channels.toCss(0xd0, 0xd0, 0xd0, 0xd0), '#d0d0d0d0'); + assert.equal(channels.toCss(0xe0, 0xe0, 0xe0, 0xe0), '#e0e0e0e0'); + assert.equal(channels.toCss(0xf0, 0xf0, 0xf0, 0xf0), '#f0f0f0f0'); + assert.equal(channels.toCss(0xff, 0xff, 0xff, 0xff), '#ffffffff'); }); }); @@ -90,6 +90,47 @@ describe('Color', () => { assert.equal(channels.toRgba(0xff, 0xff, 0xff, 0xff), 0xffffffff); }); }); + + describe('toColor', () => { + it('should convert an rgb array to an IColor', () => { + assert.deepStrictEqual(channels.toColor(0x00, 0x00, 0x00), { css: '#000000', rgba: 0x000000FF }); + assert.deepStrictEqual(channels.toColor(0x10, 0x10, 0x10), { css: '#101010', rgba: 0x101010FF }); + assert.deepStrictEqual(channels.toColor(0x20, 0x20, 0x20), { css: '#202020', rgba: 0x202020FF }); + assert.deepStrictEqual(channels.toColor(0x30, 0x30, 0x30), { css: '#303030', rgba: 0x303030FF }); + assert.deepStrictEqual(channels.toColor(0x40, 0x40, 0x40), { css: '#404040', rgba: 0x404040FF }); + assert.deepStrictEqual(channels.toColor(0x50, 0x50, 0x50), { css: '#505050', rgba: 0x505050FF }); + assert.deepStrictEqual(channels.toColor(0x60, 0x60, 0x60), { css: '#606060', rgba: 0x606060FF }); + assert.deepStrictEqual(channels.toColor(0x70, 0x70, 0x70), { css: '#707070', rgba: 0x707070FF }); + assert.deepStrictEqual(channels.toColor(0x80, 0x80, 0x80), { css: '#808080', rgba: 0x808080FF }); + assert.deepStrictEqual(channels.toColor(0x90, 0x90, 0x90), { css: '#909090', rgba: 0x909090FF }); + assert.deepStrictEqual(channels.toColor(0xa0, 0xa0, 0xa0), { css: '#a0a0a0', rgba: 0xa0a0a0FF }); + assert.deepStrictEqual(channels.toColor(0xb0, 0xb0, 0xb0), { css: '#b0b0b0', rgba: 0xb0b0b0FF }); + assert.deepStrictEqual(channels.toColor(0xc0, 0xc0, 0xc0), { css: '#c0c0c0', rgba: 0xc0c0c0FF }); + assert.deepStrictEqual(channels.toColor(0xd0, 0xd0, 0xd0), { css: '#d0d0d0', rgba: 0xd0d0d0FF }); + assert.deepStrictEqual(channels.toColor(0xe0, 0xe0, 0xe0), { css: '#e0e0e0', rgba: 0xe0e0e0FF }); + assert.deepStrictEqual(channels.toColor(0xf0, 0xf0, 0xf0), { css: '#f0f0f0', rgba: 0xf0f0f0FF }); + assert.deepStrictEqual(channels.toColor(0xff, 0xff, 0xff), { css: '#ffffff', rgba: 0xffffffFF }); + }); + it('should convert an rgba array to an IColor', () => { + assert.deepStrictEqual(channels.toColor(0x00, 0x00, 0x00, 0x00), { css: '#00000000', rgba: 0x00000000 }); + assert.deepStrictEqual(channels.toColor(0x10, 0x10, 0x10, 0x10), { css: '#10101010', rgba: 0x10101010 }); + assert.deepStrictEqual(channels.toColor(0x20, 0x20, 0x20, 0x20), { css: '#20202020', rgba: 0x20202020 }); + assert.deepStrictEqual(channels.toColor(0x30, 0x30, 0x30, 0x30), { css: '#30303030', rgba: 0x30303030 }); + assert.deepStrictEqual(channels.toColor(0x40, 0x40, 0x40, 0x40), { css: '#40404040', rgba: 0x40404040 }); + assert.deepStrictEqual(channels.toColor(0x50, 0x50, 0x50, 0x50), { css: '#50505050', rgba: 0x50505050 }); + assert.deepStrictEqual(channels.toColor(0x60, 0x60, 0x60, 0x60), { css: '#60606060', rgba: 0x60606060 }); + assert.deepStrictEqual(channels.toColor(0x70, 0x70, 0x70, 0x70), { css: '#70707070', rgba: 0x70707070 }); + assert.deepStrictEqual(channels.toColor(0x80, 0x80, 0x80, 0x80), { css: '#80808080', rgba: 0x80808080 }); + assert.deepStrictEqual(channels.toColor(0x90, 0x90, 0x90, 0x90), { css: '#90909090', rgba: 0x90909090 }); + assert.deepStrictEqual(channels.toColor(0xa0, 0xa0, 0xa0, 0xa0), { css: '#a0a0a0a0', rgba: 0xa0a0a0a0 }); + assert.deepStrictEqual(channels.toColor(0xb0, 0xb0, 0xb0, 0xb0), { css: '#b0b0b0b0', rgba: 0xb0b0b0b0 }); + assert.deepStrictEqual(channels.toColor(0xc0, 0xc0, 0xc0, 0xc0), { css: '#c0c0c0c0', rgba: 0xc0c0c0c0 }); + assert.deepStrictEqual(channels.toColor(0xd0, 0xd0, 0xd0, 0xd0), { css: '#d0d0d0d0', rgba: 0xd0d0d0d0 }); + assert.deepStrictEqual(channels.toColor(0xe0, 0xe0, 0xe0, 0xe0), { css: '#e0e0e0e0', rgba: 0xe0e0e0e0 }); + assert.deepStrictEqual(channels.toColor(0xf0, 0xf0, 0xf0, 0xf0), { css: '#f0f0f0f0', rgba: 0xf0f0f0f0 }); + assert.deepStrictEqual(channels.toColor(0xff, 0xff, 0xff, 0xff), { css: '#ffffffff', rgba: 0xffffffff }); + }); + }); }); describe('color', () => { From 1a82e0d421e76149d50d3eb3db628a88e5b5ccb0 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 28 Dec 2023 09:47:05 -0800 Subject: [PATCH 32/76] Remove todo Not much gain by caching the value --- src/browser/renderer/shared/TextureAtlas.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/browser/renderer/shared/TextureAtlas.ts b/src/browser/renderer/shared/TextureAtlas.ts index 3b3d8809..af2cafb8 100644 --- a/src/browser/renderer/shared/TextureAtlas.ts +++ b/src/browser/renderer/shared/TextureAtlas.ts @@ -291,7 +291,6 @@ export class TextureAtlas implements ITextureAtlas { break; case Attributes.CM_RGB: const arr = AttributeData.toColorRGB(bgColor); - // TODO: This object creation is slow result = channels.toColor(arr[0], arr[1], arr[2]); break; case Attributes.CM_DEFAULT: From 559026bbf3ce2a74cbb7682663596895219c79d7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 29 Dec 2023 03:40:44 -0800 Subject: [PATCH 33/76] Implement and default to text metrics measure strategy Fixes #3449 --- src/browser/services/CharSizeService.ts | 63 ++++++++++++++++++------- 1 file changed, 45 insertions(+), 18 deletions(-) diff --git a/src/browser/services/CharSizeService.ts b/src/browser/services/CharSizeService.ts index 614b9b30..ef0b1ab4 100644 --- a/src/browser/services/CharSizeService.ts +++ b/src/browser/services/CharSizeService.ts @@ -8,12 +8,6 @@ import { EventEmitter } from 'common/EventEmitter'; import { ICharSizeService } from 'browser/services/Services'; import { Disposable } from 'common/Lifecycle'; - -const enum MeasureSettings { - REPEAT = 32 -} - - export class CharSizeService extends Disposable implements ICharSizeService { public serviceBrand: undefined; @@ -32,7 +26,11 @@ export class CharSizeService extends Disposable implements ICharSizeService { @IOptionsService private readonly _optionsService: IOptionsService ) { super(); - this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService); + try { + this._measureStrategy = new TextMetricsMeasureStrategy(this._optionsService); + } catch { + this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService); + } this.register(this._optionsService.onMultipleOptionChange(['fontFamily', 'fontSize'], () => this.measure())); } @@ -47,12 +45,7 @@ export class CharSizeService extends Disposable implements ICharSizeService { } interface IMeasureStrategy { - measure(): IReadonlyMeasureResult; -} - -interface IReadonlyMeasureResult { - readonly width: number; - readonly height: number; + measure(): Readonly; } interface IMeasureResult { @@ -60,8 +53,10 @@ interface IMeasureResult { height: number; } -// TODO: For supporting browsers we should also provide a CanvasCharDimensionsProvider that uses -// ctx.measureText +const enum DomMeasureStrategyConstants { + REPEAT = 32 +} + class DomMeasureStrategy implements IMeasureStrategy { private _result: IMeasureResult = { width: 0, height: 0 }; private _measureElement: HTMLElement; @@ -73,14 +68,14 @@ class DomMeasureStrategy implements IMeasureStrategy { ) { this._measureElement = this._document.createElement('span'); this._measureElement.classList.add('xterm-char-measure-element'); - this._measureElement.textContent = 'W'.repeat(MeasureSettings.REPEAT); + this._measureElement.textContent = 'W'.repeat(DomMeasureStrategyConstants.REPEAT); this._measureElement.setAttribute('aria-hidden', 'true'); this._measureElement.style.whiteSpace = 'pre'; this._measureElement.style.fontKerning = 'none'; this._parentElement.appendChild(this._measureElement); } - public measure(): IReadonlyMeasureResult { + public measure(): Readonly { this._measureElement.style.fontFamily = this._optionsService.rawOptions.fontFamily; this._measureElement.style.fontSize = `${this._optionsService.rawOptions.fontSize}px`; @@ -93,10 +88,42 @@ class DomMeasureStrategy implements IMeasureStrategy { // If values are 0 then the element is likely currently display:none, in which case we should // retain the previous value. if (geometry.width !== 0 && geometry.height !== 0) { - this._result.width = geometry.width / MeasureSettings.REPEAT; + this._result.width = geometry.width / DomMeasureStrategyConstants.REPEAT; this._result.height = Math.ceil(geometry.height); } return this._result; } } + +class TextMetricsMeasureStrategy implements IMeasureStrategy { + private _result: IMeasureResult = { width: 0, height: 0 }; + private _canvas: OffscreenCanvas; + private _ctx: OffscreenCanvasRenderingContext2D; + + constructor( + private _optionsService: IOptionsService + ) { + // This will throw if any required API is not supported + this._canvas = new OffscreenCanvas(100, 100); + this._ctx = this._canvas.getContext('2d')!; + const a = this._ctx.measureText('W'); + if (!('width' in a && 'fontBoundingBoxAscent' in a && 'fontBoundingBoxDescent' in a)) { + throw new Error('Required font metrics not supported'); + } + } + + public measure(): Readonly { + this._ctx.font = `${this._optionsService.rawOptions.fontSize}px ${this._optionsService.rawOptions.fontFamily}`; + + const metrics = this._ctx.measureText('W'); + + // Sanity check that the values are not 0 + if (metrics.width !== 0 && metrics.fontBoundingBoxAscent !== 0) { + this._result.width = metrics.width; + this._result.height = Math.ceil(metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent); + } + + return this._result; + } +} From 379c382fe7465ff91398dfbf84d7b87be8b5eb05 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 29 Dec 2023 03:47:44 -0800 Subject: [PATCH 34/76] Pull common measure parts into base class --- src/browser/services/CharSizeService.ts | 48 ++++++++++++------------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/src/browser/services/CharSizeService.ts b/src/browser/services/CharSizeService.ts index ef0b1ab4..da14b67d 100644 --- a/src/browser/services/CharSizeService.ts +++ b/src/browser/services/CharSizeService.ts @@ -27,9 +27,9 @@ export class CharSizeService extends Disposable implements ICharSizeService { ) { super(); try { - this._measureStrategy = new TextMetricsMeasureStrategy(this._optionsService); + this._measureStrategy = this.register(new TextMetricsMeasureStrategy(this._optionsService)); } catch { - this._measureStrategy = new DomMeasureStrategy(document, parentElement, this._optionsService); + this._measureStrategy = this.register(new DomMeasureStrategy(document, parentElement, this._optionsService)); } this.register(this._optionsService.onMultipleOptionChange(['fontFamily', 'fontSize'], () => this.measure())); } @@ -57,8 +57,22 @@ const enum DomMeasureStrategyConstants { REPEAT = 32 } -class DomMeasureStrategy implements IMeasureStrategy { - private _result: IMeasureResult = { width: 0, height: 0 }; +abstract class BaseMeasureStategy extends Disposable implements IMeasureStrategy { + protected _result: IMeasureResult = { width: 0, height: 0 }; + + protected _validateAndSet(width: number | undefined, height: number | undefined): void { + // If values are 0 then the element is likely currently display:none, in which case we should + // retain the previous value. + if (width !== undefined && width > 0 && height !== undefined && height > 0) { + this._result.width = width; + this._result.height = height; + } + } + + public abstract measure(): Readonly; +} + +class DomMeasureStrategy extends BaseMeasureStategy { private _measureElement: HTMLElement; constructor( @@ -66,6 +80,7 @@ class DomMeasureStrategy implements IMeasureStrategy { private _parentElement: HTMLElement, private _optionsService: IOptionsService ) { + super(); this._measureElement = this._document.createElement('span'); this._measureElement.classList.add('xterm-char-measure-element'); this._measureElement.textContent = 'W'.repeat(DomMeasureStrategyConstants.REPEAT); @@ -80,30 +95,20 @@ class DomMeasureStrategy implements IMeasureStrategy { this._measureElement.style.fontSize = `${this._optionsService.rawOptions.fontSize}px`; // Note that this triggers a synchronous layout - const geometry = { - height: Number(this._measureElement.offsetHeight), - width: Number(this._measureElement.offsetWidth) - }; - - // If values are 0 then the element is likely currently display:none, in which case we should - // retain the previous value. - if (geometry.width !== 0 && geometry.height !== 0) { - this._result.width = geometry.width / DomMeasureStrategyConstants.REPEAT; - this._result.height = Math.ceil(geometry.height); - } + this._validateAndSet(Number(this._measureElement.offsetWidth) / DomMeasureStrategyConstants.REPEAT, Number(this._measureElement.offsetHeight)); return this._result; } } -class TextMetricsMeasureStrategy implements IMeasureStrategy { - private _result: IMeasureResult = { width: 0, height: 0 }; +class TextMetricsMeasureStrategy extends BaseMeasureStategy { private _canvas: OffscreenCanvas; private _ctx: OffscreenCanvasRenderingContext2D; constructor( private _optionsService: IOptionsService ) { + super(); // This will throw if any required API is not supported this._canvas = new OffscreenCanvas(100, 100); this._ctx = this._canvas.getContext('2d')!; @@ -115,15 +120,8 @@ class TextMetricsMeasureStrategy implements IMeasureStrategy { public measure(): Readonly { this._ctx.font = `${this._optionsService.rawOptions.fontSize}px ${this._optionsService.rawOptions.fontFamily}`; - const metrics = this._ctx.measureText('W'); - - // Sanity check that the values are not 0 - if (metrics.width !== 0 && metrics.fontBoundingBoxAscent !== 0) { - this._result.width = metrics.width; - this._result.height = Math.ceil(metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent); - } - + this._validateAndSet(metrics.width, metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent); return this._result; } } From 82d50a4f89d9699deef35fbfa0ac214f83992186 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 29 Dec 2023 03:57:37 -0800 Subject: [PATCH 35/76] Allow fit addon test to work when hidden --- addons/addon-fit/test/FitAddon.api.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/addons/addon-fit/test/FitAddon.api.ts b/addons/addon-fit/test/FitAddon.api.ts index 5611972e..24641fc6 100644 --- a/addons/addon-fit/test/FitAddon.api.ts +++ b/addons/addon-fit/test/FitAddon.api.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { openTerminal, launchBrowser } from '../../../out-test/api/TestUtils'; +import { openTerminal, launchBrowser, timeout } from '../../../out-test/api/TestUtils'; import { Browser, Page } from '@playwright/test'; const APP = 'http://127.0.0.1:3001/test'; @@ -75,7 +75,15 @@ describe('FitAddon', () => { await page.evaluate(`window.term = new Terminal()`); await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); await loadFit(); - assert.equal(await page.evaluate(`window.fit.proposeDimensions()`), undefined); + const dimensions: { cols: number, rows: number } | undefined = await page.evaluate(`window.fit.proposeDimensions()`); + // The value of dims will be undefined if the char measure strategy falls back to the DOM + // method, so only assert if it's not undefined. + if (dimensions) { + assert.isAbove(dimensions.cols, 85); + assert.isBelow(dimensions.cols, 88); + assert.isAbove(dimensions.rows, 24); + assert.isBelow(dimensions.rows, 29); + } }); }); From a7bf3b22c1960b6432982c79a9e3038448c04cec Mon Sep 17 00:00:00 2001 From: Per Bothner Date: Fri, 29 Dec 2023 18:13:09 -0800 Subject: [PATCH 36/76] Update required eslint and eslint-plugin-jsdoc versions This fixes a problem when using node 20.*.*, because eslint-plugin-jsdoc@39.9.1 requires node version "^14 || ^16 || ^17 || ^18 || ^19". --- package.json | 4 +- yarn.lock | 167 +++++++++++++++++++++++++++++++-------------------- 2 files changed, 103 insertions(+), 68 deletions(-) diff --git a/package.json b/package.json index d0510e5d..050b8cad 100644 --- a/package.json +++ b/package.json @@ -78,8 +78,8 @@ "chai": "^4.3.4", "cross-env": "^7.0.3", "deep-equal": "^2.0.5", - "eslint": "^8.45.0", - "eslint-plugin-jsdoc": "^39.3.6", + "eslint": "^8.56.0", + "eslint-plugin-jsdoc": "^46.9.1", "express": "^4.17.1", "express-ws": "^5.0.2", "glob": "^7.2.0", diff --git a/yarn.lock b/yarn.lock index a0cb39da..b2788793 100644 --- a/yarn.lock +++ b/yarn.lock @@ -265,14 +265,14 @@ resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz#1d572bfbbe14b7704e0ba0f39b74815b84870d70" integrity sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw== -"@es-joy/jsdoccomment@~0.36.1": - version "0.36.1" - resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.36.1.tgz#c37db40da36e4b848da5fd427a74bae3b004a30f" - integrity sha512-922xqFsTpHs6D0BUiG4toiyPOMc8/jafnWKxz1KWgS4XzKPy2qXf1Pe6UFuNSCQqt6tOuhAWXBNuuyUhJmw9Vg== +"@es-joy/jsdoccomment@~0.41.0": + version "0.41.0" + resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz#4a2f7db42209c0425c71a1476ef1bdb6dcd836f6" + integrity sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw== dependencies: - comment-parser "1.3.1" - esquery "^1.4.0" - jsdoc-type-pratt-parser "~3.1.0" + comment-parser "1.4.1" + esquery "^1.5.0" + jsdoc-type-pratt-parser "~4.0.0" "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": version "4.4.0" @@ -281,15 +281,20 @@ dependencies: eslint-visitor-keys "^3.3.0" -"@eslint-community/regexpp@^4.4.0", "@eslint-community/regexpp@^4.5.1": +"@eslint-community/regexpp@^4.5.1": version "4.6.2" resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.6.2.tgz#1816b5f6948029c5eaacb0703b850ee0cb37d8f8" integrity sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw== -"@eslint/eslintrc@^2.1.0": - version "2.1.0" - resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.0.tgz#82256f164cc9e0b59669efc19d57f8092706841d" - integrity sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A== +"@eslint-community/regexpp@^4.6.1": + version "4.10.0" + resolved "https://registry.yarnpkg.com/@eslint-community/regexpp/-/regexpp-4.10.0.tgz#548f6de556857c8bb73bbee70c35dc82a2e74d63" + integrity sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA== + +"@eslint/eslintrc@^2.1.4": + version "2.1.4" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz#388a269f0f25c1b6adc317b5a2c55714894c70ad" + integrity sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ== dependencies: ajv "^6.12.4" debug "^4.3.2" @@ -301,17 +306,17 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" -"@eslint/js@8.44.0": - version "8.44.0" - resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.44.0.tgz#961a5903c74139390478bdc808bcde3fc45ab7af" - integrity sha512-Ag+9YM4ocKQx9AarydN0KY2j0ErMHNIocPDrVo8zAE44xLTjEtz81OdR68/cydGtk6m6jDb5Za3r2useMzYmSw== +"@eslint/js@8.56.0": + version "8.56.0" + resolved "https://registry.yarnpkg.com/@eslint/js/-/js-8.56.0.tgz#ef20350fec605a7f7035a01764731b2de0f3782b" + integrity sha512-gMsVel9D7f2HLkBma9VbtzZRehRogVRfbr++f06nL2vnCGCNlzOD+/MUov/F4p8myyAHspEhVobgjpX64q5m6A== -"@humanwhocodes/config-array@^0.11.10": - version "0.11.10" - resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.10.tgz#5a3ffe32cc9306365fb3fd572596cd602d5e12d2" - integrity sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ== +"@humanwhocodes/config-array@^0.11.13": + version "0.11.13" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.13.tgz#075dc9684f40a531d9b26b0822153c1e832ee297" + integrity sha512-JSBDMiDKSzQVngfRjOdFXgFfklaXI4K9nLF49Auh21lmBWRLIK3+xTErTWD4KU54pb6coM6ESE7Awz/FNU3zgQ== dependencies: - "@humanwhocodes/object-schema" "^1.2.1" + "@humanwhocodes/object-schema" "^2.0.1" debug "^4.1.1" minimatch "^3.0.5" @@ -320,10 +325,10 @@ resolved "https://registry.yarnpkg.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz#af5b2691a22b44be847b0ca81641c5fb6ad0172c" integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== -"@humanwhocodes/object-schema@^1.2.1": - version "1.2.1" - resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" - integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== +"@humanwhocodes/object-schema@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-2.0.1.tgz#e5211452df060fa8522b55c7b3c0c4d1981cb044" + integrity sha512-dvuCeX5fC9dXgJn9t+X5atfmgQAzUOWqS1254Gh0m6i8wKd10ebXkfNKiRK+1GWi/yTvvLDHpoxLr0xxxeslWw== "@istanbuljs/load-nyc-config@^1.0.0": version "1.1.0" @@ -756,6 +761,11 @@ "@typescript-eslint/types" "6.2.0" eslint-visitor-keys "^3.4.1" +"@ungap/structured-clone@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.2.0.tgz#756641adb587851b5ccb3e095daf27ae581c8406" + integrity sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ== + "@webassemblyjs/ast@1.11.6", "@webassemblyjs/ast@^1.11.5": version "1.11.6" resolved "https://registry.yarnpkg.com/@webassemblyjs/ast/-/ast-1.11.6.tgz#db046555d3c413f8966ca50a95176a0e2c642e24" @@ -970,7 +980,7 @@ ajv-keywords@^3.5.2: resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.5.2.tgz#31f29da5ab6e00d1c2d329acf7b5929614d5014d" integrity sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ== -ajv@^6.10.0, ajv@^6.12.4, ajv@^6.12.5: +ajv@^6.12.4, ajv@^6.12.5: version "6.12.6" resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== @@ -1029,6 +1039,11 @@ archy@^1.0.0: resolved "https://registry.yarnpkg.com/archy/-/archy-1.0.0.tgz#f9c8c13757cc1dd7bc379ac77b2c62a5c2868c40" integrity sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw== +are-docs-informative@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/are-docs-informative/-/are-docs-informative-0.0.2.tgz#387f0e93f5d45280373d387a59d34c96db321963" + integrity sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -1149,6 +1164,11 @@ buffer-from@^1.0.0: resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== +builtin-modules@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/builtin-modules/-/builtin-modules-3.3.0.tgz#cae62812b89801e9656336e46223e030386be7b6" + integrity sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw== + bytes@3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" @@ -1355,10 +1375,10 @@ commander@^7.0.0: resolved "https://registry.yarnpkg.com/commander/-/commander-7.2.0.tgz#a36cb57d0b501ce108e4d20559a150a391d97ab7" integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== -comment-parser@1.3.1: - version "1.3.1" - resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.3.1.tgz#3d7ea3adaf9345594aedee6563f422348f165c1b" - integrity sha512-B52sN2VNghyq5ofvUsqZjmk6YkihBX5vMSChmSK9v4ShjKf3Vk5Xcmgpw4o+iIgtrnM/u5FiMpz9VKb8lpBveA== +comment-parser@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.4.1.tgz#bdafead37961ac079be11eb7ec65c4d021eaf9cc" + integrity sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg== commondir@^1.0.1: version "1.0.1" @@ -1666,18 +1686,20 @@ escodegen@^2.0.0: optionalDependencies: source-map "~0.6.1" -eslint-plugin-jsdoc@^39.3.6: - version "39.9.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-39.9.1.tgz#e9ce1723411fd7ea0933b3ef0dd02156ae3068e2" - integrity sha512-Rq2QY6BZP2meNIs48aZ3GlIlJgBqFCmR55+UBvaDkA3ZNQ0SvQXOs2QKkubakEijV8UbIVbVZKsOVN8G3MuqZw== +eslint-plugin-jsdoc@^46.9.1: + version "46.9.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.9.1.tgz#d30adce51fecc768e87481bf4de46b8618c3d50e" + integrity sha512-11Ox5LCl2wY7gGkp9UOyew70o9qvii1daAH+h/MFobRVRNcy7sVlH+jm0HQdgcvcru6285GvpjpUyoa051j03Q== dependencies: - "@es-joy/jsdoccomment" "~0.36.1" - comment-parser "1.3.1" + "@es-joy/jsdoccomment" "~0.41.0" + are-docs-informative "^0.0.2" + comment-parser "1.4.1" debug "^4.3.4" escape-string-regexp "^4.0.0" - esquery "^1.4.0" - semver "^7.3.8" - spdx-expression-parse "^3.0.1" + esquery "^1.5.0" + is-builtin-module "^3.2.1" + semver "^7.5.4" + spdx-expression-parse "^4.0.0" eslint-scope@5.1.1: version "5.1.1" @@ -1687,10 +1709,10 @@ eslint-scope@5.1.1: esrecurse "^4.3.0" estraverse "^4.1.1" -eslint-scope@^7.2.0: - version "7.2.1" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.1.tgz#936821d3462675f25a18ac5fd88a67cc15b393bd" - integrity sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA== +eslint-scope@^7.2.2: + version "7.2.2" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.2.2.tgz#deb4f92563390f32006894af62a22dba1c46423f" + integrity sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg== dependencies: esrecurse "^4.3.0" estraverse "^5.2.0" @@ -1700,27 +1722,33 @@ eslint-visitor-keys@^3.3.0, eslint-visitor-keys@^3.4.1: resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.1.tgz#c22c48f48942d08ca824cc526211ae400478a994" integrity sha512-pZnmmLwYzf+kWaM/Qgrvpen51upAktaaiI01nsJD/Yr3lMOdNtq0cxkrrg16w64VtisN6okbs7Q8AfGqj4c9fA== -eslint@^8.45.0: - version "8.45.0" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.45.0.tgz#bab660f90d18e1364352c0a6b7c6db8edb458b78" - integrity sha512-pd8KSxiQpdYRfYa9Wufvdoct3ZPQQuVuU5O6scNgMuOMYuxvH0IGaYK0wUFjo4UYYQQCUndlXiMbnxopwvvTiw== +eslint-visitor-keys@^3.4.3: + version "3.4.3" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz#0cd72fe8550e3c2eae156a96a4dddcd1c8ac5800" + integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== + +eslint@^8.56.0: + version "8.56.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.56.0.tgz#4957ce8da409dc0809f99ab07a1b94832ab74b15" + integrity sha512-Go19xM6T9puCOWntie1/P997aXxFsOi37JIHRWI514Hc6ZnaHGKY9xFhrU65RT6CcBEzZoGG1e6Nq+DT04ZtZQ== dependencies: "@eslint-community/eslint-utils" "^4.2.0" - "@eslint-community/regexpp" "^4.4.0" - "@eslint/eslintrc" "^2.1.0" - "@eslint/js" "8.44.0" - "@humanwhocodes/config-array" "^0.11.10" + "@eslint-community/regexpp" "^4.6.1" + "@eslint/eslintrc" "^2.1.4" + "@eslint/js" "8.56.0" + "@humanwhocodes/config-array" "^0.11.13" "@humanwhocodes/module-importer" "^1.0.1" "@nodelib/fs.walk" "^1.2.8" - ajv "^6.10.0" + "@ungap/structured-clone" "^1.2.0" + ajv "^6.12.4" chalk "^4.0.0" cross-spawn "^7.0.2" debug "^4.3.2" doctrine "^3.0.0" escape-string-regexp "^4.0.0" - eslint-scope "^7.2.0" - eslint-visitor-keys "^3.4.1" - espree "^9.6.0" + eslint-scope "^7.2.2" + eslint-visitor-keys "^3.4.3" + espree "^9.6.1" esquery "^1.4.2" esutils "^2.0.2" fast-deep-equal "^3.1.3" @@ -1743,7 +1771,7 @@ eslint@^8.45.0: strip-ansi "^6.0.1" text-table "^0.2.0" -espree@^9.6.0: +espree@^9.6.0, espree@^9.6.1: version "9.6.1" resolved "https://registry.yarnpkg.com/espree/-/espree-9.6.1.tgz#a2a17b8e434690a5432f2f8018ce71d331a48c6f" integrity sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ== @@ -1757,7 +1785,7 @@ esprima@^4.0.0, esprima@^4.0.1: resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== -esquery@^1.4.0, esquery@^1.4.2: +esquery@^1.4.2, esquery@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.5.0.tgz#6ce17738de8577694edd7361c57182ac8cb0db0b" integrity sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg== @@ -2341,6 +2369,13 @@ is-boolean-object@^1.1.0: call-bind "^1.0.2" has-tostringtag "^1.0.0" +is-builtin-module@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/is-builtin-module/-/is-builtin-module-3.2.1.tgz#f03271717d8654cfcaf07ab0463faa3571581169" + integrity sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A== + dependencies: + builtin-modules "^3.3.0" + is-callable@^1.1.3: version "1.2.7" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" @@ -2599,10 +2634,10 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" -jsdoc-type-pratt-parser@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-3.1.0.tgz#a4a56bdc6e82e5865ffd9febc5b1a227ff28e67e" - integrity sha512-MgtD0ZiCDk9B+eI73BextfRrVQl0oyzRG8B2BjORts6jbunj4ScKPcyXGTbB6eXL4y9TzxCm6hyeLq/2ASzNdw== +jsdoc-type-pratt-parser@~4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz#136f0571a99c184d84ec84662c45c29ceff71114" + integrity sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ== jsdom@^18.0.1: version "18.1.1" @@ -3348,7 +3383,7 @@ semver@^6.0.0, semver@^6.3.0, semver@^6.3.1: resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== -semver@^7.3.4, semver@^7.3.8, semver@^7.5.3, semver@^7.5.4: +semver@^7.3.4, semver@^7.5.3, semver@^7.5.4: version "7.5.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.4.tgz#483986ec4ed38e1c6c48c34894a9182dbff68a6e" integrity sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA== @@ -3490,10 +3525,10 @@ spdx-exceptions@^2.1.0: resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz#3f28ce1a77a00372683eade4a433183527a2163d" integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== -spdx-expression-parse@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" - integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== +spdx-expression-parse@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz#a23af9f3132115465dac215c099303e4ceac5794" + integrity sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ== dependencies: spdx-exceptions "^2.1.0" spdx-license-ids "^3.0.0" From 7b7fb73611f33940f0dcc7c2ac9851a1dc37a201 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 30 Dec 2023 09:30:25 -0800 Subject: [PATCH 37/76] Ignore out dirs in eslint --- .eslintrc.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.eslintrc.json b/.eslintrc.json index 9ba50120..682e42de 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -42,6 +42,8 @@ }, "ignorePatterns": [ "addons/*/src/third-party/*.ts", + "out/*", + "out-test/*", "**/inwasm-sdks/*", "**/typings/*.d.ts", "**/node_modules", From 856666d5bdc003c467384c5e4f25efab5ae2469a Mon Sep 17 00:00:00 2001 From: Szymon Kaliski Date: Fri, 5 Jan 2024 09:43:33 +0100 Subject: [PATCH 38/76] fix url parsing for urls with percentage sign in them --- addons/addon-web-links/src/WebLinkProvider.ts | 15 +++++++++++++-- addons/addon-web-links/test/WebLinksAddon.api.ts | 7 +++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/addons/addon-web-links/src/WebLinkProvider.ts b/addons/addon-web-links/src/WebLinkProvider.ts index 25dd983c..1000b788 100644 --- a/addons/addon-web-links/src/WebLinkProvider.ts +++ b/addons/addon-web-links/src/WebLinkProvider.ts @@ -41,6 +41,18 @@ export class WebLinkProvider implements ILinkProvider { } } +function baseUrlString(url: URL): string { + if (url.password && url.username) { + return `${url.protocol}//${url.username}:${url.password}@${url.host}`; + } + + if (url.username) { + return `${url.protocol}//${url.username}@${url.host}`; + } + + return `${url.protocol}//${url.host}`; +} + export class LinkComputer { public static computeLink(y: number, regex: RegExp, terminal: Terminal, activate: (event: MouseEvent, uri: string) => void): ILink[] { const rex = new RegExp(regex.source, (regex.flags || '') + 'g'); @@ -64,8 +76,7 @@ export class LinkComputer { // - append / also match domain urls w'o any path notion try { const url = new URL(text); - const urlText = decodeURI(url.toString()); - if (text !== urlText && text + '/' !== urlText) { + if (!text.startsWith(baseUrlString(url))) { continue; } } catch (e) { diff --git a/addons/addon-web-links/test/WebLinksAddon.api.ts b/addons/addon-web-links/test/WebLinksAddon.api.ts index fb5be20b..5b05ce2e 100644 --- a/addons/addon-web-links/test/WebLinksAddon.api.ts +++ b/addons/addon-web-links/test/WebLinksAddon.api.ts @@ -115,6 +115,13 @@ describe('WebLinksAddon', () => { await resetAndHover(5, 1); await evalLinkStateData('http://test:password@example.com/some_path', { start: { x: 12, y: 1 }, end: { x: 13, y: 2 } }); }); + it('url encoded params work properly', async () => { + await writeSync(page, '¥¥¥cafe\u0301 http://test:password@example.com/some_path?param=1%202%3'); + await resetAndHover(12, 0); + await evalLinkStateData('http://test:password@example.com/some_path?param=1%202%3', { start: { x: 12, y: 1 }, end: { x: 27, y: 2 } }); + await resetAndHover(5, 1); + await evalLinkStateData('http://test:password@example.com/some_path?param=1%202%3', { start: { x: 12, y: 1 }, end: { x: 27, y: 2 } }); + }); }); }); From f03d870bcdcdce18a2871757460e3229458dba63 Mon Sep 17 00:00:00 2001 From: Szymon Kaliski Date: Thu, 11 Jan 2024 11:23:24 -0800 Subject: [PATCH 39/76] adjust a comment --- addons/addon-web-links/src/WebLinkProvider.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/addons/addon-web-links/src/WebLinkProvider.ts b/addons/addon-web-links/src/WebLinkProvider.ts index 1000b788..713f9c23 100644 --- a/addons/addon-web-links/src/WebLinkProvider.ts +++ b/addons/addon-web-links/src/WebLinkProvider.ts @@ -68,12 +68,8 @@ export class LinkComputer { // check via URL if the matched text would form a proper url // NOTE: This outsources the ugly url parsing to the browser. - // To avoid surprising auto expansion from URL we additionally - // check afterwards if the provided string resembles the parsed - // one close enough: - // - decodeURI decode path segement back to byte repr - // to detect unicode auto conversion correctly - // - append / also match domain urls w'o any path notion + // we check if the provided string resembles the URL-parsed one + // up to the end of the domain name (ignoring path and params) try { const url = new URL(text); if (!text.startsWith(baseUrlString(url))) { From 3c88c767cd5a66077681769c92eb39b60add6eac Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 30 Jan 2024 11:16:19 -0800 Subject: [PATCH 40/76] Fix background selection blending for true color Bug in #4920 See microsoft/vscode#200428 --- src/browser/renderer/shared/CellColorResolver.ts | 8 ++++---- test/playwright/SharedRendererTests.ts | 16 +++++++++------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/browser/renderer/shared/CellColorResolver.ts b/src/browser/renderer/shared/CellColorResolver.ts index 50725108..6f61a704 100644 --- a/src/browser/renderer/shared/CellColorResolver.ts +++ b/src/browser/renderer/shared/CellColorResolver.ts @@ -92,7 +92,7 @@ export class CellColorResolver { $bg = this._themeService.colors.ansi[this.result.fg & Attributes.PCOLOR_MASK].rgba; break; case Attributes.CM_RGB: - $bg = (this.result.fg & Attributes.RGB_MASK) << 8 | 0xFF; + $bg = ((this.result.fg & Attributes.RGB_MASK) << 8) | 0xFF; break; case Attributes.CM_DEFAULT: default: @@ -105,7 +105,7 @@ export class CellColorResolver { $bg = this._themeService.colors.ansi[this.result.bg & Attributes.PCOLOR_MASK].rgba; break; case Attributes.CM_RGB: - $bg = this.result.bg & Attributes.RGB_MASK << 8 | 0xFF; + $bg = ((this.result.bg & Attributes.RGB_MASK) << 8) | 0xFF; break; // No need to consider default bg color here as it's not possible } @@ -143,7 +143,7 @@ export class CellColorResolver { $fg = this._themeService.colors.ansi[this.result.bg & Attributes.PCOLOR_MASK].rgba; break; case Attributes.CM_RGB: - $fg = this.result.bg & Attributes.RGB_MASK << 8 | 0xFF; + $fg = ((this.result.bg & Attributes.RGB_MASK) << 8) | 0xFF; break; // No need to consider default bg color here as it's not possible } @@ -154,7 +154,7 @@ export class CellColorResolver { $fg = this._themeService.colors.ansi[this.result.fg & Attributes.PCOLOR_MASK].rgba; break; case Attributes.CM_RGB: - $fg = (this.result.fg & Attributes.RGB_MASK) << 8 | 0xFF; + $fg = ((this.result.fg & Attributes.RGB_MASK) << 8) | 0xFF; break; case Attributes.CM_DEFAULT: default: diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index 657fba44..29dc51c3 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -6,7 +6,7 @@ import { IImage32, decodePng } from '@lunapaint/png-codec'; import { LocatorScreenshotOptions, test } from '@playwright/test'; import { ITheme } from '@xterm/xterm'; -import { ITestContext, MaybeAsync, openTerminal, pollFor, pollForApproximate } from './TestUtils'; +import { ITestContext, MaybeAsync, openTerminal, pollFor, pollForApproximate, timeout } from './TestUtils'; export interface ISharedRendererTestContext { value: ITestContext; @@ -989,13 +989,15 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void }; await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`); await ctx.value.proxy.focus(); - await ctx.value.proxy.writeln('\x1b[41m red bg'); - await ctx.value.proxy.writeln('\x1b[7m inverse'); - await ctx.value.proxy.writeln('\x1b[31;7m red fg inverse'); + await ctx.value.proxy.writeln('\x1b[41m red bg\x1b[0m'); + await ctx.value.proxy.writeln('\x1b[7m inverse\x1b[0m'); + await ctx.value.proxy.writeln('\x1b[31;7m red fg inverse\x1b[0m'); + await ctx.value.proxy.writeln('\x1b[48:2:0:204:0:0m red truecolor bg\x1b[0m'); await ctx.value.proxy.selectAll(); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [230,128,128,255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [255,255,255,255]); - await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 3), [230,128,128,255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [230, 128, 128, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 2), [255, 255, 255, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 3), [230, 128, 128, 255]); + await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 4), [230, 128, 128, 255]); }); test('powerline decorative symbols', async () => { const theme: ITheme = { From f1e563ad5ffe732a9709432101fec212faca8f48 Mon Sep 17 00:00:00 2001 From: arencoskun Date: Thu, 1 Feb 2024 11:17:25 +0300 Subject: [PATCH 41/76] Expose API method for writing to application side (#4948) --- src/browser/Terminal.ts | 9 +++++++++ src/browser/TestUtils.test.ts | 3 +++ src/browser/public/Terminal.ts | 3 +++ test/playwright/TestUtils.ts | 1 + typings/xterm.d.ts | 6 ++++++ 5 files changed, 22 insertions(+) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 0e945aa9..6e06855a 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -1191,6 +1191,15 @@ export class Terminal extends CoreTerminal implements ITerminal { return false; } + + /** + * Input data to application side. + * The data is treated the same way as typed input at the terminal (will appear in the onData event). + */ + public input(data: string): void { + this.coreService.triggerDataEvent(data, true); + return this.write(data); + } /** * Resizes the terminal. diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 59cc773a..95f1fbac 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -72,6 +72,9 @@ export class MockTerminal implements ITerminal { public focus(): void { throw new Error('Method not implemented.'); } + public input(data: string): void { + throw new Error('Method not implemented.'); + } public resize(columns: number, rows: number): void { throw new Error('Method not implemented.'); } diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index ade46fa4..d8c8315d 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -138,6 +138,9 @@ export class Terminal extends Disposable implements ITerminalApi { public focus(): void { this._core.focus(); } + public input(data: string): void { + this._core.input(data); + } public resize(columns: number, rows: number): void { this._verifyIntegers(columns, rows); this._core.resize(columns, rows); diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index 4d4112f0..facbc881 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -216,6 +216,7 @@ export class TerminalProxy implements ITerminalProxyCustomMethods, PlaywrightApi return new Promise(r => term.writeln(typeof data === 'string' ? data : new Uint8Array(data), r)); }, [await this.getHandle(), typeof data === 'string' ? data : Array.from(data)] as const); } + public async input(data: string): Promise { return this.evaluate(([term]) => term.input(data)); } public async resize(cols: number, rows: number): Promise { return this._page.evaluate(([term, cols, rows]) => term.resize(cols, rows), [await this.getHandle(), cols, rows] as const); } public async registerMarker(y?: number | undefined): Promise { return this._page.evaluate(([term, y]) => term.registerMarker(y), [await this.getHandle(), y] as const); } public async registerDecoration(decorationOptions: IDecorationOptions): Promise { return this._page.evaluate(([term, decorationOptions]) => term.registerDecoration(decorationOptions), [await this.getHandle(), decorationOptions] as const); } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 39a9c91a..92680639 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -962,6 +962,12 @@ declare module '@xterm/xterm' { * Focus the terminal. */ focus(): void; + + /** + * Input data to application side. + * The data is treated the same way as typed input at the terminal (will appear in the onData event). + */ + input(data: string): void; /** * Resizes the terminal. It's best practice to debounce calls to resize, From bbd1a1dbe2aac4d0463f35d9fdaf2730bdfbcd7f Mon Sep 17 00:00:00 2001 From: arencoskun Date: Thu, 1 Feb 2024 13:05:13 +0300 Subject: [PATCH 42/76] Try to fix linter errors --- src/browser/Terminal.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 6e06855a..0b4d2e70 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -1193,9 +1193,10 @@ export class Terminal extends CoreTerminal implements ITerminal { } /** - * Input data to application side. - * The data is treated the same way as typed input at the terminal (will appear in the onData event). - */ + * Input data to application side. + * The data is treated the same way as typed input at the terminal. + * (will appear in the onData event). + */ public input(data: string): void { this.coreService.triggerDataEvent(data, true); return this.write(data); From 09a0dd658d20b8ab623a7a85f1f7e29c36ef9e85 Mon Sep 17 00:00:00 2001 From: arencoskun Date: Thu, 1 Feb 2024 13:11:54 +0300 Subject: [PATCH 43/76] Final fixes --- src/browser/Terminal.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 0b4d2e70..b680cc97 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -1191,15 +1191,15 @@ export class Terminal extends CoreTerminal implements ITerminal { return false; } - + /** - * Input data to application side. - * The data is treated the same way as typed input at the terminal. - * (will appear in the onData event). - */ + * Input data to application side. + * The data is treated the same way as typed input at the terminal. + * (will appear in the onData event). + */ public input(data: string): void { this.coreService.triggerDataEvent(data, true); - return this.write(data); + this.write(data); } /** From cae42772e88732a29d7980d4bbb5661f59aaa859 Mon Sep 17 00:00:00 2001 From: arencoskun Date: Thu, 1 Feb 2024 14:11:29 +0300 Subject: [PATCH 44/76] Move function to CoreTerminal, add additional argument to specify if the input was user input or not. --- src/browser/Terminal.ts | 10 ---------- src/browser/TestUtils.test.ts | 2 +- src/browser/public/Terminal.ts | 4 ++-- src/common/CoreTerminal.ts | 13 +++++++++++++ test/playwright/TestUtils.ts | 2 +- typings/xterm.d.ts | 6 +++++- 6 files changed, 22 insertions(+), 15 deletions(-) diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index b680cc97..0e945aa9 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -1192,16 +1192,6 @@ export class Terminal extends CoreTerminal implements ITerminal { return false; } - /** - * Input data to application side. - * The data is treated the same way as typed input at the terminal. - * (will appear in the onData event). - */ - public input(data: string): void { - this.coreService.triggerDataEvent(data, true); - this.write(data); - } - /** * Resizes the terminal. * diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 95f1fbac..c7c8438c 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -72,7 +72,7 @@ export class MockTerminal implements ITerminal { public focus(): void { throw new Error('Method not implemented.'); } - public input(data: string): void { + public input(data: string, wasUserInput: boolean = true): void { throw new Error('Method not implemented.'); } public resize(columns: number, rows: number): void { diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index d8c8315d..a6349225 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -138,8 +138,8 @@ export class Terminal extends Disposable implements ITerminalApi { public focus(): void { this._core.focus(); } - public input(data: string): void { - this._core.input(data); + public input(data: string, wasUserInput: boolean = true): void { + this._core.input(data, wasUserInput); } public resize(columns: number, rows: number): void { this._verifyIntegers(columns, rows); diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 1789daf8..b6242458 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -168,6 +168,19 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._writeBuffer.writeSync(data, maxSubsequentCalls); } + /** + * Input data to application side. + * The data is treated the same way as typed input at the terminal. + * (will appear in the onData event). + * wasUserInput indicates, whether the input is genuine user input. + * It is true by default and triggers additional actions like prompt focus or selection clearing. + * Set it to false if your data sent does not resemble what a user would have typed + * (e.g. sequence embedded data). + */ + public input(data: string, wasUserInput: boolean = true): void { + this.coreService.triggerDataEvent(data, wasUserInput); + } + public resize(x: number, y: number): void { if (isNaN(x) || isNaN(y)) { return; diff --git a/test/playwright/TestUtils.ts b/test/playwright/TestUtils.ts index facbc881..1427578c 100644 --- a/test/playwright/TestUtils.ts +++ b/test/playwright/TestUtils.ts @@ -216,7 +216,7 @@ export class TerminalProxy implements ITerminalProxyCustomMethods, PlaywrightApi return new Promise(r => term.writeln(typeof data === 'string' ? data : new Uint8Array(data), r)); }, [await this.getHandle(), typeof data === 'string' ? data : Array.from(data)] as const); } - public async input(data: string): Promise { return this.evaluate(([term]) => term.input(data)); } + public async input(data: string, wasUserInput: boolean = true): Promise { return this.evaluate(([term]) => term.input(data, wasUserInput)); } public async resize(cols: number, rows: number): Promise { return this._page.evaluate(([term, cols, rows]) => term.resize(cols, rows), [await this.getHandle(), cols, rows] as const); } public async registerMarker(y?: number | undefined): Promise { return this._page.evaluate(([term, y]) => term.registerMarker(y), [await this.getHandle(), y] as const); } public async registerDecoration(decorationOptions: IDecorationOptions): Promise { return this._page.evaluate(([term, decorationOptions]) => term.registerDecoration(decorationOptions), [await this.getHandle(), decorationOptions] as const); } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 92680639..95c368b1 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -966,8 +966,12 @@ declare module '@xterm/xterm' { /** * Input data to application side. * The data is treated the same way as typed input at the terminal (will appear in the onData event). + * wasUserInput indicates, whether the input is genuine user input. + * It is true by default and triggers additional actions like prompt focus or selection clearing. + * Set it to false if your data sent does not resemble what a user would have typed + * (e.g. sequence embedded data). */ - input(data: string): void; + input(data: string, wasUserInput?: boolean): void; /** * Resizes the terminal. It's best practice to debounce calls to resize, From c51a0b745439b60cda5c49d4ecf2873bf08e1dae Mon Sep 17 00:00:00 2001 From: arencoskun Date: Thu, 1 Feb 2024 16:05:02 +0300 Subject: [PATCH 45/76] Fix linter errors (hopefully) --- typings/xterm.d.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 95c368b1..c8189e18 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -962,14 +962,16 @@ declare module '@xterm/xterm' { * Focus the terminal. */ focus(): void; - + /** * Input data to application side. - * The data is treated the same way as typed input at the terminal (will appear in the onData event). + * The data is treated the same way as typed input at the terminal + * (will appear in the onData event). * wasUserInput indicates, whether the input is genuine user input. - * It is true by default and triggers additional actions like prompt focus or selection clearing. - * Set it to false if your data sent does not resemble what a user would have typed - * (e.g. sequence embedded data). + * It is true by default and triggers additional actions like prompt + * focus or selection clearing. + * Set it to false if your data sent does not resemble + * what a user would have typed (e.g. sequence embedded data). */ input(data: string, wasUserInput?: boolean): void; From 06aa2c86df8fcd8e60c1d8df9e02513438db3bb0 Mon Sep 17 00:00:00 2001 From: arencoskun Date: Thu, 1 Feb 2024 16:32:31 +0300 Subject: [PATCH 46/76] Whitespace fix --- typings/xterm.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index c8189e18..3de30524 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -968,7 +968,7 @@ declare module '@xterm/xterm' { * The data is treated the same way as typed input at the terminal * (will appear in the onData event). * wasUserInput indicates, whether the input is genuine user input. - * It is true by default and triggers additional actions like prompt + * It is true by default and triggers additional actions like prompt * focus or selection clearing. * Set it to false if your data sent does not resemble * what a user would have typed (e.g. sequence embedded data). From cddc888065e849e17543ec7f46394688e1ce09f8 Mon Sep 17 00:00:00 2001 From: arencoskun Date: Thu, 1 Feb 2024 16:49:15 +0300 Subject: [PATCH 47/76] Add definition to xterm-headless.d.ts --- typings/xterm-headless.d.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index f8cef382..81a6aad1 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -718,6 +718,18 @@ declare module '@xterm/headless' { */ onTitleChange: IEvent; + /** + * Input data to application side. + * The data is treated the same way as typed input at the terminal + * (will appear in the onData event). + * wasUserInput indicates, whether the input is genuine user input. + * It is true by default and triggers additional actions like prompt + * focus or selection clearing. + * Set it to false if your data sent does not resemble + * what a user would have typed (e.g. sequence embedded data). + */ + input(data: string, wasUserInput?: boolean): void; + /** * Resizes the terminal. It's best practice to debounce calls to resize, * this will help ensure that the pty can respond to the resize event From dce6e31c89531c3345e4f4fd56acdc7484fa9a63 Mon Sep 17 00:00:00 2001 From: arencoskun Date: Fri, 2 Feb 2024 08:59:57 +0300 Subject: [PATCH 48/76] Add headless definitons --- src/headless/Terminal.ts | 13 +++++++++++++ src/headless/public/Terminal.ts | 3 +++ 2 files changed, 16 insertions(+) diff --git a/src/headless/Terminal.ts b/src/headless/Terminal.ts index 18000c8f..c1e4950f 100644 --- a/src/headless/Terminal.ts +++ b/src/headless/Terminal.ts @@ -81,6 +81,19 @@ export class Terminal extends CoreTerminal { this._onBell.fire(); } + /** + * Input data to application side. + * The data is treated the same way as typed input at the terminal. + * (will appear in the onData event). + * wasUserInput indicates, whether the input is genuine user input. + * It is true by default and triggers additional actions like prompt focus or selection clearing. + * Set it to false if your data sent does not resemble what a user would have typed + * (e.g. sequence embedded data). + */ + public input(data: string, wasUserInput: boolean = true): void { + this.coreService.triggerDataEvent(data, wasUserInput); + } + /** * Resizes the terminal. * diff --git a/src/headless/public/Terminal.ts b/src/headless/public/Terminal.ts index df202660..0d73f9d3 100644 --- a/src/headless/public/Terminal.ts +++ b/src/headless/public/Terminal.ts @@ -134,6 +134,9 @@ export class Terminal extends Disposable implements ITerminalApi { this._publicOptions[propName] = options[propName]; } } + public input(data: string, wasUserInput: boolean = true): void { + this._core.input(data, wasUserInput); + } public resize(columns: number, rows: number): void { this._verifyIntegers(columns, rows); this._core.resize(columns, rows); From b1a72b2e839755c0b86754d475e9b8e62450a3ba Mon Sep 17 00:00:00 2001 From: arencoskun Date: Fri, 2 Feb 2024 09:04:53 +0300 Subject: [PATCH 49/76] Linter fix --- src/headless/Terminal.ts | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/headless/Terminal.ts b/src/headless/Terminal.ts index c1e4950f..a5f7736a 100644 --- a/src/headless/Terminal.ts +++ b/src/headless/Terminal.ts @@ -81,15 +81,15 @@ export class Terminal extends CoreTerminal { this._onBell.fire(); } - /** - * Input data to application side. - * The data is treated the same way as typed input at the terminal. - * (will appear in the onData event). - * wasUserInput indicates, whether the input is genuine user input. - * It is true by default and triggers additional actions like prompt focus or selection clearing. - * Set it to false if your data sent does not resemble what a user would have typed - * (e.g. sequence embedded data). - */ + /** + * Input data to application side. + * The data is treated the same way as typed input at the terminal. + * (will appear in the onData event). + * wasUserInput indicates, whether the input is genuine user input. + * It is true by default and triggers additional actions like prompt focus or selection clearing. + * Set it to false if your data sent does not resemble what a user would have typed + * (e.g. sequence embedded data). + */ public input(data: string, wasUserInput: boolean = true): void { this.coreService.triggerDataEvent(data, wasUserInput); } From 546bcb51336eb6aec3bb4a61074d2cee237a4f02 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 2 Feb 2024 09:58:21 -0800 Subject: [PATCH 50/76] Prevent npe in render service See microsoft/vscode#204104 --- src/browser/services/RenderService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index c2cb9a05..a184641b 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -247,7 +247,7 @@ export class RenderService extends Disposable implements IRenderService { return; } if (this._isPaused) { - this._pausedResizeTask.set(() => this._renderer.value!.handleResize(cols, rows)); + this._pausedResizeTask.set(() => this._renderer.value?.handleResize(cols, rows)); } else { this._renderer.value.handleResize(cols, rows); } From 8d1771ef646a27c764b3edb7d9d72dcd100359b2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 2 Feb 2024 12:03:08 -0800 Subject: [PATCH 51/76] Tweak docs --- src/common/CoreTerminal.ts | 9 --------- src/headless/Terminal.ts | 9 --------- typings/xterm-headless.d.ts | 16 ++++++++-------- typings/xterm.d.ts | 16 ++++++++-------- 4 files changed, 16 insertions(+), 34 deletions(-) diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index b6242458..327b8bc2 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -168,15 +168,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._writeBuffer.writeSync(data, maxSubsequentCalls); } - /** - * Input data to application side. - * The data is treated the same way as typed input at the terminal. - * (will appear in the onData event). - * wasUserInput indicates, whether the input is genuine user input. - * It is true by default and triggers additional actions like prompt focus or selection clearing. - * Set it to false if your data sent does not resemble what a user would have typed - * (e.g. sequence embedded data). - */ public input(data: string, wasUserInput: boolean = true): void { this.coreService.triggerDataEvent(data, wasUserInput); } diff --git a/src/headless/Terminal.ts b/src/headless/Terminal.ts index a5f7736a..66040756 100644 --- a/src/headless/Terminal.ts +++ b/src/headless/Terminal.ts @@ -81,15 +81,6 @@ export class Terminal extends CoreTerminal { this._onBell.fire(); } - /** - * Input data to application side. - * The data is treated the same way as typed input at the terminal. - * (will appear in the onData event). - * wasUserInput indicates, whether the input is genuine user input. - * It is true by default and triggers additional actions like prompt focus or selection clearing. - * Set it to false if your data sent does not resemble what a user would have typed - * (e.g. sequence embedded data). - */ public input(data: string, wasUserInput: boolean = true): void { this.coreService.triggerDataEvent(data, wasUserInput); } diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 81a6aad1..b2ad5745 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -719,14 +719,14 @@ declare module '@xterm/headless' { onTitleChange: IEvent; /** - * Input data to application side. - * The data is treated the same way as typed input at the terminal - * (will appear in the onData event). - * wasUserInput indicates, whether the input is genuine user input. - * It is true by default and triggers additional actions like prompt - * focus or selection clearing. - * Set it to false if your data sent does not resemble - * what a user would have typed (e.g. sequence embedded data). + * Input data to application side. The data is treated the same way input + * typed into the terminal would (ie. the {@link onData} event will fire). + * @param data The data to forward to the application. + * @param wasUserInput Whether the input is genuine user input. This is true + * by default and triggers additionalbehavior like focus or selection + * clearing. Set this to false if the data sent should not be treated like + * user input would, for example passing an escape sequence to the + * application. */ input(data: string, wasUserInput?: boolean): void; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 3de30524..d957d655 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -964,14 +964,14 @@ declare module '@xterm/xterm' { focus(): void; /** - * Input data to application side. - * The data is treated the same way as typed input at the terminal - * (will appear in the onData event). - * wasUserInput indicates, whether the input is genuine user input. - * It is true by default and triggers additional actions like prompt - * focus or selection clearing. - * Set it to false if your data sent does not resemble - * what a user would have typed (e.g. sequence embedded data). + * Input data to application side. The data is treated the same way input + * typed into the terminal would (ie. the {@link onData} event will fire). + * @param data The data to forward to the application. + * @param wasUserInput Whether the input is genuine user input. This is true + * by default and triggers additionalbehavior like focus or selection + * clearing. Set this to false if the data sent should not be treated like + * user input would, for example passing an escape sequence to the + * application. */ input(data: string, wasUserInput?: boolean): void; From 547bda6e4fa03386448148664f05aaf5c672f91f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 22 Feb 2024 09:33:21 +0100 Subject: [PATCH 52/76] fix #4964 --- addons/addon-web-links/src/WebLinkProvider.ts | 30 ++++++++----------- addons/addon-web-links/src/WebLinksAddon.ts | 2 +- .../addon-web-links/test/WebLinksAddon.api.ts | 13 ++++++++ 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/addons/addon-web-links/src/WebLinkProvider.ts b/addons/addon-web-links/src/WebLinkProvider.ts index 713f9c23..63211dad 100644 --- a/addons/addon-web-links/src/WebLinkProvider.ts +++ b/addons/addon-web-links/src/WebLinkProvider.ts @@ -41,16 +41,18 @@ export class WebLinkProvider implements ILinkProvider { } } -function baseUrlString(url: URL): string { - if (url.password && url.username) { - return `${url.protocol}//${url.username}:${url.password}@${url.host}`; +function isUrl(urlString: string): boolean { + try { + const url = new URL(urlString); + const parsedBase = url.password && url.username + ? `${url.protocol}//${url.username}:${url.password}@${url.host}` + : url.username + ? `${url.protocol}//${url.username}@${url.host}` + : `${url.protocol}//${url.host}`; + return urlString.toLocaleLowerCase().startsWith(parsedBase); + } catch (e) { + return false; } - - if (url.username) { - return `${url.protocol}//${url.username}@${url.host}`; - } - - return `${url.protocol}//${url.host}`; } export class LinkComputer { @@ -67,15 +69,7 @@ export class LinkComputer { const text = match[0]; // check via URL if the matched text would form a proper url - // NOTE: This outsources the ugly url parsing to the browser. - // we check if the provided string resembles the URL-parsed one - // up to the end of the domain name (ignoring path and params) - try { - const url = new URL(text); - if (!text.startsWith(baseUrlString(url))) { - continue; - } - } catch (e) { + if (!isUrl(text)) { continue; } diff --git a/addons/addon-web-links/src/WebLinksAddon.ts b/addons/addon-web-links/src/WebLinksAddon.ts index 8902d8e0..b3f0548c 100644 --- a/addons/addon-web-links/src/WebLinksAddon.ts +++ b/addons/addon-web-links/src/WebLinksAddon.ts @@ -18,7 +18,7 @@ import { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider'; // - final interpunction like ,.!? // - any sort of brackets <>()[]{} (not spec conform, but often used to enclose urls) // - unsafe chars from rfc1738: {}|\^~[]` -const strictUrlRegex = /https?:[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/; +const strictUrlRegex = /(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/; function handleLink(event: MouseEvent, uri: string): void { diff --git a/addons/addon-web-links/test/WebLinksAddon.api.ts b/addons/addon-web-links/test/WebLinksAddon.api.ts index 5b05ce2e..26a7421d 100644 --- a/addons/addon-web-links/test/WebLinksAddon.api.ts +++ b/addons/addon-web-links/test/WebLinksAddon.api.ts @@ -123,6 +123,19 @@ describe('WebLinksAddon', () => { await evalLinkStateData('http://test:password@example.com/some_path?param=1%202%3', { start: { x: 12, y: 1 }, end: { x: 27, y: 2 } }); }); }); + + // issue #4964 + it.only('uppercase in protocol and host, default ports', async () => { + const data = ` HTTP://EXAMPLE.COM \\r\\n` + + ` HTTPS://Example.com \\r\\n` + + ` HTTP://Example.com:80 \\r\\n` + + ` HTTP://Example.com:80/staysUpper \\r\\n`; + await writeSync(page, data); + await pollForLinkAtCell(3, 0, `HTTP://EXAMPLE.COM`); + await pollForLinkAtCell(3, 1, `HTTPS://Example.com`); + await pollForLinkAtCell(3, 2, `HTTP://Example.com:80`); + await pollForLinkAtCell(3, 3, `HTTP://Example.com:80/staysUpper`); + }); }); async function testHostName(hostname: string): Promise { From 768452a519e96bb53faa1eb7e306113295c6346b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 22 Feb 2024 09:38:36 +0100 Subject: [PATCH 53/76] make linter happy --- addons/addon-web-links/test/WebLinksAddon.api.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/addons/addon-web-links/test/WebLinksAddon.api.ts b/addons/addon-web-links/test/WebLinksAddon.api.ts index 26a7421d..e4e5ce94 100644 --- a/addons/addon-web-links/test/WebLinksAddon.api.ts +++ b/addons/addon-web-links/test/WebLinksAddon.api.ts @@ -126,15 +126,15 @@ describe('WebLinksAddon', () => { // issue #4964 it.only('uppercase in protocol and host, default ports', async () => { - const data = ` HTTP://EXAMPLE.COM \\r\\n` + + const data = ` HTTP://EXAMPLE.COM \\r\\n` + ` HTTPS://Example.com \\r\\n` + ` HTTP://Example.com:80 \\r\\n` + ` HTTP://Example.com:80/staysUpper \\r\\n`; - await writeSync(page, data); - await pollForLinkAtCell(3, 0, `HTTP://EXAMPLE.COM`); - await pollForLinkAtCell(3, 1, `HTTPS://Example.com`); - await pollForLinkAtCell(3, 2, `HTTP://Example.com:80`); - await pollForLinkAtCell(3, 3, `HTTP://Example.com:80/staysUpper`); + await writeSync(page, data); + await pollForLinkAtCell(3, 0, `HTTP://EXAMPLE.COM`); + await pollForLinkAtCell(3, 1, `HTTPS://Example.com`); + await pollForLinkAtCell(3, 2, `HTTP://Example.com:80`); + await pollForLinkAtCell(3, 3, `HTTP://Example.com:80/staysUpper`); }); }); From 5143f904ee7926740f194328356594ebf2c9a8b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 22 Feb 2024 09:40:38 +0100 Subject: [PATCH 54/76] remove leftover .only --- addons/addon-web-links/test/WebLinksAddon.api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/addons/addon-web-links/test/WebLinksAddon.api.ts b/addons/addon-web-links/test/WebLinksAddon.api.ts index e4e5ce94..6c3c8b1e 100644 --- a/addons/addon-web-links/test/WebLinksAddon.api.ts +++ b/addons/addon-web-links/test/WebLinksAddon.api.ts @@ -125,7 +125,7 @@ describe('WebLinksAddon', () => { }); // issue #4964 - it.only('uppercase in protocol and host, default ports', async () => { + it('uppercase in protocol and host, default ports', async () => { const data = ` HTTP://EXAMPLE.COM \\r\\n` + ` HTTPS://Example.com \\r\\n` + ` HTTP://Example.com:80 \\r\\n` + From 10fe2498777abe45691ec427826299c476a85e01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Thu, 22 Feb 2024 09:55:42 +0100 Subject: [PATCH 55/76] allow user+password to still match --- addons/addon-web-links/src/WebLinkProvider.ts | 2 +- addons/addon-web-links/test/WebLinksAddon.api.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/addons/addon-web-links/src/WebLinkProvider.ts b/addons/addon-web-links/src/WebLinkProvider.ts index 63211dad..66691f44 100644 --- a/addons/addon-web-links/src/WebLinkProvider.ts +++ b/addons/addon-web-links/src/WebLinkProvider.ts @@ -49,7 +49,7 @@ function isUrl(urlString: string): boolean { : url.username ? `${url.protocol}//${url.username}@${url.host}` : `${url.protocol}//${url.host}`; - return urlString.toLocaleLowerCase().startsWith(parsedBase); + return urlString.toLocaleLowerCase().startsWith(parsedBase.toLocaleLowerCase()); } catch (e) { return false; } diff --git a/addons/addon-web-links/test/WebLinksAddon.api.ts b/addons/addon-web-links/test/WebLinksAddon.api.ts index 6c3c8b1e..99c11600 100644 --- a/addons/addon-web-links/test/WebLinksAddon.api.ts +++ b/addons/addon-web-links/test/WebLinksAddon.api.ts @@ -129,12 +129,14 @@ describe('WebLinksAddon', () => { const data = ` HTTP://EXAMPLE.COM \\r\\n` + ` HTTPS://Example.com \\r\\n` + ` HTTP://Example.com:80 \\r\\n` + - ` HTTP://Example.com:80/staysUpper \\r\\n`; + ` HTTP://Example.com:80/staysUpper \\r\\n` + + ` HTTP://Ab:xY@abc.com:80/staysUpper \\r\\n`; await writeSync(page, data); await pollForLinkAtCell(3, 0, `HTTP://EXAMPLE.COM`); await pollForLinkAtCell(3, 1, `HTTPS://Example.com`); await pollForLinkAtCell(3, 2, `HTTP://Example.com:80`); await pollForLinkAtCell(3, 3, `HTTP://Example.com:80/staysUpper`); + await pollForLinkAtCell(3, 4, `HTTP://Ab:xY@abc.com:80/staysUpper`); }); }); From 8ef3c1084417e9b5ee7813c1e50f14a37f83f6ea Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 29 Feb 2024 07:37:12 -0800 Subject: [PATCH 56/76] Fix spacing issue when measuring before element is attached See microsoft/vscode#204690 --- src/browser/renderer/dom/WidthCache.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/browser/renderer/dom/WidthCache.ts b/src/browser/renderer/dom/WidthCache.ts index 1527bad0..03d6cb70 100644 --- a/src/browser/renderer/dom/WidthCache.ts +++ b/src/browser/renderer/dom/WidthCache.ts @@ -134,9 +134,14 @@ export class WidthCache implements IDisposable { public get(c: string, bold: boolean | number, italic: boolean | number): number { let cp = 0; if (!bold && !italic && c.length === 1 && (cp = c.charCodeAt(0)) < WidthCacheSettings.FLAT_SIZE) { - return this._flat[cp] !== WidthCacheSettings.FLAT_UNSET - ? this._flat[cp] - : (this._flat[cp] = this._measure(c, 0)); + if (this._flat[cp] !== WidthCacheSettings.FLAT_UNSET) { + return this._flat[cp]; + } + const width = this._measure(c, 0); + if (width > 0) { + this._flat[cp] = width; + } + return width; } let key = c; if (bold) key += 'B'; @@ -147,7 +152,9 @@ export class WidthCache implements IDisposable { if (bold) variant |= FontVariant.BOLD; if (italic) variant |= FontVariant.ITALIC; width = this._measure(c, variant); - this._holey!.set(key, width); + if (width > 0) { + this._holey!.set(key, width); + } } return width; } From 72dea6279ebb52e7425bb3f296809c28ad4b0c16 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 1 Mar 2024 08:39:19 -0800 Subject: [PATCH 57/76] v5.4.0 --- addons/addon-attach/package.json | 2 +- addons/addon-canvas/package.json | 2 +- addons/addon-fit/package.json | 2 +- addons/addon-image/package.json | 2 +- addons/addon-ligatures/package.json | 2 +- addons/addon-search/package.json | 2 +- addons/addon-serialize/package.json | 2 +- addons/addon-unicode-graphemes/package.json | 2 +- addons/addon-unicode11/package.json | 2 +- addons/addon-web-links/package.json | 2 +- addons/addon-webgl/package.json | 2 +- package.json | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/addons/addon-attach/package.json b/addons/addon-attach/package.json index 94b483dd..c2f42ce5 100644 --- a/addons/addon-attach/package.json +++ b/addons/addon-attach/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-attach", - "version": "0.9.0", + "version": "0.10.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/addon-canvas/package.json b/addons/addon-canvas/package.json index 9aabb001..8fdec96b 100644 --- a/addons/addon-canvas/package.json +++ b/addons/addon-canvas/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-canvas", - "version": "0.5.0", + "version": "0.6.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/addon-fit/package.json b/addons/addon-fit/package.json index 585f3621..be81b196 100644 --- a/addons/addon-fit/package.json +++ b/addons/addon-fit/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-fit", - "version": "0.8.0", + "version": "0.9.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/addon-image/package.json b/addons/addon-image/package.json index 8572e330..2e28955d 100644 --- a/addons/addon-image/package.json +++ b/addons/addon-image/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-image", - "version": "0.6.0", + "version": "0.7.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/addon-ligatures/package.json b/addons/addon-ligatures/package.json index e5c6b972..60166577 100644 --- a/addons/addon-ligatures/package.json +++ b/addons/addon-ligatures/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-ligatures", - "version": "0.7.0", + "version": "0.8.0", "description": "Add support for programming ligatures to xterm.js", "author": { "name": "The xterm.js authors", diff --git a/addons/addon-search/package.json b/addons/addon-search/package.json index 369d11c3..35a4015e 100644 --- a/addons/addon-search/package.json +++ b/addons/addon-search/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-search", - "version": "0.13.0", + "version": "0.14.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/addon-serialize/package.json b/addons/addon-serialize/package.json index 763c52ca..9c50288d 100644 --- a/addons/addon-serialize/package.json +++ b/addons/addon-serialize/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-serialize", - "version": "0.11.0", + "version": "0.12.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/addon-unicode-graphemes/package.json b/addons/addon-unicode-graphemes/package.json index d49eda87..610e14b4 100644 --- a/addons/addon-unicode-graphemes/package.json +++ b/addons/addon-unicode-graphemes/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-unicode-graphemes", - "version": "0.1.0", + "version": "0.2.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/addon-unicode11/package.json b/addons/addon-unicode11/package.json index ad6a4892..e511c65a 100644 --- a/addons/addon-unicode11/package.json +++ b/addons/addon-unicode11/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-unicode11", - "version": "0.6.0", + "version": "0.7.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/addon-web-links/package.json b/addons/addon-web-links/package.json index 6367907a..6a65f289 100644 --- a/addons/addon-web-links/package.json +++ b/addons/addon-web-links/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-web-links", - "version": "0.9.0", + "version": "0.10.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/addons/addon-webgl/package.json b/addons/addon-webgl/package.json index 9a31c306..f504bb7e 100644 --- a/addons/addon-webgl/package.json +++ b/addons/addon-webgl/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-webgl", - "version": "0.16.0", + "version": "0.17.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/package.json b/package.json index 050b8cad..dd425db6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@xterm/xterm", "description": "Full xterm terminal, in your browser", - "version": "5.3.0", + "version": "5.4.0", "main": "lib/xterm.js", "style": "css/xterm.css", "types": "typings/xterm.d.ts", From 6ca8dc54ad942be16d84fd7e37453a3ff08c80b2 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 1 Mar 2024 09:00:33 -0800 Subject: [PATCH 58/76] Fix publishing of stable --- bin/publish.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bin/publish.js b/bin/publish.js index 360e09fe..ad945534 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -57,9 +57,8 @@ function checkAndPublishPackage(packageDir) { const packageJson = require(path.join(packageDir, 'package.json')); // Determine if this is a stable or beta release - // TODO: Uncomment when publishing 5.4 - // const publishedVersions = getPublishedVersions(packageJson); - const isStableRelease = false; //!publishedVersions.includes(packageJson.version); + const publishedVersions = getPublishedVersions(packageJson); + const isStableRelease = !publishedVersions.includes(packageJson.version); // Get the next version let nextVersion = isStableRelease ? packageJson.version : getNextBetaVersion(packageJson); From da3aad4497e33c8757b4820c95bc8d7d70ade362 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 1 Mar 2024 09:18:18 -0800 Subject: [PATCH 59/76] Update script/css import in readme Part of #4859 --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7514e76c..c85ce317 100644 --- a/README.md +++ b/README.md @@ -30,8 +30,8 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t - - + +
From 17fd7389c48e45e7d7bbb00dd3852570c30a1071 Mon Sep 17 00:00:00 2001 From: Daniel Steinberg Date: Sat, 2 Mar 2024 03:46:10 +0000 Subject: [PATCH 60/76] Add @xterm scope to packages --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c85ce317..b055ea7b 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ Xterm.js is a front-end component written in TypeScript that lets applications b First, you need to install the module, we ship exclusively through [npm](https://www.npmjs.com/), so you need that installed and then add xterm.js as a dependency by running: ```bash -npm install xterm +npm install @xterm/xterm ``` To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to the head of your HTML page. Then create a `
` onto which xterm can attach itself. Finally, instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`. @@ -113,7 +113,7 @@ All current and past releases are available on this repo's [Releases page](https Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with: ```bash -npm install -S xterm@beta +npm install -S @xterm/xterm@beta ``` These should generally be stable, but some bugs may slip in. We recommend using the beta build primarily to test out new features and to verify bug fixes. From c32313a471bf91fbcc21ff69fdae8018f9009037 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 7 Mar 2024 05:21:59 -0800 Subject: [PATCH 61/76] Add default to api d.ts Fixes #4992 --- typings/xterm-headless.d.ts | 2 +- typings/xterm.d.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index b2ad5745..40034607 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -156,7 +156,7 @@ declare module '@xterm/headless' { /** * The amount of scrollback in the terminal. Scrollback is the amount of * rows that are retained when lines are scrolled beyond the initial - * viewport. + * viewport. Defaults to 1000. */ scrollback?: number; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d957d655..a46db4be 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -225,7 +225,7 @@ declare module '@xterm/xterm' { /** * The amount of scrollback in the terminal. Scrollback is the amount of * rows that are retained when lines are scrolled beyond the initial - * viewport. + * viewport. Defaults to 1000. */ scrollback?: number; From 33a7a9b6a2dc1e8781722bc9e6797e25f61ee87b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 14 Mar 2024 07:14:13 -0700 Subject: [PATCH 62/76] Implement rescaleOverlappingGlyphs in webgl Part of #4969 --- addons/addon-webgl/src/GlyphRenderer.ts | 27 ++++++++++++++++---- addons/addon-webgl/src/WebglRenderer.ts | 11 +++++--- src/browser/renderer/shared/RendererUtils.ts | 13 ++++++++++ src/browser/services/RenderService.ts | 3 ++- src/common/services/OptionsService.ts | 1 + src/common/services/Services.ts | 1 + typings/xterm-headless.d.ts | 11 ++++++++ typings/xterm.d.ts | 11 ++++++++ 8 files changed, 68 insertions(+), 10 deletions(-) diff --git a/addons/addon-webgl/src/GlyphRenderer.ts b/addons/addon-webgl/src/GlyphRenderer.ts index 1fb0e18c..7e221fc6 100644 --- a/addons/addon-webgl/src/GlyphRenderer.ts +++ b/addons/addon-webgl/src/GlyphRenderer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; +import { isEmoji, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas'; import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types'; import { NULL_CELL_CODE } from 'common/buffer/Constants'; @@ -11,6 +11,7 @@ import { Disposable, toDisposable } from 'common/Lifecycle'; import { Terminal } from '@xterm/xterm'; import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types'; import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils'; +import type { IOptionsService } from 'common/services/Services'; interface IVertices { attributes: Float32Array; @@ -111,7 +112,8 @@ export class GlyphRenderer extends Disposable { constructor( private readonly _terminal: Terminal, private readonly _gl: IWebGL2RenderingContext, - private _dimensions: IRenderDimensions + private _dimensions: IRenderDimensions, + private readonly _optionsService: IOptionsService ) { super(); @@ -212,15 +214,15 @@ export class GlyphRenderer extends Disposable { return this._atlas ? this._atlas.beginFrame() : true; } - public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, lastBg: number): void { + public updateCell(x: number, y: number, code: number, bg: number, fg: number, ext: number, chars: string, width: number, lastBg: number): void { // Since this function is called for every cell (`rows*cols`), it must be very optimized. It // should not instantiate any variables unless a new glyph is drawn to the cache where the // slight slowdown is acceptable for the developer ergonomics provided as it's a once of for // each glyph. - this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, lastBg); + this._updateCell(this._vertices.attributes, x, y, code, bg, fg, ext, chars, width, lastBg); } - private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, lastBg: number): void { + private _updateCell(array: Float32Array, x: number, y: number, code: number | undefined, bg: number, fg: number, ext: number, chars: string, width: number, lastBg: number): void { $i = (y * this._terminal.cols + x) * INDICES_PER_CELL; // Exit early if this is a null character, allow space character to continue as it may have @@ -275,6 +277,21 @@ export class GlyphRenderer extends Disposable { array[$i + 8] = $glyph.sizeClipSpace.y; } // a_cellpos only changes on resize + + // Reduce scale horizontally for wide glyphs printed in cells that would overlap with the + // following cell (ie. the width is not 2). + if (this._optionsService.rawOptions.rescaleOverlappingGlyphs) { + if ( + // Is single cell width + width === 1 && + // Glyph exceeds cell bounds, + 1 to avoid hurting readability + $glyph.size.x > this._dimensions.device.cell.width + 1 && + // Never rescale emoji + code && !isEmoji(code) + ) { + array[$i + 2] = (this._dimensions.device.cell.width - /* improve readability */1) / this._dimensions.device.canvas.width; // - 1 to improve readability + } + } } public clear(): void { diff --git a/addons/addon-webgl/src/WebglRenderer.ts b/addons/addon-webgl/src/WebglRenderer.ts index 3a01e244..fa178652 100644 --- a/addons/addon-webgl/src/WebglRenderer.ts +++ b/addons/addon-webgl/src/WebglRenderer.ts @@ -36,7 +36,8 @@ export class WebglRenderer extends Disposable implements IRenderer { private _observerDisposable = this.register(new MutableDisposable()); private _model: RenderModel = new RenderModel(); - private _workCell: CellData = new CellData(); + private _workCell: ICellData = new CellData(); + private _workCell2: ICellData = new CellData(); private _cellColorResolver: CellColorResolver; private _canvas: HTMLCanvasElement; @@ -245,7 +246,7 @@ export class WebglRenderer extends Disposable implements IRenderer { */ private _initializeWebGLState(): [RectangleRenderer, GlyphRenderer] { this._rectangleRenderer.value = new RectangleRenderer(this._terminal, this._gl, this.dimensions, this._themeService); - this._glyphRenderer.value = new GlyphRenderer(this._terminal, this._gl, this.dimensions); + this._glyphRenderer.value = new GlyphRenderer(this._terminal, this._gl, this.dimensions, this._optionsService); // Update dimensions and acquire char atlas this.handleCharSizeChanged(); @@ -388,6 +389,7 @@ export class WebglRenderer extends Disposable implements IRenderer { let range: [number, number]; let chars: string; let code: number; + let width: number; let i: number; let x: number; let j: number; @@ -500,7 +502,8 @@ export class WebglRenderer extends Disposable implements IRenderer { this._model.cells[i + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg; this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext; - this._glyphRenderer.value!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, lastBg); + width = cell.getWidth(); + this._glyphRenderer.value!.updateCell(x, y, code, this._cellColorResolver.result.bg, this._cellColorResolver.result.fg, this._cellColorResolver.result.ext, chars, width, lastBg); if (isJoined) { // Restore work cell @@ -509,7 +512,7 @@ export class WebglRenderer extends Disposable implements IRenderer { // Null out non-first cells for (x++; x < lastCharX; x++) { j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; - this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0); + this._glyphRenderer.value!.updateCell(x, y, NULL_CELL_CODE, 0, 0, 0, NULL_CELL_CHAR, 0, 0); this._model.cells[j] = NULL_CELL_CODE; this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg; this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg; diff --git a/src/browser/renderer/shared/RendererUtils.ts b/src/browser/renderer/shared/RendererUtils.ts index 9a4bffe0..b9fc7312 100644 --- a/src/browser/renderer/shared/RendererUtils.ts +++ b/src/browser/renderer/shared/RendererUtils.ts @@ -27,6 +27,19 @@ function isBoxOrBlockGlyph(codepoint: number): boolean { return 0x2500 <= codepoint && codepoint <= 0x259F; } +export function isEmoji(codepoint: number): boolean { + return ( + codepoint >= 0x1F600 && codepoint <= 0x1F64F || // Emoticons + codepoint >= 0x1F300 && codepoint <= 0x1F5FF || // Misc Symbols and Pictographs + codepoint >= 0x1F680 && codepoint <= 0x1F6FF || // Transport and Map + codepoint >= 0x2600 && codepoint <= 0x26FF || // Misc symbols + codepoint >= 0x2700 && codepoint <= 0x27BF || // Dingbats + codepoint >= 0xFE00 && codepoint <= 0xFE0F || // Variation Selectors + codepoint >= 0x1F900 && codepoint <= 0x1F9FF || // Supplemental Symbols and Pictographs + codepoint >= 0x1F1E6 && codepoint <= 0x1F1FF + ); +} + export function treatGlyphAsBackgroundColor(codepoint: number): boolean { return isPowerlineGlyph(codepoint) || isBoxOrBlockGlyph(codepoint); } diff --git a/src/browser/services/RenderService.ts b/src/browser/services/RenderService.ts index a184641b..d4f2be46 100644 --- a/src/browser/services/RenderService.ts +++ b/src/browser/services/RenderService.ts @@ -87,7 +87,8 @@ export class RenderService extends Disposable implements IRenderService { 'fontSize', 'fontWeight', 'fontWeightBold', - 'minimumContrastRatio' + 'minimumContrastRatio', + 'rescaleOverlappingGlyphs' ], () => { this.clear(); this.handleResize(bufferService.cols, bufferService.rows); diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index ba92992e..0375f6ad 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -44,6 +44,7 @@ export const DEFAULT_OPTIONS: Readonly> = { allowTransparency: false, tabStopWidth: 8, theme: {}, + rescaleOverlappingGlyphs: false, rightClickSelectsWord: isMac, windowOptions: {}, windowsMode: false, diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 304e8cbb..210a0afb 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -234,6 +234,7 @@ export interface ITerminalOptions { macOptionIsMeta?: boolean; macOptionClickForcesSelection?: boolean; minimumContrastRatio?: number; + rescaleOverlappingGlyphs?: boolean; rightClickSelectsWord?: boolean; rows?: number; screenReaderMode?: boolean; diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index b2ad5745..d27c39e0 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -140,6 +140,17 @@ declare module '@xterm/headless' { */ minimumContrastRatio?: number; + /** + * Whether to rescale glyphs horizontally that are a single cell wide but + * have glyphs that would overlap following cell(s). This typically happens + * for ambiguous width characters (eg. the roman numeral characters U+2160+) + * which aren't featured in monospace fonts. Emoji glyphs are never + * rescaled. This is an important feature for achieving GB18030 compliance. + * + * Note that this doesn't work with the DOM renderer. The default is false. + */ + rescaleOverlappingGlyphs?: boolean; + /** * Whether to select the word under the cursor on right click, this is * standard behavior in a lot of macOS applications. diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index d957d655..00d0e497 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -209,6 +209,17 @@ declare module '@xterm/xterm' { */ minimumContrastRatio?: number; + /** + * Whether to rescale glyphs horizontally that are a single cell wide but + * have glyphs that would overlap following cell(s). This typically happens + * for ambiguous width characters (eg. the roman numeral characters U+2160+) + * which aren't featured in monospace fonts. Emoji glyphs are never + * rescaled. This is an important feature for achieving GB18030 compliance. + * + * Note that this doesn't work with the DOM renderer. The default is false. + */ + rescaleOverlappingGlyphs?: boolean; + /** * Whether to select the word under the cursor on right click, this is * standard behavior in a lot of macOS applications. From df559e3444950e76e9f2b2cfddac1e66f8131dfa Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 14 Mar 2024 07:26:25 -0700 Subject: [PATCH 63/76] Rescale overlapping in canvas --- addons/addon-canvas/src/BaseRenderLayer.ts | 23 ++++++++++++++++++++-- addons/addon-webgl/src/GlyphRenderer.ts | 2 +- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/addons/addon-canvas/src/BaseRenderLayer.ts b/addons/addon-canvas/src/BaseRenderLayer.ts index e7e23400..0868ba8e 100644 --- a/addons/addon-canvas/src/BaseRenderLayer.ts +++ b/addons/addon-canvas/src/BaseRenderLayer.ts @@ -8,7 +8,7 @@ import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver'; import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache'; import { TEXT_BASELINE } from 'browser/renderer/shared/Constants'; import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs'; -import { throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; +import { isEmoji, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel'; import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types'; import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; @@ -365,6 +365,8 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer */ protected _drawChars(cell: ICellData, x: number, y: number): void { const chars = cell.getChars(); + const code = cell.getCode(); + const width = cell.getWidth(); this._cellColorResolver.resolve(cell, x, this._bufferService.buffer.ydisp + y, this._deviceCellWidth); if (!this._charAtlas) { @@ -400,6 +402,23 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer this._bitmapGenerator[glyph.texturePage]!.refresh(); this._bitmapGenerator[glyph.texturePage]!.version = this._charAtlas.pages[glyph.texturePage].version; } + + // Reduce scale horizontally for wide glyphs printed in cells that would overlap with the + // following cell (ie. the width is not 2). + let renderWidth = glyph.size.x; + if (this._optionsService.rawOptions.rescaleOverlappingGlyphs) { + if ( + // Is single cell width + width === 1 && + // Glyph exceeds cell bounds, + 1 to avoid hurting readability + glyph.size.x > this._deviceCellWidth + 1 && + // Never rescale emoji + code && !isEmoji(code) + ) { + renderWidth = this._deviceCellWidth - 1; // - 1 to improve readability + } + } + this._ctx.drawImage( this._bitmapGenerator[glyph.texturePage]?.bitmap || this._charAtlas!.pages[glyph.texturePage].canvas, glyph.texturePosition.x, @@ -408,7 +427,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer glyph.size.y, x * this._deviceCellWidth + this._deviceCharLeft - glyph.offset.x, y * this._deviceCellHeight + this._deviceCharTop - glyph.offset.y, - glyph.size.x, + renderWidth, glyph.size.y ); this._ctx.restore(); diff --git a/addons/addon-webgl/src/GlyphRenderer.ts b/addons/addon-webgl/src/GlyphRenderer.ts index 7e221fc6..7ac89084 100644 --- a/addons/addon-webgl/src/GlyphRenderer.ts +++ b/addons/addon-webgl/src/GlyphRenderer.ts @@ -289,7 +289,7 @@ export class GlyphRenderer extends Disposable { // Never rescale emoji code && !isEmoji(code) ) { - array[$i + 2] = (this._dimensions.device.cell.width - /* improve readability */1) / this._dimensions.device.canvas.width; // - 1 to improve readability + array[$i + 2] = (this._dimensions.device.cell.width - 1) / this._dimensions.device.canvas.width; // - 1 to improve readability } } } From 48c6e96a78d4a4ba76f12786676b4e904ad8124d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 15 Mar 2024 08:18:26 -0700 Subject: [PATCH 64/76] Don't rescale powerline or nerd fonts Part of #4969 --- src/browser/renderer/shared/RendererUtils.ts | 29 ++++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/browser/renderer/shared/RendererUtils.ts b/src/browser/renderer/shared/RendererUtils.ts index b9fc7312..f99c23a1 100644 --- a/src/browser/renderer/shared/RendererUtils.ts +++ b/src/browser/renderer/shared/RendererUtils.ts @@ -23,6 +23,10 @@ export function isRestrictedPowerlineGlyph(codepoint: number): boolean { return 0xE0B0 <= codepoint && codepoint <= 0xE0B7; } +function isNerdFontGlyph(codepoint: number): boolean { + return 0xE000 <= codepoint && codepoint <= 0xF8FF; +} + function isBoxOrBlockGlyph(codepoint: number): boolean { return 0x2500 <= codepoint && codepoint <= 0x259F; } @@ -30,16 +34,29 @@ function isBoxOrBlockGlyph(codepoint: number): boolean { export function isEmoji(codepoint: number): boolean { return ( codepoint >= 0x1F600 && codepoint <= 0x1F64F || // Emoticons - codepoint >= 0x1F300 && codepoint <= 0x1F5FF || // Misc Symbols and Pictographs - codepoint >= 0x1F680 && codepoint <= 0x1F6FF || // Transport and Map - codepoint >= 0x2600 && codepoint <= 0x26FF || // Misc symbols - codepoint >= 0x2700 && codepoint <= 0x27BF || // Dingbats - codepoint >= 0xFE00 && codepoint <= 0xFE0F || // Variation Selectors - codepoint >= 0x1F900 && codepoint <= 0x1F9FF || // Supplemental Symbols and Pictographs + codepoint >= 0x1F300 && codepoint <= 0x1F5FF || // Misc Symbols and Pictographs + codepoint >= 0x1F680 && codepoint <= 0x1F6FF || // Transport and Map + codepoint >= 0x2600 && codepoint <= 0x26FF || // Misc symbols + codepoint >= 0x2700 && codepoint <= 0x27BF || // Dingbats + codepoint >= 0xFE00 && codepoint <= 0xFE0F || // Variation Selectors + codepoint >= 0x1F900 && codepoint <= 0x1F9FF || // Supplemental Symbols and Pictographs codepoint >= 0x1F1E6 && codepoint <= 0x1F1FF ); } +export function allowRescaling(codepoint: number | undefined, width: number, glyphSizeX: number, deviceCellWidth: number): boolean { + return ( + // Is single cell width + width === 1 && + // Glyph exceeds cell bounds, + 1 to avoid hurting readability + glyphSizeX > deviceCellWidth + 1 && + // Never rescale emoji + codepoint !== undefined && !isEmoji(codepoint) && + // Never rescale powerline or nerd fonts + !isPowerlineGlyph(codepoint) && !isNerdFontGlyph(codepoint) + ); +} + export function treatGlyphAsBackgroundColor(codepoint: number): boolean { return isPowerlineGlyph(codepoint) || isBoxOrBlockGlyph(codepoint); } From 6b3d485d774d96041afbfee13189f1b34950f1f7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 15 Mar 2024 08:30:55 -0700 Subject: [PATCH 65/76] Note new excluded glyphs in API --- typings/xterm-headless.d.ts | 10 ++++++++-- typings/xterm.d.ts | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 9c1feaea..5e84f266 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -144,8 +144,14 @@ declare module '@xterm/headless' { * Whether to rescale glyphs horizontally that are a single cell wide but * have glyphs that would overlap following cell(s). This typically happens * for ambiguous width characters (eg. the roman numeral characters U+2160+) - * which aren't featured in monospace fonts. Emoji glyphs are never - * rescaled. This is an important feature for achieving GB18030 compliance. + * which aren't featured in monospace fonts. This is an important feature + * for achieving GB18030 compliance. + * + * The following glyphs will never be rescaled: + * + * - Emoji glyphs + * - Powerline glyphs + * - Nerd font glyphs * * Note that this doesn't work with the DOM renderer. The default is false. */ diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index b4e01d84..33008289 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -213,8 +213,14 @@ declare module '@xterm/xterm' { * Whether to rescale glyphs horizontally that are a single cell wide but * have glyphs that would overlap following cell(s). This typically happens * for ambiguous width characters (eg. the roman numeral characters U+2160+) - * which aren't featured in monospace fonts. Emoji glyphs are never - * rescaled. This is an important feature for achieving GB18030 compliance. + * which aren't featured in monospace fonts. This is an important feature + * for achieving GB18030 compliance. + * + * The following glyphs will never be rescaled: + * + * - Emoji glyphs + * - Powerline glyphs + * - Nerd font glyphs * * Note that this doesn't work with the DOM renderer. The default is false. */ From 47409f39f684c417717d885b2cba56dd918d591a Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 16 Mar 2024 09:05:45 -0700 Subject: [PATCH 66/76] Set up shared context on browser even if process exists Fixes #4995 --- src/common/Color.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/common/Color.ts b/src/common/Color.ts index 5ec2d87d..b7b3ff47 100644 --- a/src/common/Color.ts +++ b/src/common/Color.ts @@ -3,7 +3,6 @@ * @license MIT */ -import { isNode } from 'common/Platform'; import { IColor, IColorRGB } from 'common/Types'; let $r = 0; @@ -117,9 +116,10 @@ export namespace color { * '#rrggbbaa'). */ export namespace css { + // Attempt to set get the shared canvas context let $ctx: CanvasRenderingContext2D | undefined; let $litmusColor: CanvasGradient | undefined; - if (!isNode) { + try { // This is guaranteed to run in the first window, so document should be correct const canvas = document.createElement('canvas'); canvas.width = 1; @@ -133,6 +133,9 @@ export namespace css { $litmusColor = $ctx.createLinearGradient(0, 0, 1, 1); } } + catch { + // noop + } /** * Converts a css string to an IColor, this should handle all valid CSS color strings and will From 37efeeb044c55ab5debe60f9ae92cdd280285a06 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 16 Mar 2024 09:08:54 -0700 Subject: [PATCH 67/76] Make process node check stricter Fixes #4995 --- src/common/Platform.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/Platform.ts b/src/common/Platform.ts index 1007fc0a..4102f20c 100644 --- a/src/common/Platform.ts +++ b/src/common/Platform.ts @@ -14,7 +14,7 @@ interface INavigator { declare const navigator: INavigator; declare const process: unknown; -export const isNode = (typeof process !== 'undefined') ? true : false; +export const isNode = (typeof process !== 'undefined' && 'title' in (process as any)) ? true : false; const userAgent = (isNode) ? 'node' : navigator.userAgent; const platform = (isNode) ? 'node' : navigator.platform; From 4071f41f525016ff9f02e11b281f39a47258b909 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 16 Mar 2024 09:16:31 -0700 Subject: [PATCH 68/76] Don't rescale unless it exceeds 25% of following cell Fixes #5005 --- addons/addon-canvas/src/BaseRenderLayer.ts | 11 ++--------- addons/addon-webgl/src/GlyphRenderer.ts | 11 ++--------- src/browser/renderer/shared/RendererUtils.ts | 5 +++-- 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/addons/addon-canvas/src/BaseRenderLayer.ts b/addons/addon-canvas/src/BaseRenderLayer.ts index 0868ba8e..cd3cfa1f 100644 --- a/addons/addon-canvas/src/BaseRenderLayer.ts +++ b/addons/addon-canvas/src/BaseRenderLayer.ts @@ -8,7 +8,7 @@ import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver'; import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache'; import { TEXT_BASELINE } from 'browser/renderer/shared/Constants'; import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs'; -import { isEmoji, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; +import { allowRescaling, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; import { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel'; import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types'; import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; @@ -407,14 +407,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer // following cell (ie. the width is not 2). let renderWidth = glyph.size.x; if (this._optionsService.rawOptions.rescaleOverlappingGlyphs) { - if ( - // Is single cell width - width === 1 && - // Glyph exceeds cell bounds, + 1 to avoid hurting readability - glyph.size.x > this._deviceCellWidth + 1 && - // Never rescale emoji - code && !isEmoji(code) - ) { + if (allowRescaling(code, width, glyph.size.x, this._deviceCellWidth)) { renderWidth = this._deviceCellWidth - 1; // - 1 to improve readability } } diff --git a/addons/addon-webgl/src/GlyphRenderer.ts b/addons/addon-webgl/src/GlyphRenderer.ts index 7ac89084..35b56eef 100644 --- a/addons/addon-webgl/src/GlyphRenderer.ts +++ b/addons/addon-webgl/src/GlyphRenderer.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { isEmoji, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; +import { allowRescaling, throwIfFalsy } from 'browser/renderer/shared/RendererUtils'; import { TextureAtlas } from 'browser/renderer/shared/TextureAtlas'; import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types'; import { NULL_CELL_CODE } from 'common/buffer/Constants'; @@ -281,14 +281,7 @@ export class GlyphRenderer extends Disposable { // Reduce scale horizontally for wide glyphs printed in cells that would overlap with the // following cell (ie. the width is not 2). if (this._optionsService.rawOptions.rescaleOverlappingGlyphs) { - if ( - // Is single cell width - width === 1 && - // Glyph exceeds cell bounds, + 1 to avoid hurting readability - $glyph.size.x > this._dimensions.device.cell.width + 1 && - // Never rescale emoji - code && !isEmoji(code) - ) { + if (allowRescaling(code, width, $glyph.size.x, this._dimensions.device.cell.width)) { array[$i + 2] = (this._dimensions.device.cell.width - 1) / this._dimensions.device.canvas.width; // - 1 to improve readability } } diff --git a/src/browser/renderer/shared/RendererUtils.ts b/src/browser/renderer/shared/RendererUtils.ts index f99c23a1..792d5b1d 100644 --- a/src/browser/renderer/shared/RendererUtils.ts +++ b/src/browser/renderer/shared/RendererUtils.ts @@ -48,8 +48,9 @@ export function allowRescaling(codepoint: number | undefined, width: number, gly return ( // Is single cell width width === 1 && - // Glyph exceeds cell bounds, + 1 to avoid hurting readability - glyphSizeX > deviceCellWidth + 1 && + // Glyph exceeds cell bounds, add 25% to avoid hurting readability by rescaling glyphs that + // barely overlap + glyphSizeX > deviceCellWidth * 1.25 && // Never rescale emoji codepoint !== undefined && !isEmoji(codepoint) && // Never rescale powerline or nerd fonts From 3e5d0015afa09633aabb9a5e2634f4d866c4f8f6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 16 Mar 2024 09:30:25 -0700 Subject: [PATCH 69/76] Fix search not destroying cache on linefeed Fixes #4994 --- addons/addon-search/src/SearchAddon.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/addons/addon-search/src/SearchAddon.ts b/addons/addon-search/src/SearchAddon.ts index 3fae7373..17850f60 100644 --- a/addons/addon-search/src/SearchAddon.ts +++ b/addons/addon-search/src/SearchAddon.ts @@ -78,6 +78,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA */ private _linesCache: LineCacheEntry[] | undefined; private _linesCacheTimeoutId = 0; + private _lineFeedListener: IDisposable | undefined; private _cursorMoveListener: IDisposable | undefined; private _resizeListener: IDisposable | undefined; @@ -427,6 +428,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA const terminal = this._terminal!; if (!this._linesCache) { this._linesCache = new Array(terminal.buffer.active.length); + this._lineFeedListener = terminal.onLineFeed(() => this._destroyLinesCache()); this._cursorMoveListener = terminal.onCursorMove(() => this._destroyLinesCache()); this._resizeListener = terminal.onResize(() => this._destroyLinesCache()); } @@ -445,6 +447,10 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA this._resizeListener.dispose(); this._resizeListener = undefined; } + if (this._lineFeedListener) { + this._lineFeedListener.dispose(); + this._lineFeedListener = undefined; + } if (this._linesCacheTimeoutId) { window.clearTimeout(this._linesCacheTimeoutId); this._linesCacheTimeoutId = 0; From 14fbdd3e9cc00aed02fbd24963a1d601e0db83c8 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 16 Mar 2024 09:32:49 -0700 Subject: [PATCH 70/76] Adopt MutableDisposable in SearchAddon --- addons/addon-search/src/SearchAddon.ts | 27 ++++++++------------------ 1 file changed, 8 insertions(+), 19 deletions(-) diff --git a/addons/addon-search/src/SearchAddon.ts b/addons/addon-search/src/SearchAddon.ts index 17850f60..d42f4d27 100644 --- a/addons/addon-search/src/SearchAddon.ts +++ b/addons/addon-search/src/SearchAddon.ts @@ -6,7 +6,7 @@ import type { Terminal, IDisposable, ITerminalAddon, IDecoration } from '@xterm/xterm'; import type { SearchAddon as ISearchApi } from '@xterm/addon-search'; import { EventEmitter } from 'common/EventEmitter'; -import { Disposable, toDisposable, disposeArray, MutableDisposable } from 'common/Lifecycle'; +import { Disposable, toDisposable, disposeArray, MutableDisposable, getDisposeArrayDisposable } from 'common/Lifecycle'; export interface ISearchOptions { regex?: boolean; @@ -78,9 +78,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA */ private _linesCache: LineCacheEntry[] | undefined; private _linesCacheTimeoutId = 0; - private _lineFeedListener: IDisposable | undefined; - private _cursorMoveListener: IDisposable | undefined; - private _resizeListener: IDisposable | undefined; + private _linesCacheDisposables = new MutableDisposable(); private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number }>()); public readonly onDidChangeResults = this._onDidChangeResults.event; @@ -428,9 +426,11 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA const terminal = this._terminal!; if (!this._linesCache) { this._linesCache = new Array(terminal.buffer.active.length); - this._lineFeedListener = terminal.onLineFeed(() => this._destroyLinesCache()); - this._cursorMoveListener = terminal.onCursorMove(() => this._destroyLinesCache()); - this._resizeListener = terminal.onResize(() => this._destroyLinesCache()); + this._linesCacheDisposables.value = getDisposeArrayDisposable([ + terminal.onLineFeed(() => this._destroyLinesCache()), + terminal.onCursorMove(() => this._destroyLinesCache()), + terminal.onResize(() => this._destroyLinesCache()) + ]); } window.clearTimeout(this._linesCacheTimeoutId); @@ -439,18 +439,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA private _destroyLinesCache(): void { this._linesCache = undefined; - if (this._cursorMoveListener) { - this._cursorMoveListener.dispose(); - this._cursorMoveListener = undefined; - } - if (this._resizeListener) { - this._resizeListener.dispose(); - this._resizeListener = undefined; - } - if (this._lineFeedListener) { - this._lineFeedListener.dispose(); - this._lineFeedListener = undefined; - } + this._linesCacheDisposables.clear(); if (this._linesCacheTimeoutId) { window.clearTimeout(this._linesCacheTimeoutId); this._linesCacheTimeoutId = 0; From c78af2b448b9fa34359750a639f07a8049cf1af9 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 16 Mar 2024 09:49:36 -0700 Subject: [PATCH 71/76] Fix dom cursor blink animation Fixes #4987 Doesn't seem to regress #4773 which caused this issue --- src/browser/renderer/dom/DomRenderer.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 1549b130..3e8f2111 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -209,8 +209,8 @@ export class DomRenderer extends Disposable implements IRenderer { ` animation: blink_block` + `_` + this._terminalClass + ` 1s step-end infinite;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` + - ` background-color: ${colors.cursor.css} !important;` + - ` color: ${colors.cursorAccent.css} !important;` + + ` background-color: ${colors.cursor.css};` + + ` color: ${colors.cursorAccent.css};` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_OUTLINE_CLASS} {` + ` outline: 1px solid ${colors.cursor.css};` + From 18147b2134b1c73a3aae57f012bd46a690309b13 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 16 Mar 2024 09:55:20 -0700 Subject: [PATCH 72/76] Fix bar blink animation --- src/browser/renderer/dom/DomRenderer.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 3e8f2111..68004ff5 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -184,11 +184,17 @@ export class DomRenderer extends Disposable implements IRenderer { `}`; // Blink animation styles += - `@keyframes blink_box_shadow` + `_` + this._terminalClass + ` {` + + `@keyframes blink_underline` + `_` + this._terminalClass + ` {` + ` 50% {` + ` border-bottom-style: hidden;` + ` }` + `}`; + styles += + `@keyframes blink_bar` + `_` + this._terminalClass + ` {` + + ` 50% {` + + ` box-shadow: none;` + + ` }` + + `}`; styles += `@keyframes blink_block` + `_` + this._terminalClass + ` {` + ` 0% {` + @@ -202,8 +208,11 @@ export class DomRenderer extends Disposable implements IRenderer { `}`; // Cursor styles += - `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}:not(.${RowCss.CURSOR_STYLE_BLOCK_CLASS}) {` + - ` animation: blink_box_shadow` + `_` + this._terminalClass + ` 1s step-end infinite;` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_UNDERLINE_CLASS} {` + + ` animation: blink_underline` + `_` + this._terminalClass + ` 1s step-end infinite;` + + `}` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BAR_CLASS} {` + + ` animation: blink_bar` + `_` + this._terminalClass + ` 1s step-end infinite;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` + ` animation: blink_block` + `_` + this._terminalClass + ` 1s step-end infinite;` + From 7b76ca7e49276d4388f750106576302982fcb23e Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 16 Mar 2024 09:57:23 -0700 Subject: [PATCH 73/76] Make dom rendere animation classes more readable --- src/browser/renderer/dom/DomRenderer.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 68004ff5..e6db73e8 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -183,20 +183,23 @@ export class DomRenderer extends Disposable implements IRenderer { ` font-style: italic;` + `}`; // Blink animation + const blinkAnimationUnderlineId = `blink_underline_${this._terminalClass}`; + const blinkAnimationBarId = `blink_bar_${this._terminalClass}`; + const blinkAnimationBlockId = `blink_block_${this._terminalClass}`; styles += - `@keyframes blink_underline` + `_` + this._terminalClass + ` {` + + `@keyframes ${blinkAnimationUnderlineId} {` + ` 50% {` + ` border-bottom-style: hidden;` + ` }` + `}`; styles += - `@keyframes blink_bar` + `_` + this._terminalClass + ` {` + + `@keyframes ${blinkAnimationBarId} {` + ` 50% {` + ` box-shadow: none;` + ` }` + `}`; styles += - `@keyframes blink_block` + `_` + this._terminalClass + ` {` + + `@keyframes ${blinkAnimationBlockId} {` + ` 0% {` + ` background-color: ${colors.cursor.css};` + ` color: ${colors.cursorAccent.css};` + @@ -209,13 +212,13 @@ export class DomRenderer extends Disposable implements IRenderer { // Cursor styles += `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_UNDERLINE_CLASS} {` + - ` animation: blink_underline` + `_` + this._terminalClass + ` 1s step-end infinite;` + + ` animation: ${blinkAnimationUnderlineId} 1s step-end infinite;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BAR_CLASS} {` + - ` animation: blink_bar` + `_` + this._terminalClass + ` 1s step-end infinite;` + + ` animation: ${blinkAnimationBarId} 1s step-end infinite;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` + - ` animation: blink_block` + `_` + this._terminalClass + ` 1s step-end infinite;` + + ` animation: ${blinkAnimationBlockId} 1s step-end infinite;` + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` + ` background-color: ${colors.cursor.css};` + From a4f1299f42beaa1c7298dee68600ba15de51bdc5 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 16 Mar 2024 11:05:01 -0700 Subject: [PATCH 74/76] Special case cursor block blink to not use important Fixes #4987 --- src/browser/renderer/dom/DomRenderer.ts | 7 +++++++ test/playwright/SharedRendererTests.ts | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index e6db73e8..c89cee62 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -220,10 +220,17 @@ export class DomRenderer extends Disposable implements IRenderer { `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` + ` animation: ${blinkAnimationBlockId} 1s step-end infinite;` + `}` + + // !important helps fix an issue where the cursor will not render on top of the selection, + // however it's very hard to fix this issue and retain the blink animation without the use of + // !important. So this edge case fails when cursor blink is on. `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` + ` background-color: ${colors.cursor.css};` + ` color: ${colors.cursorAccent.css};` + `}` + + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS}:not(.${RowCss.CURSOR_BLINK_CLASS}) {` + + ` background-color: ${colors.cursor.css} !important;` + + ` color: ${colors.cursorAccent.css} !important;` + + `}` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_STYLE_OUTLINE_CLASS} {` + ` outline: 1px solid ${colors.cursor.css};` + ` outline-offset: -1px;` + diff --git a/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts index 29dc51c3..c4b66536 100644 --- a/test/playwright/SharedRendererTests.ts +++ b/test/playwright/SharedRendererTests.ts @@ -6,7 +6,7 @@ import { IImage32, decodePng } from '@lunapaint/png-codec'; import { LocatorScreenshotOptions, test } from '@playwright/test'; import { ITheme } from '@xterm/xterm'; -import { ITestContext, MaybeAsync, openTerminal, pollFor, pollForApproximate, timeout } from './TestUtils'; +import { ITestContext, MaybeAsync, openTerminal, pollFor, pollForApproximate } from './TestUtils'; export interface ISharedRendererTestContext { value: ITestContext; From f8ae7edaa0d6f188101df9b3a9d26a835e2c223f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Tue, 19 Mar 2024 08:56:13 -0700 Subject: [PATCH 75/76] Only scale when over 50% of following cell See microsoft/vscode#208102 --- src/browser/renderer/shared/RendererUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/renderer/shared/RendererUtils.ts b/src/browser/renderer/shared/RendererUtils.ts index 792d5b1d..13c3bdd0 100644 --- a/src/browser/renderer/shared/RendererUtils.ts +++ b/src/browser/renderer/shared/RendererUtils.ts @@ -48,9 +48,9 @@ export function allowRescaling(codepoint: number | undefined, width: number, gly return ( // Is single cell width width === 1 && - // Glyph exceeds cell bounds, add 25% to avoid hurting readability by rescaling glyphs that + // Glyph exceeds cell bounds, add 50% to avoid hurting readability by rescaling glyphs that // barely overlap - glyphSizeX > deviceCellWidth * 1.25 && + glyphSizeX > Math.ceil(deviceCellWidth * 1.5) && // Never rescale emoji codepoint !== undefined && !isEmoji(codepoint) && // Never rescale powerline or nerd fonts From 4c90009a0e7678dcb751d4632f5354dded9c3ed1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Wed, 20 Mar 2024 10:02:46 -0700 Subject: [PATCH 76/76] Don't rescale ascii See microsoft/vscode#208102 --- src/browser/renderer/shared/RendererUtils.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/browser/renderer/shared/RendererUtils.ts b/src/browser/renderer/shared/RendererUtils.ts index 13c3bdd0..01064364 100644 --- a/src/browser/renderer/shared/RendererUtils.ts +++ b/src/browser/renderer/shared/RendererUtils.ts @@ -51,8 +51,10 @@ export function allowRescaling(codepoint: number | undefined, width: number, gly // Glyph exceeds cell bounds, add 50% to avoid hurting readability by rescaling glyphs that // barely overlap glyphSizeX > Math.ceil(deviceCellWidth * 1.5) && + // Never rescale ascii + codepoint !== undefined && codepoint > 0xFF && // Never rescale emoji - codepoint !== undefined && !isEmoji(codepoint) && + !isEmoji(codepoint) && // Never rescale powerline or nerd fonts !isPowerlineGlyph(codepoint) && !isNerdFontGlyph(codepoint) );