From 0aeeebdd646b7ddd9b990565669778cf46b42dfa Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sat, 4 Nov 2023 13:05:42 +0000 Subject: [PATCH 01/58] Devcontainer update --- .devcontainer/devcontainer.json | 8 +++++--- .nvmrc | 2 +- .vscode/launch.json | 2 +- package.json | 2 +- 4 files changed, 8 insertions(+), 6 deletions(-) diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index c09a29bf..9c83cc4c 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,13 +1,15 @@ { "name": "xterm.js", - "image": "mcr.microsoft.com/devcontainers/typescript-node:0-18-buster", + "image": "mcr.microsoft.com/devcontainers/typescript-node:18-bookworm", "features": { - "ghcr.io/devcontainers/features/node:1": {} // yarn + "ghcr.io/devcontainers/features/node:1": { + "version": 18 + } // yarn }, "forwardPorts": [ 3000 ], - "postCreateCommand": "yarn install", + "postCreateCommand": "yarn install && yarn setup", "customizations": { "vscode": { "extensions": [ diff --git a/.nvmrc b/.nvmrc index b6a7d89c..3c032078 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -16 +18 diff --git a/.vscode/launch.json b/.vscode/launch.json index 5dbd01ce..eaa5e12e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -61,7 +61,7 @@ "runtimeExecutable": "npm", "runtimeArgs": ["start"], "stopOnEntry": true, - "runtimeVersion": "16", + "runtimeVersion": "18", "serverReadyAction": { "action": "openExternally", "pattern": "App listening to (http://.*?:[0-9]+)" diff --git a/package.json b/package.json index d0510e5d..28e9952b 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,7 @@ "cross-env": "^7.0.3", "deep-equal": "^2.0.5", "eslint": "^8.45.0", - "eslint-plugin-jsdoc": "^39.3.6", + "eslint-plugin-jsdoc": "^46.8.2", "express": "^4.17.1", "express-ws": "^5.0.2", "glob": "^7.2.0", From 5c1d9b28cd411ee1c1787ddc971b7bdc9c418b80 Mon Sep 17 00:00:00 2001 From: Simon Lamon <32477463+silamon@users.noreply.github.com> Date: Sat, 4 Nov 2023 13:10:36 +0000 Subject: [PATCH 02/58] Update yarn lock --- yarn.lock | 69 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 25 deletions(-) diff --git a/yarn.lock b/yarn.lock index a0cb39da..5f25390b 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.40.1": + version "0.40.1" + resolved "https://registry.yarnpkg.com/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz#13acd77fb372ed1c83b7355edd865a3b370c9ec4" + integrity sha512-YORCdZSusAlBrFpZ77pJjc5r1bQs5caPWtAu+WWmiSo+8XaUzseapVrfAtiRFbQWnrBxxLLEwF6f6ZG/UgCQCg== dependencies: - comment-parser "1.3.1" - esquery "^1.4.0" - jsdoc-type-pratt-parser "~3.1.0" + comment-parser "1.4.0" + 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" @@ -1029,6 +1029,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 +1154,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 +1365,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.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/comment-parser/-/comment-parser-1.4.0.tgz#0f8c560f59698193854f12884c20c0e39a26d32c" + integrity sha512-QLyTNiZ2KDOibvFPlZ6ZngVsZ/0gYnE6uTXi5aoDg8ed3AkJAz4sEje3Y8a29hQ1s6A99MZXe47fLAXQ1rTqaw== commondir@^1.0.1: version "1.0.1" @@ -1666,17 +1676,19 @@ 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.8.2: + version "46.8.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.8.2.tgz#3e6b1c93e91e38fe01874d45da121b56393c54a5" + integrity sha512-5TSnD018f3tUJNne4s4gDWQflbsgOycIKEUBoCLn6XtBMgNHxQFmV8vVxUtiPxAQq8lrX85OaSG/2gnctxw9uQ== dependencies: - "@es-joy/jsdoccomment" "~0.36.1" - comment-parser "1.3.1" + "@es-joy/jsdoccomment" "~0.40.1" + are-docs-informative "^0.0.2" + comment-parser "1.4.0" debug "^4.3.4" escape-string-regexp "^4.0.0" - esquery "^1.4.0" - semver "^7.3.8" + esquery "^1.5.0" + is-builtin-module "^3.2.1" + semver "^7.5.4" spdx-expression-parse "^3.0.1" eslint-scope@5.1.1: @@ -1757,7 +1769,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 +2353,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 +2618,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 +3367,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== From 856666d5bdc003c467384c5e4f25efab5ae2469a Mon Sep 17 00:00:00 2001 From: Szymon Kaliski Date: Fri, 5 Jan 2024 09:43:33 +0100 Subject: [PATCH 03/58] 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 04/58] 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 05/58] 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 06/58] 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 07/58] 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 08/58] 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 09/58] 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 10/58] 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 11/58] 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 12/58] 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 13/58] 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 14/58] 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 15/58] 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 16/58] 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 17/58] 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 18/58] 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 19/58] 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 20/58] 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 21/58] 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 22/58] 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 23/58] 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 24/58] 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 25/58] 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 fe22671a6edfefb38727e1c2d9346baea1e7ad9a Mon Sep 17 00:00:00 2001 From: tisilent Date: Wed, 6 Mar 2024 17:03:25 +0800 Subject: [PATCH 26/58] Clear timer when dispose --- src/browser/Viewport.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index a8e1a498..cea2ca8f 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -49,6 +49,8 @@ export class Viewport extends Disposable implements IViewport { target: -1 }; + private _ensureTimeout: number; + private readonly _onRequestScrollLines = this.register(new EventEmitter<{ amount: number, suppressScrollEvent: boolean }>()); public readonly onRequestScrollLines = this._onRequestScrollLines.event; @@ -81,7 +83,7 @@ export class Viewport extends Disposable implements IViewport { this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.syncScrollArea())); // Perform this async to ensure the ICharSizeService is ready. - setTimeout(() => this.syncScrollArea()); + this._ensureTimeout = window.setTimeout(() => this.syncScrollArea()); } private _handleThemeChange(colors: ReadonlyColorSet): void { @@ -398,4 +400,8 @@ export class Viewport extends Disposable implements IViewport { this._viewportElement.scrollTop += deltaY; return this._bubbleScroll(ev, deltaY); } + + public dispose(): void { + clearTimeout(this._ensureTimeout); + } } From 553b9f261a2f231d4e16f27d6e745592432c0b94 Mon Sep 17 00:00:00 2001 From: Knox Lively Date: Wed, 6 Mar 2024 23:56:04 -0700 Subject: [PATCH 27/58] adding Wave Terminal to the README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b055ea7b..5f4a786a 100644 --- a/README.md +++ b/README.md @@ -222,6 +222,7 @@ Xterm.js is used in several world-class applications to provide great terminal e - [**Cloudtutor.io**](https://cloudtutor.io): innovative online learning platform that offers users access to an interactive lab. - [**Helix Editor Playground**](https://github.com/tomgroenwoldt/helix-editor-playground): Online playground for the terminal based helix editor. - [**Coder**](https://github.com/coder/coder): Self-Hosted Remote Development Environments +- [**Wave Terminal**](https://waveterm.dev): An open-source, ai-native, terminal built for seamless workflows. - [And much more...](https://github.com/xtermjs/xterm.js/network/dependents?package_id=UGFja2FnZS0xNjYzMjc4OQ%3D%3D) Do you use xterm.js in your application as well? Please [open a Pull Request](https://github.com/sourcelair/xterm.js/pulls) to include it here. We would love to have it on our list. Note: Please add any new contributions to the end of the list only. 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 28/58] 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 29/58] 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 30/58] 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 31/58] 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 32/58] 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 33/58] 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 34/58] 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 35/58] 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 36/58] 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 37/58] 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 38/58] 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 39/58] 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 40/58] 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 41/58] 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 42/58] 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 43/58] 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) ); From 78297e52aaf12f67f1d1abf885869adc176fceb3 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 5 Apr 2024 06:50:30 -0700 Subject: [PATCH 44/58] v5.5 --- 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 c2f42ce5..71b1188d 100644 --- a/addons/addon-attach/package.json +++ b/addons/addon-attach/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-attach", - "version": "0.10.0", + "version": "0.11.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 8fdec96b..2ca5d163 100644 --- a/addons/addon-canvas/package.json +++ b/addons/addon-canvas/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-canvas", - "version": "0.6.0", + "version": "0.7.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 be81b196..cacf31dd 100644 --- a/addons/addon-fit/package.json +++ b/addons/addon-fit/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-fit", - "version": "0.9.0", + "version": "0.10.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 2e28955d..ac4dc018 100644 --- a/addons/addon-image/package.json +++ b/addons/addon-image/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-image", - "version": "0.7.0", + "version": "0.8.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 60166577..80608ee3 100644 --- a/addons/addon-ligatures/package.json +++ b/addons/addon-ligatures/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-ligatures", - "version": "0.8.0", + "version": "0.9.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 35a4015e..9292a0c4 100644 --- a/addons/addon-search/package.json +++ b/addons/addon-search/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-search", - "version": "0.14.0", + "version": "0.15.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 9c50288d..30dadbb6 100644 --- a/addons/addon-serialize/package.json +++ b/addons/addon-serialize/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-serialize", - "version": "0.12.0", + "version": "0.13.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 610e14b4..3d846286 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.2.0", + "version": "0.3.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 e511c65a..9f2c21b8 100644 --- a/addons/addon-unicode11/package.json +++ b/addons/addon-unicode11/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-unicode11", - "version": "0.7.0", + "version": "0.8.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 6a65f289..da888716 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.10.0", + "version": "0.11.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 f504bb7e..da37eb26 100644 --- a/addons/addon-webgl/package.json +++ b/addons/addon-webgl/package.json @@ -1,6 +1,6 @@ { "name": "@xterm/addon-webgl", - "version": "0.17.0", + "version": "0.18.0", "author": { "name": "The xterm.js authors", "url": "https://xtermjs.org/" diff --git a/package.json b/package.json index dd425db6..c79b1196 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@xterm/xterm", "description": "Full xterm terminal, in your browser", - "version": "5.4.0", + "version": "5.5.0", "main": "lib/xterm.js", "style": "css/xterm.css", "types": "typings/xterm.d.ts", From 52e8a75e9f3b0f12cdba71d8c3cfe3a5f4958885 Mon Sep 17 00:00:00 2001 From: ksqsf Date: Mon, 8 Apr 2024 13:37:55 +0800 Subject: [PATCH 45/58] Fix duplicate input for some IMEs fixes #5023 --- src/browser/input/CompositionHelper.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/browser/input/CompositionHelper.ts b/src/browser/input/CompositionHelper.ts index 7542969a..9891709f 100644 --- a/src/browser/input/CompositionHelper.ts +++ b/src/browser/input/CompositionHelper.ts @@ -159,8 +159,9 @@ export class CompositionHelper { // otherwise input characters can be duplicated. (Issue #3191) currentCompositionPosition.start += this._dataAlreadySent.length; if (this._isComposing) { - // Use the end position to get the string if a new composition has started. - input = this._textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end); + // Use the start position of the new composition to get the string + // if a new composition has started. + input = this._textarea.value.substring(currentCompositionPosition.start, this._compositionPosition.start); } else { // Don't use the end position here in order to pick up any characters after the // composition has finished, for example when typing a non-composition character From e845abc784dba466f504d9da629ce15e4ec1875d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Fri, 12 Apr 2024 16:47:59 -0700 Subject: [PATCH 46/58] Finish OSC hyperlinks when the second param is only whitespace Fixes #4916 --- src/common/InputHandler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index a4b8c64b..9b300993 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -2979,7 +2979,7 @@ export class InputHandler extends Disposable implements IInputHandler { if (args[1]) { return this._createHyperlink(args[0], args[1]); } - if (args[0]) { + if (args[0].trim()) { return false; } return this._finishHyperlink(); From 8458bb4c4f4b8b3cf1d4a7207e0d47b13d17260c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 18 Apr 2024 16:27:48 -0700 Subject: [PATCH 47/58] Expose onWriteParsed on API Inconsistency with headless API --- src/headless/public/Terminal.ts | 1 + typings/xterm-headless.d.ts | 11 +++++++++++ 2 files changed, 12 insertions(+) diff --git a/src/headless/public/Terminal.ts b/src/headless/public/Terminal.ts index 0d73f9d3..1b39c184 100644 --- a/src/headless/public/Terminal.ts +++ b/src/headless/public/Terminal.ts @@ -80,6 +80,7 @@ export class Terminal extends Disposable implements ITerminalApi { public get onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; } public get onScroll(): IEvent { return this._core.onScroll; } public get onTitleChange(): IEvent { return this._core.onTitleChange; } + public get onWriteParsed(): IEvent { return this._core.onWriteParsed; } public get parser(): IParser { this._checkProposedApi(); diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 5e84f266..2d3329ed 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -714,6 +714,17 @@ declare module '@xterm/headless' { */ onLineFeed: IEvent; + /** + * Adds an event listener for when data has been parsed by the terminal, + * after {@link write} is called. This event is useful to listen for any + * changes in the buffer. + * + * This fires at most once per frame, after data parsing completes. Note + * that this can fire when there are still writes pending if there is a lot + * of data. + */ + onWriteParsed: IEvent; + /** * Adds an event listener for when the terminal is resized. The event value * contains the new size. From 16db22711946e7843b360ba583eb05e4a73694f6 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Apr 2024 08:11:59 -0700 Subject: [PATCH 48/58] Prevent smooth scroll from running more than once per frame Fixes #5036 --- src/browser/Viewport.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts index a8e1a498..1b2f7b5b 100644 --- a/src/browser/Viewport.ts +++ b/src/browser/Viewport.ts @@ -36,6 +36,8 @@ export class Viewport extends Disposable implements IViewport { private _activeBuffer: IBuffer; private _renderDimensions: IRenderDimensions; + private _smoothScrollAnimationFrame: number = 0; + // Stores a partial line amount when scrolling, this is used to keep track of how much of a line // is scrolled so we can "scroll" over partial lines and feel natural on touchpads. This is a // quick fix and could have a more robust solution in place that reset the value when needed. @@ -211,7 +213,12 @@ export class Viewport extends Disposable implements IViewport { // Continue or finish smooth scroll if (percent < 1) { - this._coreBrowserService.window.requestAnimationFrame(() => this._smoothScroll()); + if (!this._smoothScrollAnimationFrame) { + this._smoothScrollAnimationFrame = this._coreBrowserService.window.requestAnimationFrame(() => { + this._smoothScrollAnimationFrame = 0; + this._smoothScroll(); + }); + } } else { this._clearSmoothScrollState(); } From 5c8a9084e3e52c528ac14ce5035e4c7d9d060dd1 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Apr 2024 08:41:22 -0700 Subject: [PATCH 49/58] Add mass decoration test button Part of #4911 --- demo/client.ts | 30 +++++++++++++++++++++++++++++- demo/index.html | 1 + 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/demo/client.ts b/demo/client.ts index 7e0830a5..a3e58051 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -44,7 +44,7 @@ if ('WebAssembly' in window) { // Pulling in the module's types relies on the above, it's looks a // little weird here as we're importing "this" module -import { Terminal as TerminalType, ITerminalOptions } from '@xterm/xterm'; +import { Terminal as TerminalType, ITerminalOptions, type IDisposable } from '@xterm/xterm'; export interface IWindowWithTerminal extends Window { term: TerminalType; @@ -255,6 +255,7 @@ if (document.location.pathname === '/test') { document.getElementById('add-grapheme-clusters').addEventListener('click', addGraphemeClusters); document.getElementById('add-decoration').addEventListener('click', addDecoration); document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler); + document.getElementById('decoration-stress-test').addEventListener('click', decorationStressTest); document.getElementById('weblinks-test').addEventListener('click', testWeblinks); document.getElementById('bce').addEventListener('click', coloredErase); addVtButtons(); @@ -1170,6 +1171,33 @@ function addOverviewRuler(): void { term.registerDecoration({ marker: term.registerMarker(10), overviewRulerOptions: { color: '#ffffff80', position: 'full' } }); } +let decorationStressTestDecorations: IDisposable[] | undefined; +function decorationStressTest(): void { + if (decorationStressTestDecorations) { + for (const d of decorationStressTestDecorations) { + d.dispose(); + } + decorationStressTestDecorations = undefined; + } else { + const t = term as Terminal; + const buffer = t.buffer.active; + const cursorY = buffer.baseY + buffer.cursorY; + decorationStressTestDecorations = []; + for (const x of [0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]) { + for (let y = 0; y < t.buffer.active.length; y++) { + const cursorOffsetY = y - cursorY; + decorationStressTestDecorations.push(t.registerDecoration({ + marker: t.registerMarker(cursorOffsetY), + x, + width: 4, + backgroundColor: '#FF0000', + overviewRulerOptions: { color: '#FF0000' } + })); + } + } + } +} + (console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => { function getBox(width: number, height: number): any { return { diff --git a/demo/index.html b/demo/index.html index caff2ca2..238c9886 100644 --- a/demo/index.html +++ b/demo/index.html @@ -102,6 +102,7 @@
Decorations
+
Weblinks Addon
From dc541d549543f4bff48993074a7222d923d1aa72 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Apr 2024 10:32:30 -0700 Subject: [PATCH 50/58] Optimize SortedList.delete by batching to idle task --- src/common/SortedList.ts | 43 ++++++++++++++++++++++-- src/common/services/DecorationService.ts | 3 +- 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts index c3250091..0fb71e4e 100644 --- a/src/common/SortedList.ts +++ b/src/common/SortedList.ts @@ -3,6 +3,8 @@ * @license MIT */ +import { IdleTaskQueue } from 'common/TaskQueue'; + // Work variables to avoid garbage collection. let i = 0; @@ -12,7 +14,10 @@ let i = 0; * includes the by key iterator. */ export class SortedList { - private readonly _array: T[] = []; + private _array: T[] = []; + private readonly _deletedIndices: Set = new Set(); + private readonly _cleanupDeletedTask = new IdleTaskQueue(); + private _isCleaningUp = false; constructor( private readonly _getKey: (value: T) => number @@ -21,9 +26,13 @@ export class SortedList { public clear(): void { this._array.length = 0; + this._deletedIndices.clear(); + this._cleanupDeletedTask.clear(); + this._isCleaningUp = false; } public insert(value: T): void { + this._flushCleanupDeleted(); if (this._array.length === 0) { this._array.push(value); return; @@ -49,14 +58,42 @@ export class SortedList { } do { if (this._array[i] === value) { - this._array.splice(i, 1); + if (this._deletedIndices.size === 0) { + this._cleanupDeletedTask.enqueue(() => this._cleanupDeleted()); + } + this._deletedIndices.add(i); return true; } } while (++i < this._array.length && this._getKey(this._array[i]) === key); return false; } + private _cleanupDeleted(): void { + this._isCleaningUp = true; + const sortedDeletedIndices = Array.from(this._deletedIndices).sort((a, b) => a - b); + let sortedDeletedIndicesIndex = 0; + const newArray = new Array(this._array.length - sortedDeletedIndices.length); + let newArrayIndex = 0; + for (let i = 0; i < this._array.length; i++) { + if (sortedDeletedIndices[sortedDeletedIndicesIndex] === i) { + sortedDeletedIndicesIndex++; + } else { + newArray[newArrayIndex++] = this._array[i]; + } + } + this._array = newArray; + this._deletedIndices.clear(); + this._isCleaningUp = false; + } + + private _flushCleanupDeleted(): void { + if (!this._isCleaningUp) { + this._cleanupDeletedTask.flush(); + } + } + public *getKeyIterator(key: number): IterableIterator { + this._flushCleanupDeleted(); if (this._array.length === 0) { return; } @@ -73,6 +110,7 @@ export class SortedList { } public forEachByKey(key: number, callback: (value: T) => void): void { + this._flushCleanupDeleted(); if (this._array.length === 0) { return; } @@ -89,6 +127,7 @@ export class SortedList { } public values(): IterableIterator { + this._flushCleanupDeleted(); // Duplicate the array to avoid issues when _array changes while iterating return [...this._array].values(); } diff --git a/src/common/services/DecorationService.ts b/src/common/services/DecorationService.ts index da759152..c9be78af 100644 --- a/src/common/services/DecorationService.ts +++ b/src/common/services/DecorationService.ts @@ -45,7 +45,8 @@ export class DecorationService extends Disposable implements IDecorationService const decoration = new Decoration(options); if (decoration) { const markerDispose = decoration.marker.onDispose(() => decoration.dispose()); - decoration.onDispose(() => { + const listener = decoration.onDispose(() => { + listener.dispose(); if (decoration) { if (this._decorations.delete(decoration)) { this._onDecorationRemoved.fire(decoration); From 3af11c2b1f8c79cf61b040231e851f6d523c4e19 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Apr 2024 10:47:40 -0700 Subject: [PATCH 51/58] Optimize SortedList.insert by batching to idle task --- src/common/SortedList.ts | 58 +++++++++++++++++++++++++++++++++------- 1 file changed, 49 insertions(+), 9 deletions(-) diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts index 0fb71e4e..0aba3668 100644 --- a/src/common/SortedList.ts +++ b/src/common/SortedList.ts @@ -15,8 +15,14 @@ let i = 0; */ export class SortedList { private _array: T[] = []; + + private readonly _addedValues: Set = new Set(); + private readonly _cleanupAddedTask = new IdleTaskQueue(); + private _isCleaningUpAdded = false; + private readonly _deletedIndices: Set = new Set(); - private readonly _cleanupDeletedTask = new IdleTaskQueue(); + + private readonly _cleanupTask = new IdleTaskQueue(); private _isCleaningUp = false; constructor( @@ -27,21 +33,52 @@ export class SortedList { public clear(): void { this._array.length = 0; this._deletedIndices.clear(); - this._cleanupDeletedTask.clear(); + this._cleanupTask.clear(); this._isCleaningUp = false; } public insert(value: T): void { this._flushCleanupDeleted(); - if (this._array.length === 0) { - this._array.push(value); - return; + if (this._addedValues.size === 0) { + this._cleanupAddedTask.enqueue(() => this._cleanupAdded()); + } + this._addedValues.add(value); + // if (this._array.length === 0) { + // this._array.push(value); + // return; + // } + // i = this._search(this._getKey(value)); + // this._array.splice(i, 0, value); + } + + private _cleanupAdded(): void { + const sortedAddedValues = Array.from(this._addedValues).sort((a, b) => this._getKey(a) - this._getKey(b)); + let sortedAddedValuesIndex = 0; + let arrayIndex = 0; + + const newArray = new Array(this._array.length + this._addedValues.size); + + for (let newArrayIndex = 0; newArrayIndex < newArray.length; newArrayIndex++) { + if (arrayIndex >= this._array.length || this._getKey(sortedAddedValues[sortedAddedValuesIndex]) === this._getKey(this._array[arrayIndex])) { + newArray[newArrayIndex] = sortedAddedValues[sortedAddedValuesIndex]; + sortedAddedValuesIndex++; + } else { + newArray[newArrayIndex] = this._array[arrayIndex++]; + } + } + + this._array = newArray; + this._addedValues.clear(); + } + + private _flushCleanupAdded(): void { + if (!this._isCleaningUpAdded) { + this._cleanupAddedTask.flush(); } - i = this._search(this._getKey(value)); - this._array.splice(i, 0, value); } public delete(value: T): boolean { + this._flushCleanupAdded(); if (this._array.length === 0) { return false; } @@ -59,7 +96,7 @@ export class SortedList { do { if (this._array[i] === value) { if (this._deletedIndices.size === 0) { - this._cleanupDeletedTask.enqueue(() => this._cleanupDeleted()); + this._cleanupTask.enqueue(() => this._cleanupDeleted()); } this._deletedIndices.add(i); return true; @@ -88,11 +125,12 @@ export class SortedList { private _flushCleanupDeleted(): void { if (!this._isCleaningUp) { - this._cleanupDeletedTask.flush(); + this._cleanupTask.flush(); } } public *getKeyIterator(key: number): IterableIterator { + this._flushCleanupAdded(); this._flushCleanupDeleted(); if (this._array.length === 0) { return; @@ -110,6 +148,7 @@ export class SortedList { } public forEachByKey(key: number, callback: (value: T) => void): void { + this._flushCleanupAdded(); this._flushCleanupDeleted(); if (this._array.length === 0) { return; @@ -127,6 +166,7 @@ export class SortedList { } public values(): IterableIterator { + this._flushCleanupAdded(); this._flushCleanupDeleted(); // Duplicate the array to avoid issues when _array changes while iterating return [...this._array].values(); From 69c4966804d6fc33ebe27bf3f38901a6e5875aff Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Apr 2024 10:48:55 -0700 Subject: [PATCH 52/58] Consistent naming --- src/common/SortedList.ts | 61 ++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts index 0aba3668..42dc7ef3 100644 --- a/src/common/SortedList.ts +++ b/src/common/SortedList.ts @@ -16,14 +16,13 @@ let i = 0; export class SortedList { private _array: T[] = []; - private readonly _addedValues: Set = new Set(); - private readonly _cleanupAddedTask = new IdleTaskQueue(); - private _isCleaningUpAdded = false; + private readonly _insertedValues: Set = new Set(); + private readonly _flushInsertedTask = new IdleTaskQueue(); + private _isFlushingInserted = false; private readonly _deletedIndices: Set = new Set(); - - private readonly _cleanupTask = new IdleTaskQueue(); - private _isCleaningUp = false; + private readonly _flushDeletedTask = new IdleTaskQueue(); + private _isflushingDeleted = false; constructor( private readonly _getKey: (value: T) => number @@ -33,30 +32,24 @@ export class SortedList { public clear(): void { this._array.length = 0; this._deletedIndices.clear(); - this._cleanupTask.clear(); - this._isCleaningUp = false; + this._flushDeletedTask.clear(); + this._isflushingDeleted = false; } public insert(value: T): void { this._flushCleanupDeleted(); - if (this._addedValues.size === 0) { - this._cleanupAddedTask.enqueue(() => this._cleanupAdded()); + if (this._insertedValues.size === 0) { + this._flushInsertedTask.enqueue(() => this._flushInserted()); } - this._addedValues.add(value); - // if (this._array.length === 0) { - // this._array.push(value); - // return; - // } - // i = this._search(this._getKey(value)); - // this._array.splice(i, 0, value); + this._insertedValues.add(value); } - private _cleanupAdded(): void { - const sortedAddedValues = Array.from(this._addedValues).sort((a, b) => this._getKey(a) - this._getKey(b)); + private _flushInserted(): void { + const sortedAddedValues = Array.from(this._insertedValues).sort((a, b) => this._getKey(a) - this._getKey(b)); let sortedAddedValuesIndex = 0; let arrayIndex = 0; - const newArray = new Array(this._array.length + this._addedValues.size); + const newArray = new Array(this._array.length + this._insertedValues.size); for (let newArrayIndex = 0; newArrayIndex < newArray.length; newArrayIndex++) { if (arrayIndex >= this._array.length || this._getKey(sortedAddedValues[sortedAddedValuesIndex]) === this._getKey(this._array[arrayIndex])) { @@ -68,17 +61,17 @@ export class SortedList { } this._array = newArray; - this._addedValues.clear(); + this._insertedValues.clear(); } - private _flushCleanupAdded(): void { - if (!this._isCleaningUpAdded) { - this._cleanupAddedTask.flush(); + private _flushCleanupInserted(): void { + if (!this._isFlushingInserted) { + this._flushInsertedTask.flush(); } } public delete(value: T): boolean { - this._flushCleanupAdded(); + this._flushCleanupInserted(); if (this._array.length === 0) { return false; } @@ -96,7 +89,7 @@ export class SortedList { do { if (this._array[i] === value) { if (this._deletedIndices.size === 0) { - this._cleanupTask.enqueue(() => this._cleanupDeleted()); + this._flushDeletedTask.enqueue(() => this._flushDeleted()); } this._deletedIndices.add(i); return true; @@ -105,8 +98,8 @@ export class SortedList { return false; } - private _cleanupDeleted(): void { - this._isCleaningUp = true; + private _flushDeleted(): void { + this._isflushingDeleted = true; const sortedDeletedIndices = Array.from(this._deletedIndices).sort((a, b) => a - b); let sortedDeletedIndicesIndex = 0; const newArray = new Array(this._array.length - sortedDeletedIndices.length); @@ -120,17 +113,17 @@ export class SortedList { } this._array = newArray; this._deletedIndices.clear(); - this._isCleaningUp = false; + this._isflushingDeleted = false; } private _flushCleanupDeleted(): void { - if (!this._isCleaningUp) { - this._cleanupTask.flush(); + if (!this._isflushingDeleted) { + this._flushDeletedTask.flush(); } } public *getKeyIterator(key: number): IterableIterator { - this._flushCleanupAdded(); + this._flushCleanupInserted(); this._flushCleanupDeleted(); if (this._array.length === 0) { return; @@ -148,7 +141,7 @@ export class SortedList { } public forEachByKey(key: number, callback: (value: T) => void): void { - this._flushCleanupAdded(); + this._flushCleanupInserted(); this._flushCleanupDeleted(); if (this._array.length === 0) { return; @@ -166,7 +159,7 @@ export class SortedList { } public values(): IterableIterator { - this._flushCleanupAdded(); + this._flushCleanupInserted(); this._flushCleanupDeleted(); // Duplicate the array to avoid issues when _array changes while iterating return [...this._array].values(); From 45fd3c900f2cfe448a129ba5b7525bacb24c9b9f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Apr 2024 10:52:43 -0700 Subject: [PATCH 53/58] Only flush when needed --- src/common/SortedList.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts index 42dc7ef3..8a2ad9cc 100644 --- a/src/common/SortedList.ts +++ b/src/common/SortedList.ts @@ -22,7 +22,7 @@ export class SortedList { private readonly _deletedIndices: Set = new Set(); private readonly _flushDeletedTask = new IdleTaskQueue(); - private _isflushingDeleted = false; + private _isFlushingDeleted = false; constructor( private readonly _getKey: (value: T) => number @@ -33,7 +33,7 @@ export class SortedList { this._array.length = 0; this._deletedIndices.clear(); this._flushDeletedTask.clear(); - this._isflushingDeleted = false; + this._isFlushingDeleted = false; } public insert(value: T): void { @@ -65,7 +65,7 @@ export class SortedList { } private _flushCleanupInserted(): void { - if (!this._isFlushingInserted) { + if (!this._isFlushingInserted && this._insertedValues.size > 0) { this._flushInsertedTask.flush(); } } @@ -99,7 +99,7 @@ export class SortedList { } private _flushDeleted(): void { - this._isflushingDeleted = true; + this._isFlushingDeleted = true; const sortedDeletedIndices = Array.from(this._deletedIndices).sort((a, b) => a - b); let sortedDeletedIndicesIndex = 0; const newArray = new Array(this._array.length - sortedDeletedIndices.length); @@ -113,11 +113,11 @@ export class SortedList { } this._array = newArray; this._deletedIndices.clear(); - this._isflushingDeleted = false; + this._isFlushingDeleted = false; } private _flushCleanupDeleted(): void { - if (!this._isflushingDeleted) { + if (!this._isFlushingDeleted && this._deletedIndices.size > 0) { this._flushDeletedTask.flush(); } } From 71dddbc8b93f141413c32c20443639681b5ad914 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Apr 2024 10:57:39 -0700 Subject: [PATCH 54/58] Move from set to array --- src/common/SortedList.ts | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts index 8a2ad9cc..eb88f368 100644 --- a/src/common/SortedList.ts +++ b/src/common/SortedList.ts @@ -16,11 +16,11 @@ let i = 0; export class SortedList { private _array: T[] = []; - private readonly _insertedValues: Set = new Set(); + private readonly _insertedValues: T[] = []; private readonly _flushInsertedTask = new IdleTaskQueue(); private _isFlushingInserted = false; - private readonly _deletedIndices: Set = new Set(); + private readonly _deletedIndices: number[] = []; private readonly _flushDeletedTask = new IdleTaskQueue(); private _isFlushingDeleted = false; @@ -31,17 +31,20 @@ export class SortedList { public clear(): void { this._array.length = 0; - this._deletedIndices.clear(); + this._insertedValues.length = 0; + this._flushInsertedTask.clear(); + this._isFlushingInserted = false; + this._deletedIndices.length = 0; this._flushDeletedTask.clear(); this._isFlushingDeleted = false; } public insert(value: T): void { this._flushCleanupDeleted(); - if (this._insertedValues.size === 0) { + if (this._insertedValues.length === 0) { this._flushInsertedTask.enqueue(() => this._flushInserted()); } - this._insertedValues.add(value); + this._insertedValues.push(value); } private _flushInserted(): void { @@ -49,7 +52,7 @@ export class SortedList { let sortedAddedValuesIndex = 0; let arrayIndex = 0; - const newArray = new Array(this._array.length + this._insertedValues.size); + const newArray = new Array(this._array.length + this._insertedValues.length); for (let newArrayIndex = 0; newArrayIndex < newArray.length; newArrayIndex++) { if (arrayIndex >= this._array.length || this._getKey(sortedAddedValues[sortedAddedValuesIndex]) === this._getKey(this._array[arrayIndex])) { @@ -61,11 +64,11 @@ export class SortedList { } this._array = newArray; - this._insertedValues.clear(); + this._insertedValues.length = 0; } private _flushCleanupInserted(): void { - if (!this._isFlushingInserted && this._insertedValues.size > 0) { + if (!this._isFlushingInserted && this._insertedValues.length > 0) { this._flushInsertedTask.flush(); } } @@ -88,10 +91,10 @@ export class SortedList { } do { if (this._array[i] === value) { - if (this._deletedIndices.size === 0) { + if (this._deletedIndices.length === 0) { this._flushDeletedTask.enqueue(() => this._flushDeleted()); } - this._deletedIndices.add(i); + this._deletedIndices.push(i); return true; } } while (++i < this._array.length && this._getKey(this._array[i]) === key); @@ -112,12 +115,12 @@ export class SortedList { } } this._array = newArray; - this._deletedIndices.clear(); + this._deletedIndices.length = 0; this._isFlushingDeleted = false; } private _flushCleanupDeleted(): void { - if (!this._isFlushingDeleted && this._deletedIndices.size > 0) { + if (!this._isFlushingDeleted && this._deletedIndices.length > 0) { this._flushDeletedTask.flush(); } } From ef1152a8fb0c5e7bdf5bda2bb433b7fe148c93b7 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Apr 2024 10:59:17 -0700 Subject: [PATCH 55/58] Doc --- src/common/SortedList.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts index eb88f368..67e1047e 100644 --- a/src/common/SortedList.ts +++ b/src/common/SortedList.ts @@ -9,9 +9,10 @@ import { IdleTaskQueue } from 'common/TaskQueue'; let i = 0; /** - * A generic list that is maintained in sorted order and allows values with duplicate keys. This - * list is based on binary search and as such locating a key will take O(log n) amortized, this - * includes the by key iterator. + * A generic list that is maintained in sorted order and allows values with duplicate keys. Deferred + * batch insertion and deletion is used to significantly reduce the time it takes to insert and + * delete a large amount of items in succession. This list is based on binary search and as such + * locating a key will take O(log n) amortized, this includes the by key iterator. */ export class SortedList { private _array: T[] = []; From 73fe7015b7776b80c78927ae28809a73f5b8ffcc Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Apr 2024 11:00:41 -0700 Subject: [PATCH 56/58] Use sort on object --- src/common/SortedList.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts index 67e1047e..1fb4f962 100644 --- a/src/common/SortedList.ts +++ b/src/common/SortedList.ts @@ -49,7 +49,7 @@ export class SortedList { } private _flushInserted(): void { - const sortedAddedValues = Array.from(this._insertedValues).sort((a, b) => this._getKey(a) - this._getKey(b)); + const sortedAddedValues = this._insertedValues.sort((a, b) => this._getKey(a) - this._getKey(b)); let sortedAddedValuesIndex = 0; let arrayIndex = 0; @@ -104,7 +104,7 @@ export class SortedList { private _flushDeleted(): void { this._isFlushingDeleted = true; - const sortedDeletedIndices = Array.from(this._deletedIndices).sort((a, b) => a - b); + const sortedDeletedIndices = this._deletedIndices.sort((a, b) => a - b); let sortedDeletedIndicesIndex = 0; const newArray = new Array(this._array.length - sortedDeletedIndices.length); let newArrayIndex = 0; From a4a7ce843ce1555c9ab6159ddba39100536db222 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Apr 2024 11:08:28 -0700 Subject: [PATCH 57/58] Reduce gc pressure in api verify integers --- src/browser/public/Terminal.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index a6349225..56edbc50 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -20,6 +20,8 @@ import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOption */ const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows']; +let $value = 0; + export class Terminal extends Disposable implements ITerminalApi { private _core: ITerminal; private _addonManager: AddonManager; @@ -249,16 +251,16 @@ export class Terminal extends Disposable implements ITerminalApi { } private _verifyIntegers(...values: number[]): void { - for (const value of values) { - if (value === Infinity || isNaN(value) || value % 1 !== 0) { + for ($value of values) { + if ($value === Infinity || isNaN($value) || $value % 1 !== 0) { throw new Error('This API only accepts integers'); } } } private _verifyPositiveIntegers(...values: number[]): void { - for (const value of values) { - if (value && (value === Infinity || isNaN(value) || value % 1 !== 0 || value < 0)) { + for ($value of values) { + if ($value && ($value === Infinity || isNaN($value) || $value % 1 !== 0 || $value < 0)) { throw new Error('This API only accepts positive integers'); } } From 6b4bf428de661f7269b777de68454dd08175050d Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Sat, 20 Apr 2024 11:15:14 -0700 Subject: [PATCH 58/58] Fix tests --- src/common/SortedList.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts index 1fb4f962..82b6dfa6 100644 --- a/src/common/SortedList.ts +++ b/src/common/SortedList.ts @@ -56,7 +56,7 @@ export class SortedList { const newArray = new Array(this._array.length + this._insertedValues.length); for (let newArrayIndex = 0; newArrayIndex < newArray.length; newArrayIndex++) { - if (arrayIndex >= this._array.length || this._getKey(sortedAddedValues[sortedAddedValuesIndex]) === this._getKey(this._array[arrayIndex])) { + if (arrayIndex >= this._array.length || this._getKey(sortedAddedValues[sortedAddedValuesIndex]) <= this._getKey(this._array[arrayIndex])) { newArray[newArrayIndex] = sortedAddedValues[sortedAddedValuesIndex]; sortedAddedValuesIndex++; } else {