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/README.md b/README.md
index 7514e76c..5f4a786a 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`.
@@ -30,8 +30,8 @@ To start using xterm.js on your browser, add the `xterm.js` and `xterm.css` to t
-
-
+
+
@@ -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.
@@ -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.
diff --git a/addons/addon-attach/package.json b/addons/addon-attach/package.json
index 94b483dd..71b1188d 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.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 9aabb001..2ca5d163 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.7.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
diff --git a/addons/addon-canvas/src/BaseRenderLayer.ts b/addons/addon-canvas/src/BaseRenderLayer.ts
index e7e23400..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 { 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';
@@ -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,16 @@ 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 (allowRescaling(code, width, glyph.size.x, this._deviceCellWidth)) {
+ 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 +420,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-fit/package.json b/addons/addon-fit/package.json
index 585f3621..cacf31dd 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.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 8572e330..ac4dc018 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.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 e5c6b972..80608ee3 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.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 369d11c3..9292a0c4 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.15.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
diff --git a/addons/addon-search/src/SearchAddon.ts b/addons/addon-search/src/SearchAddon.ts
index 3fae7373..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,8 +78,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
*/
private _linesCache: LineCacheEntry[] | undefined;
private _linesCacheTimeoutId = 0;
- 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;
@@ -427,8 +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._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);
@@ -437,14 +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;
- }
+ this._linesCacheDisposables.clear();
if (this._linesCacheTimeoutId) {
window.clearTimeout(this._linesCacheTimeoutId);
this._linesCacheTimeoutId = 0;
diff --git a/addons/addon-serialize/package.json b/addons/addon-serialize/package.json
index 763c52ca..30dadbb6 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.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 d49eda87..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.1.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 ad6a4892..9f2c21b8 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.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 6367907a..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.9.0",
+ "version": "0.11.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
diff --git a/addons/addon-web-links/src/WebLinkProvider.ts b/addons/addon-web-links/src/WebLinkProvider.ts
index 713f9c23..66691f44 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.toLocaleLowerCase());
+ } 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..99c11600 100644
--- a/addons/addon-web-links/test/WebLinksAddon.api.ts
+++ b/addons/addon-web-links/test/WebLinksAddon.api.ts
@@ -123,6 +123,21 @@ 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('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` +
+ ` 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`);
+ });
});
async function testHostName(hostname: string): Promise {
diff --git a/addons/addon-webgl/package.json b/addons/addon-webgl/package.json
index 9a31c306..da37eb26 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.18.0",
"author": {
"name": "The xterm.js authors",
"url": "https://xtermjs.org/"
diff --git a/addons/addon-webgl/src/GlyphRenderer.ts b/addons/addon-webgl/src/GlyphRenderer.ts
index 1fb0e18c..35b56eef 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 { 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';
@@ -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,14 @@ 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 (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
+ }
+ }
}
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/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);
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
diff --git a/package.json b/package.json
index 050b8cad..c79b1196 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.5.0",
"main": "lib/xterm.js",
"style": "css/xterm.css",
"types": "typings/xterm.d.ts",
diff --git a/src/browser/Viewport.ts b/src/browser/Viewport.ts
index a8e1a498..cb0f35ea 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.
@@ -49,6 +51,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 +85,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 {
@@ -211,7 +215,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();
}
@@ -398,4 +407,8 @@ export class Viewport extends Disposable implements IViewport {
this._viewportElement.scrollTop += deltaY;
return this._bubbleScroll(ev, deltaY);
}
+
+ public dispose(): void {
+ clearTimeout(this._ensureTimeout);
+ }
}
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
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');
}
}
diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts
index 1549b130..92d152f0 100644
--- a/src/browser/renderer/dom/DomRenderer.ts
+++ b/src/browser/renderer/dom/DomRenderer.ts
@@ -183,14 +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_box_shadow` + `_` + this._terminalClass + ` {` +
+ `@keyframes ${blinkAnimationUnderlineId} {` +
` 50% {` +
` border-bottom-style: hidden;` +
` }` +
`}`;
styles +=
- `@keyframes blink_block` + `_` + this._terminalClass + ` {` +
+ `@keyframes ${blinkAnimationBarId} {` +
+ ` 50% {` +
+ ` box-shadow: none;` +
+ ` }` +
+ `}`;
+ styles +=
+ `@keyframes ${blinkAnimationBlockId} {` +
` 0% {` +
` background-color: ${colors.cursor.css};` +
` color: ${colors.cursorAccent.css};` +
@@ -202,13 +211,23 @@ 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: ${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: ${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;` +
`}` +
+ // !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;` +
`}` +
@@ -324,6 +343,9 @@ export class DomRenderer extends Disposable implements IRenderer {
}
this._selectionRenderModel.update(this._terminal, start, end, columnSelectMode);
+ if (!this._selectionRenderModel.hasSelection) {
+ return;
+ }
// Translate from buffer position to viewport position
const viewportStartRow = this._selectionRenderModel.viewportStartRow;
@@ -331,11 +353,6 @@ export class DomRenderer extends Disposable implements IRenderer {
const viewportCappedStartRow = this._selectionRenderModel.viewportCappedStartRow;
const viewportCappedEndRow = this._selectionRenderModel.viewportCappedEndRow;
- // No need to draw the selection
- if (viewportCappedStartRow >= this._bufferService.rows || viewportCappedEndRow < 0) {
- return;
- }
-
// Create the selections
const documentFragment = this._document.createDocumentFragment();
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;
}
diff --git a/src/browser/renderer/shared/RendererUtils.ts b/src/browser/renderer/shared/RendererUtils.ts
index 9a4bffe0..01064364 100644
--- a/src/browser/renderer/shared/RendererUtils.ts
+++ b/src/browser/renderer/shared/RendererUtils.ts
@@ -23,10 +23,43 @@ 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;
}
+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 allowRescaling(codepoint: number | undefined, width: number, glyphSizeX: number, deviceCellWidth: number): boolean {
+ return (
+ // Is single cell width
+ width === 1 &&
+ // 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
+ !isEmoji(codepoint) &&
+ // Never rescale powerline or nerd fonts
+ !isPowerlineGlyph(codepoint) && !isNerdFontGlyph(codepoint)
+ );
+}
+
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/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
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();
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;
diff --git a/src/common/SortedList.ts b/src/common/SortedList.ts
index c3250091..82b6dfa6 100644
--- a/src/common/SortedList.ts
+++ b/src/common/SortedList.ts
@@ -3,16 +3,27 @@
* @license MIT
*/
+import { IdleTaskQueue } from 'common/TaskQueue';
+
// Work variables to avoid garbage collection.
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 readonly _array: T[] = [];
+ private _array: T[] = [];
+
+ private readonly _insertedValues: T[] = [];
+ private readonly _flushInsertedTask = new IdleTaskQueue();
+ private _isFlushingInserted = false;
+
+ private readonly _deletedIndices: number[] = [];
+ private readonly _flushDeletedTask = new IdleTaskQueue();
+ private _isFlushingDeleted = false;
constructor(
private readonly _getKey: (value: T) => number
@@ -21,18 +32,50 @@ export class SortedList {
public clear(): void {
this._array.length = 0;
+ 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 {
- if (this._array.length === 0) {
- this._array.push(value);
- return;
+ this._flushCleanupDeleted();
+ if (this._insertedValues.length === 0) {
+ this._flushInsertedTask.enqueue(() => this._flushInserted());
+ }
+ this._insertedValues.push(value);
+ }
+
+ private _flushInserted(): void {
+ const sortedAddedValues = 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._insertedValues.length);
+
+ 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._insertedValues.length = 0;
+ }
+
+ private _flushCleanupInserted(): void {
+ if (!this._isFlushingInserted && this._insertedValues.length > 0) {
+ this._flushInsertedTask.flush();
}
- i = this._search(this._getKey(value));
- this._array.splice(i, 0, value);
}
public delete(value: T): boolean {
+ this._flushCleanupInserted();
if (this._array.length === 0) {
return false;
}
@@ -49,14 +92,43 @@ export class SortedList {
}
do {
if (this._array[i] === value) {
- this._array.splice(i, 1);
+ if (this._deletedIndices.length === 0) {
+ this._flushDeletedTask.enqueue(() => this._flushDeleted());
+ }
+ this._deletedIndices.push(i);
return true;
}
} while (++i < this._array.length && this._getKey(this._array[i]) === key);
return false;
}
+ private _flushDeleted(): void {
+ this._isFlushingDeleted = true;
+ const sortedDeletedIndices = 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.length = 0;
+ this._isFlushingDeleted = false;
+ }
+
+ private _flushCleanupDeleted(): void {
+ if (!this._isFlushingDeleted && this._deletedIndices.length > 0) {
+ this._flushDeletedTask.flush();
+ }
+ }
+
public *getKeyIterator(key: number): IterableIterator {
+ this._flushCleanupInserted();
+ this._flushCleanupDeleted();
if (this._array.length === 0) {
return;
}
@@ -73,6 +145,8 @@ export class SortedList {
}
public forEachByKey(key: number, callback: (value: T) => void): void {
+ this._flushCleanupInserted();
+ this._flushCleanupDeleted();
if (this._array.length === 0) {
return;
}
@@ -89,6 +163,8 @@ export class SortedList {
}
public values(): IterableIterator {
+ this._flushCleanupInserted();
+ 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);
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/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/test/playwright/SharedRendererTests.ts b/test/playwright/SharedRendererTests.ts
index 29dc51c3..68492b3d 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;
@@ -1234,6 +1234,20 @@ export function injectSharedRendererTests(ctx: ISharedRendererTestContext): void
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, rows), [0, 0, 0, 255]);
await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, rows, CellColorPosition.FIRST), [0, 0, 255, 255]);
});
+ test('#4917 The selection should not be displayed if it is not within the scope of the viewport.', async () => {
+ const theme: ITheme = {
+ selectionBackground: '#FF0000'
+ };
+ await ctx.value.page.evaluate(`window.term.options.theme = ${JSON.stringify(theme)};`);
+ for (let index = 0; index < 160; index++) {
+ await ctx.value.proxy.writeln(``);
+ }
+ await ctx.value.proxy.scrollToBottom();
+ const rows = await ctx.value.proxy.buffer.active.length;
+ await ctx.value.proxy.selectLines(rows - 1, rows - 1);
+ await ctx.value.proxy.scrollLines(-2);
+ await pollFor(ctx.value.page, () => getCellColor(ctx.value, 1, 1), [0, 0, 0, 255]);
+ });
});
}
diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts
index b2ad5745..2d3329ed 100644
--- a/typings/xterm-headless.d.ts
+++ b/typings/xterm-headless.d.ts
@@ -140,6 +140,23 @@ 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. 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.
+ */
+ rescaleOverlappingGlyphs?: boolean;
+
/**
* Whether to select the word under the cursor on right click, this is
* standard behavior in a lot of macOS applications.
@@ -156,7 +173,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;
@@ -697,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.
diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts
index d957d655..33008289 100644
--- a/typings/xterm.d.ts
+++ b/typings/xterm.d.ts
@@ -209,6 +209,23 @@ 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. 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.
+ */
+ rescaleOverlappingGlyphs?: boolean;
+
/**
* Whether to select the word under the cursor on right click, this is
* standard behavior in a lot of macOS applications.
@@ -225,7 +242,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;