Merge branch 'master' into patch-1

This commit is contained in:
Daniel Imms
2024-04-21 07:41:48 -07:00
committed by GitHub
44 changed files with 368 additions and 104 deletions
+5 -3
View File
@@ -1,13 +1,15 @@
{ {
"name": "xterm.js", "name": "xterm.js",
"image": "mcr.microsoft.com/devcontainers/typescript-node:0-18-buster", "image": "mcr.microsoft.com/devcontainers/typescript-node:18-bookworm",
"features": { "features": {
"ghcr.io/devcontainers/features/node:1": {} // yarn "ghcr.io/devcontainers/features/node:1": {
"version": 18
} // yarn
}, },
"forwardPorts": [ "forwardPorts": [
3000 3000
], ],
"postCreateCommand": "yarn install", "postCreateCommand": "yarn install && yarn setup",
"customizations": { "customizations": {
"vscode": { "vscode": {
"extensions": [ "extensions": [
+1 -1
View File
@@ -1 +1 @@
16 18
+1 -1
View File
@@ -61,7 +61,7 @@
"runtimeExecutable": "npm", "runtimeExecutable": "npm",
"runtimeArgs": ["start"], "runtimeArgs": ["start"],
"stopOnEntry": true, "stopOnEntry": true,
"runtimeVersion": "16", "runtimeVersion": "18",
"serverReadyAction": { "serverReadyAction": {
"action": "openExternally", "action": "openExternally",
"pattern": "App listening to (http://.*?:[0-9]+)" "pattern": "App listening to (http://.*?:[0-9]+)"
+5 -4
View File
@@ -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: 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 ```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 `<div id="terminal"></div>` onto which xterm can attach itself. Finally, instantiate the `Terminal` object and then call the `open` function with the DOM object of the `div`. 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 `<div id="terminal"></div>` 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
<!doctype html> <!doctype html>
<html> <html>
<head> <head>
<link rel="stylesheet" href="node_modules/xterm/css/xterm.css" /> <link rel="stylesheet" href="node_modules/@xterm/xterm/css/xterm.css" />
<script src="node_modules/xterm/lib/xterm.js"></script> <script src="node_modules/@xterm/xterm/lib/xterm.js"></script>
</head> </head>
<body> <body>
<div id="terminal"></div> <div id="terminal"></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: Our CI releases beta builds to npm for every change that goes into master. Install the latest beta build with:
```bash ```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. 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. - [**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. - [**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 - [**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) - [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. 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.
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xterm/addon-attach", "name": "@xterm/addon-attach",
"version": "0.9.0", "version": "0.11.0",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
"url": "https://xtermjs.org/" "url": "https://xtermjs.org/"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xterm/addon-canvas", "name": "@xterm/addon-canvas",
"version": "0.5.0", "version": "0.7.0",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
"url": "https://xtermjs.org/" "url": "https://xtermjs.org/"
+14 -2
View File
@@ -8,7 +8,7 @@ import { CellColorResolver } from 'browser/renderer/shared/CellColorResolver';
import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache'; import { acquireTextureAtlas } from 'browser/renderer/shared/CharAtlasCache';
import { TEXT_BASELINE } from 'browser/renderer/shared/Constants'; import { TEXT_BASELINE } from 'browser/renderer/shared/Constants';
import { tryDrawCustomChar } from 'browser/renderer/shared/CustomGlyphs'; 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 { createSelectionRenderModel } from 'browser/renderer/shared/SelectionRenderModel';
import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types'; import { IRasterizedGlyph, IRenderDimensions, ISelectionRenderModel, ITextureAtlas } from 'browser/renderer/shared/Types';
import { ICoreBrowserService, IThemeService } from 'browser/services/Services'; 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 { protected _drawChars(cell: ICellData, x: number, y: number): void {
const chars = cell.getChars(); const chars = cell.getChars();
const code = cell.getCode();
const width = cell.getWidth();
this._cellColorResolver.resolve(cell, x, this._bufferService.buffer.ydisp + y, this._deviceCellWidth); this._cellColorResolver.resolve(cell, x, this._bufferService.buffer.ydisp + y, this._deviceCellWidth);
if (!this._charAtlas) { if (!this._charAtlas) {
@@ -400,6 +402,16 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
this._bitmapGenerator[glyph.texturePage]!.refresh(); this._bitmapGenerator[glyph.texturePage]!.refresh();
this._bitmapGenerator[glyph.texturePage]!.version = this._charAtlas.pages[glyph.texturePage].version; 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._ctx.drawImage(
this._bitmapGenerator[glyph.texturePage]?.bitmap || this._charAtlas!.pages[glyph.texturePage].canvas, this._bitmapGenerator[glyph.texturePage]?.bitmap || this._charAtlas!.pages[glyph.texturePage].canvas,
glyph.texturePosition.x, glyph.texturePosition.x,
@@ -408,7 +420,7 @@ export abstract class BaseRenderLayer extends Disposable implements IRenderLayer
glyph.size.y, glyph.size.y,
x * this._deviceCellWidth + this._deviceCharLeft - glyph.offset.x, x * this._deviceCellWidth + this._deviceCharLeft - glyph.offset.x,
y * this._deviceCellHeight + this._deviceCharTop - glyph.offset.y, y * this._deviceCellHeight + this._deviceCharTop - glyph.offset.y,
glyph.size.x, renderWidth,
glyph.size.y glyph.size.y
); );
this._ctx.restore(); this._ctx.restore();
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xterm/addon-fit", "name": "@xterm/addon-fit",
"version": "0.8.0", "version": "0.10.0",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
"url": "https://xtermjs.org/" "url": "https://xtermjs.org/"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xterm/addon-image", "name": "@xterm/addon-image",
"version": "0.6.0", "version": "0.8.0",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
"url": "https://xtermjs.org/" "url": "https://xtermjs.org/"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xterm/addon-ligatures", "name": "@xterm/addon-ligatures",
"version": "0.7.0", "version": "0.9.0",
"description": "Add support for programming ligatures to xterm.js", "description": "Add support for programming ligatures to xterm.js",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xterm/addon-search", "name": "@xterm/addon-search",
"version": "0.13.0", "version": "0.15.0",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
"url": "https://xtermjs.org/" "url": "https://xtermjs.org/"
+8 -13
View File
@@ -6,7 +6,7 @@
import type { Terminal, IDisposable, ITerminalAddon, IDecoration } from '@xterm/xterm'; import type { Terminal, IDisposable, ITerminalAddon, IDecoration } from '@xterm/xterm';
import type { SearchAddon as ISearchApi } from '@xterm/addon-search'; import type { SearchAddon as ISearchApi } from '@xterm/addon-search';
import { EventEmitter } from 'common/EventEmitter'; 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 { export interface ISearchOptions {
regex?: boolean; regex?: boolean;
@@ -78,8 +78,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
*/ */
private _linesCache: LineCacheEntry[] | undefined; private _linesCache: LineCacheEntry[] | undefined;
private _linesCacheTimeoutId = 0; private _linesCacheTimeoutId = 0;
private _cursorMoveListener: IDisposable | undefined; private _linesCacheDisposables = new MutableDisposable();
private _resizeListener: IDisposable | undefined;
private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number }>()); private readonly _onDidChangeResults = this.register(new EventEmitter<{ resultIndex: number, resultCount: number }>());
public readonly onDidChangeResults = this._onDidChangeResults.event; public readonly onDidChangeResults = this._onDidChangeResults.event;
@@ -427,8 +426,11 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
const terminal = this._terminal!; const terminal = this._terminal!;
if (!this._linesCache) { if (!this._linesCache) {
this._linesCache = new Array(terminal.buffer.active.length); this._linesCache = new Array(terminal.buffer.active.length);
this._cursorMoveListener = terminal.onCursorMove(() => this._destroyLinesCache()); this._linesCacheDisposables.value = getDisposeArrayDisposable([
this._resizeListener = terminal.onResize(() => this._destroyLinesCache()); terminal.onLineFeed(() => this._destroyLinesCache()),
terminal.onCursorMove(() => this._destroyLinesCache()),
terminal.onResize(() => this._destroyLinesCache())
]);
} }
window.clearTimeout(this._linesCacheTimeoutId); window.clearTimeout(this._linesCacheTimeoutId);
@@ -437,14 +439,7 @@ export class SearchAddon extends Disposable implements ITerminalAddon , ISearchA
private _destroyLinesCache(): void { private _destroyLinesCache(): void {
this._linesCache = undefined; this._linesCache = undefined;
if (this._cursorMoveListener) { this._linesCacheDisposables.clear();
this._cursorMoveListener.dispose();
this._cursorMoveListener = undefined;
}
if (this._resizeListener) {
this._resizeListener.dispose();
this._resizeListener = undefined;
}
if (this._linesCacheTimeoutId) { if (this._linesCacheTimeoutId) {
window.clearTimeout(this._linesCacheTimeoutId); window.clearTimeout(this._linesCacheTimeoutId);
this._linesCacheTimeoutId = 0; this._linesCacheTimeoutId = 0;
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xterm/addon-serialize", "name": "@xterm/addon-serialize",
"version": "0.11.0", "version": "0.13.0",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
"url": "https://xtermjs.org/" "url": "https://xtermjs.org/"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xterm/addon-unicode-graphemes", "name": "@xterm/addon-unicode-graphemes",
"version": "0.1.0", "version": "0.3.0",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
"url": "https://xtermjs.org/" "url": "https://xtermjs.org/"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xterm/addon-unicode11", "name": "@xterm/addon-unicode11",
"version": "0.6.0", "version": "0.8.0",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
"url": "https://xtermjs.org/" "url": "https://xtermjs.org/"
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xterm/addon-web-links", "name": "@xterm/addon-web-links",
"version": "0.9.0", "version": "0.11.0",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
"url": "https://xtermjs.org/" "url": "https://xtermjs.org/"
+12 -18
View File
@@ -41,16 +41,18 @@ export class WebLinkProvider implements ILinkProvider {
} }
} }
function baseUrlString(url: URL): string { function isUrl(urlString: string): boolean {
if (url.password && url.username) { try {
return `${url.protocol}//${url.username}:${url.password}@${url.host}`; 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 { export class LinkComputer {
@@ -67,15 +69,7 @@ export class LinkComputer {
const text = match[0]; const text = match[0];
// check via URL if the matched text would form a proper url // check via URL if the matched text would form a proper url
// NOTE: This outsources the ugly url parsing to the browser. if (!isUrl(text)) {
// 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) {
continue; continue;
} }
+1 -1
View File
@@ -18,7 +18,7 @@ import { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider';
// - final interpunction like ,.!? // - final interpunction like ,.!?
// - any sort of brackets <>()[]{} (not spec conform, but often used to enclose urls) // - any sort of brackets <>()[]{} (not spec conform, but often used to enclose urls)
// - unsafe chars from rfc1738: {}|\^~[]` // - unsafe chars from rfc1738: {}|\^~[]`
const strictUrlRegex = /https?:[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/; const strictUrlRegex = /(https?|HTTPS?):[/]{2}[^\s"'!*(){}|\\\^<>`]*[^\s"':,.!?{}|\\\^~\[\]`()<>]/;
function handleLink(event: MouseEvent, uri: string): void { function handleLink(event: MouseEvent, uri: string): void {
@@ -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 } }); 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<void> { async function testHostName(hostname: string): Promise<void> {
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "@xterm/addon-webgl", "name": "@xterm/addon-webgl",
"version": "0.16.0", "version": "0.18.0",
"author": { "author": {
"name": "The xterm.js authors", "name": "The xterm.js authors",
"url": "https://xtermjs.org/" "url": "https://xtermjs.org/"
+15 -5
View File
@@ -3,7 +3,7 @@
* @license MIT * @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 { TextureAtlas } from 'browser/renderer/shared/TextureAtlas';
import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types'; import { IRasterizedGlyph, IRenderDimensions, ITextureAtlas } from 'browser/renderer/shared/Types';
import { NULL_CELL_CODE } from 'common/buffer/Constants'; import { NULL_CELL_CODE } from 'common/buffer/Constants';
@@ -11,6 +11,7 @@ import { Disposable, toDisposable } from 'common/Lifecycle';
import { Terminal } from '@xterm/xterm'; import { Terminal } from '@xterm/xterm';
import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types'; import { IRenderModel, IWebGL2RenderingContext, IWebGLVertexArrayObject } from './Types';
import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils'; import { createProgram, GLTexture, PROJECTION_MATRIX } from './WebglUtils';
import type { IOptionsService } from 'common/services/Services';
interface IVertices { interface IVertices {
attributes: Float32Array; attributes: Float32Array;
@@ -111,7 +112,8 @@ export class GlyphRenderer extends Disposable {
constructor( constructor(
private readonly _terminal: Terminal, private readonly _terminal: Terminal,
private readonly _gl: IWebGL2RenderingContext, private readonly _gl: IWebGL2RenderingContext,
private _dimensions: IRenderDimensions private _dimensions: IRenderDimensions,
private readonly _optionsService: IOptionsService
) { ) {
super(); super();
@@ -212,15 +214,15 @@ export class GlyphRenderer extends Disposable {
return this._atlas ? this._atlas.beginFrame() : true; 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 // 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 // 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 // slight slowdown is acceptable for the developer ergonomics provided as it's a once of for
// each glyph. // 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; $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 // 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; array[$i + 8] = $glyph.sizeClipSpace.y;
} }
// a_cellpos only changes on resize // 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 { public clear(): void {
+7 -4
View File
@@ -36,7 +36,8 @@ export class WebglRenderer extends Disposable implements IRenderer {
private _observerDisposable = this.register(new MutableDisposable()); private _observerDisposable = this.register(new MutableDisposable());
private _model: RenderModel = new RenderModel(); private _model: RenderModel = new RenderModel();
private _workCell: CellData = new CellData(); private _workCell: ICellData = new CellData();
private _workCell2: ICellData = new CellData();
private _cellColorResolver: CellColorResolver; private _cellColorResolver: CellColorResolver;
private _canvas: HTMLCanvasElement; private _canvas: HTMLCanvasElement;
@@ -245,7 +246,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
*/ */
private _initializeWebGLState(): [RectangleRenderer, GlyphRenderer] { private _initializeWebGLState(): [RectangleRenderer, GlyphRenderer] {
this._rectangleRenderer.value = new RectangleRenderer(this._terminal, this._gl, this.dimensions, this._themeService); 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 // Update dimensions and acquire char atlas
this.handleCharSizeChanged(); this.handleCharSizeChanged();
@@ -388,6 +389,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
let range: [number, number]; let range: [number, number];
let chars: string; let chars: string;
let code: number; let code: number;
let width: number;
let i: number; let i: number;
let x: number; let x: number;
let j: 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_FG_OFFSET] = this._cellColorResolver.result.fg;
this._model.cells[i + RENDER_MODEL_EXT_OFFSET] = this._cellColorResolver.result.ext; 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) { if (isJoined) {
// Restore work cell // Restore work cell
@@ -509,7 +512,7 @@ export class WebglRenderer extends Disposable implements IRenderer {
// Null out non-first cells // Null out non-first cells
for (x++; x < lastCharX; x++) { for (x++; x < lastCharX; x++) {
j = ((y * terminal.cols) + x) * RENDER_MODEL_INDICIES_PER_CELL; 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] = NULL_CELL_CODE;
this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg; this._model.cells[j + RENDER_MODEL_BG_OFFSET] = this._cellColorResolver.result.bg;
this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg; this._model.cells[j + RENDER_MODEL_FG_OFFSET] = this._cellColorResolver.result.fg;
+2 -3
View File
@@ -57,9 +57,8 @@ function checkAndPublishPackage(packageDir) {
const packageJson = require(path.join(packageDir, 'package.json')); const packageJson = require(path.join(packageDir, 'package.json'));
// Determine if this is a stable or beta release // Determine if this is a stable or beta release
// TODO: Uncomment when publishing 5.4 const publishedVersions = getPublishedVersions(packageJson);
// const publishedVersions = getPublishedVersions(packageJson); const isStableRelease = !publishedVersions.includes(packageJson.version);
const isStableRelease = false; //!publishedVersions.includes(packageJson.version);
// Get the next version // Get the next version
let nextVersion = isStableRelease ? packageJson.version : getNextBetaVersion(packageJson); let nextVersion = isStableRelease ? packageJson.version : getNextBetaVersion(packageJson);
+29 -1
View File
@@ -44,7 +44,7 @@ if ('WebAssembly' in window) {
// Pulling in the module's types relies on the <reference> above, it's looks a // Pulling in the module's types relies on the <reference> above, it's looks a
// little weird here as we're importing "this" module // 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 { export interface IWindowWithTerminal extends Window {
term: TerminalType; term: TerminalType;
@@ -255,6 +255,7 @@ if (document.location.pathname === '/test') {
document.getElementById('add-grapheme-clusters').addEventListener('click', addGraphemeClusters); document.getElementById('add-grapheme-clusters').addEventListener('click', addGraphemeClusters);
document.getElementById('add-decoration').addEventListener('click', addDecoration); document.getElementById('add-decoration').addEventListener('click', addDecoration);
document.getElementById('add-overview-ruler').addEventListener('click', addOverviewRuler); 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('weblinks-test').addEventListener('click', testWeblinks);
document.getElementById('bce').addEventListener('click', coloredErase); document.getElementById('bce').addEventListener('click', coloredErase);
addVtButtons(); addVtButtons();
@@ -1170,6 +1171,33 @@ function addOverviewRuler(): void {
term.registerDecoration({ marker: term.registerMarker(10), overviewRulerOptions: { color: '#ffffff80', position: 'full' } }); 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) => { (console as any).image = (source: ImageData | HTMLCanvasElement, scale: number = 1) => {
function getBox(width: number, height: number): any { function getBox(width: number, height: number): any {
return { return {
+1
View File
@@ -102,6 +102,7 @@
<dt>Decorations</dt> <dt>Decorations</dt>
<dd><button id="add-decoration" title="Add a decoration to the terminal">Decoration</button></dd> <dd><button id="add-decoration" title="Add a decoration to the terminal">Decoration</button></dd>
<dd><button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button></dd> <dd><button id="add-overview-ruler" title="Add an overview ruler to the terminal">Add Overview Ruler</button></dd>
<dd><button id="decoration-stress-test" title="Toggle between adding and removing a decoration to each line">Stress Test</button></dd>
<dt>Weblinks Addon</dt> <dt>Weblinks Addon</dt>
<dd><button id="weblinks-test" title="Various url conditions from demo data, hover&click to test">Test URLs</button></dd> <dd><button id="weblinks-test" title="Various url conditions from demo data, hover&click to test">Test URLs</button></dd>
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "@xterm/xterm", "name": "@xterm/xterm",
"description": "Full xterm terminal, in your browser", "description": "Full xterm terminal, in your browser",
"version": "5.3.0", "version": "5.5.0",
"main": "lib/xterm.js", "main": "lib/xterm.js",
"style": "css/xterm.css", "style": "css/xterm.css",
"types": "typings/xterm.d.ts", "types": "typings/xterm.d.ts",
+15 -2
View File
@@ -36,6 +36,8 @@ export class Viewport extends Disposable implements IViewport {
private _activeBuffer: IBuffer; private _activeBuffer: IBuffer;
private _renderDimensions: IRenderDimensions; 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 // 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 // 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. // 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 target: -1
}; };
private _ensureTimeout: number;
private readonly _onRequestScrollLines = this.register(new EventEmitter<{ amount: number, suppressScrollEvent: boolean }>()); private readonly _onRequestScrollLines = this.register(new EventEmitter<{ amount: number, suppressScrollEvent: boolean }>());
public readonly onRequestScrollLines = this._onRequestScrollLines.event; 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())); this.register(this._optionsService.onSpecificOptionChange('scrollback', () => this.syncScrollArea()));
// Perform this async to ensure the ICharSizeService is ready. // Perform this async to ensure the ICharSizeService is ready.
setTimeout(() => this.syncScrollArea()); this._ensureTimeout = window.setTimeout(() => this.syncScrollArea());
} }
private _handleThemeChange(colors: ReadonlyColorSet): void { private _handleThemeChange(colors: ReadonlyColorSet): void {
@@ -211,7 +215,12 @@ export class Viewport extends Disposable implements IViewport {
// Continue or finish smooth scroll // Continue or finish smooth scroll
if (percent < 1) { if (percent < 1) {
this._coreBrowserService.window.requestAnimationFrame(() => this._smoothScroll()); if (!this._smoothScrollAnimationFrame) {
this._smoothScrollAnimationFrame = this._coreBrowserService.window.requestAnimationFrame(() => {
this._smoothScrollAnimationFrame = 0;
this._smoothScroll();
});
}
} else { } else {
this._clearSmoothScrollState(); this._clearSmoothScrollState();
} }
@@ -398,4 +407,8 @@ export class Viewport extends Disposable implements IViewport {
this._viewportElement.scrollTop += deltaY; this._viewportElement.scrollTop += deltaY;
return this._bubbleScroll(ev, deltaY); return this._bubbleScroll(ev, deltaY);
} }
public dispose(): void {
clearTimeout(this._ensureTimeout);
}
} }
+3 -2
View File
@@ -159,8 +159,9 @@ export class CompositionHelper {
// otherwise input characters can be duplicated. (Issue #3191) // otherwise input characters can be duplicated. (Issue #3191)
currentCompositionPosition.start += this._dataAlreadySent.length; currentCompositionPosition.start += this._dataAlreadySent.length;
if (this._isComposing) { if (this._isComposing) {
// Use the end position to get the string if a new composition has started. // Use the start position of the new composition to get the string
input = this._textarea.value.substring(currentCompositionPosition.start, currentCompositionPosition.end); // if a new composition has started.
input = this._textarea.value.substring(currentCompositionPosition.start, this._compositionPosition.start);
} else { } else {
// Don't use the end position here in order to pick up any characters after the // 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 // composition has finished, for example when typing a non-composition character
+6 -4
View File
@@ -20,6 +20,8 @@ import { IBufferNamespace as IBufferNamespaceApi, IDecoration, IDecorationOption
*/ */
const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows']; const CONSTRUCTOR_ONLY_OPTIONS = ['cols', 'rows'];
let $value = 0;
export class Terminal extends Disposable implements ITerminalApi { export class Terminal extends Disposable implements ITerminalApi {
private _core: ITerminal; private _core: ITerminal;
private _addonManager: AddonManager; private _addonManager: AddonManager;
@@ -249,16 +251,16 @@ export class Terminal extends Disposable implements ITerminalApi {
} }
private _verifyIntegers(...values: number[]): void { private _verifyIntegers(...values: number[]): void {
for (const value of values) { for ($value of values) {
if (value === Infinity || isNaN(value) || value % 1 !== 0) { if ($value === Infinity || isNaN($value) || $value % 1 !== 0) {
throw new Error('This API only accepts integers'); throw new Error('This API only accepts integers');
} }
} }
} }
private _verifyPositiveIntegers(...values: number[]): void { private _verifyPositiveIntegers(...values: number[]): void {
for (const value of values) { for ($value of values) {
if (value && (value === Infinity || isNaN(value) || value % 1 !== 0 || value < 0)) { if ($value && ($value === Infinity || isNaN($value) || $value % 1 !== 0 || $value < 0)) {
throw new Error('This API only accepts positive integers'); throw new Error('This API only accepts positive integers');
} }
} }
+24 -5
View File
@@ -183,14 +183,23 @@ export class DomRenderer extends Disposable implements IRenderer {
` font-style: italic;` + ` font-style: italic;` +
`}`; `}`;
// Blink animation // Blink animation
const blinkAnimationUnderlineId = `blink_underline_${this._terminalClass}`;
const blinkAnimationBarId = `blink_bar_${this._terminalClass}`;
const blinkAnimationBlockId = `blink_block_${this._terminalClass}`;
styles += styles +=
`@keyframes blink_box_shadow` + `_` + this._terminalClass + ` {` + `@keyframes ${blinkAnimationUnderlineId} {` +
` 50% {` + ` 50% {` +
` border-bottom-style: hidden;` + ` border-bottom-style: hidden;` +
` }` + ` }` +
`}`; `}`;
styles += styles +=
`@keyframes blink_block` + `_` + this._terminalClass + ` {` + `@keyframes ${blinkAnimationBarId} {` +
` 50% {` +
` box-shadow: none;` +
` }` +
`}`;
styles +=
`@keyframes ${blinkAnimationBlockId} {` +
` 0% {` + ` 0% {` +
` background-color: ${colors.cursor.css};` + ` background-color: ${colors.cursor.css};` +
` color: ${colors.cursorAccent.css};` + ` color: ${colors.cursorAccent.css};` +
@@ -202,13 +211,23 @@ export class DomRenderer extends Disposable implements IRenderer {
`}`; `}`;
// Cursor // Cursor
styles += styles +=
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}:not(.${RowCss.CURSOR_STYLE_BLOCK_CLASS}) {` + `${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_UNDERLINE_CLASS} {` +
` animation: blink_box_shadow` + `_` + 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: ${blinkAnimationBarId} 1s step-end infinite;` +
`}` + `}` +
`${this._terminalSelector} .${ROW_CONTAINER_CLASS}.${FOCUS_CLASS} .${RowCss.CURSOR_CLASS}.${RowCss.CURSOR_BLINK_CLASS}.${RowCss.CURSOR_STYLE_BLOCK_CLASS} {` + `${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} {` + `${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;` + ` background-color: ${colors.cursor.css} !important;` +
` color: ${colors.cursorAccent.css} !important;` + ` color: ${colors.cursorAccent.css} !important;` +
`}` + `}` +
+11 -4
View File
@@ -134,9 +134,14 @@ export class WidthCache implements IDisposable {
public get(c: string, bold: boolean | number, italic: boolean | number): number { public get(c: string, bold: boolean | number, italic: boolean | number): number {
let cp = 0; let cp = 0;
if (!bold && !italic && c.length === 1 && (cp = c.charCodeAt(0)) < WidthCacheSettings.FLAT_SIZE) { if (!bold && !italic && c.length === 1 && (cp = c.charCodeAt(0)) < WidthCacheSettings.FLAT_SIZE) {
return this._flat[cp] !== WidthCacheSettings.FLAT_UNSET if (this._flat[cp] !== WidthCacheSettings.FLAT_UNSET) {
? this._flat[cp] return this._flat[cp];
: (this._flat[cp] = this._measure(c, 0)); }
const width = this._measure(c, 0);
if (width > 0) {
this._flat[cp] = width;
}
return width;
} }
let key = c; let key = c;
if (bold) key += 'B'; if (bold) key += 'B';
@@ -147,7 +152,9 @@ export class WidthCache implements IDisposable {
if (bold) variant |= FontVariant.BOLD; if (bold) variant |= FontVariant.BOLD;
if (italic) variant |= FontVariant.ITALIC; if (italic) variant |= FontVariant.ITALIC;
width = this._measure(c, variant); width = this._measure(c, variant);
this._holey!.set(key, width); if (width > 0) {
this._holey!.set(key, width);
}
} }
return width; return width;
} }
@@ -23,10 +23,43 @@ export function isRestrictedPowerlineGlyph(codepoint: number): boolean {
return 0xE0B0 <= codepoint && codepoint <= 0xE0B7; return 0xE0B0 <= codepoint && codepoint <= 0xE0B7;
} }
function isNerdFontGlyph(codepoint: number): boolean {
return 0xE000 <= codepoint && codepoint <= 0xF8FF;
}
function isBoxOrBlockGlyph(codepoint: number): boolean { function isBoxOrBlockGlyph(codepoint: number): boolean {
return 0x2500 <= codepoint && codepoint <= 0x259F; 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 { export function treatGlyphAsBackgroundColor(codepoint: number): boolean {
return isPowerlineGlyph(codepoint) || isBoxOrBlockGlyph(codepoint); return isPowerlineGlyph(codepoint) || isBoxOrBlockGlyph(codepoint);
} }
+2 -1
View File
@@ -87,7 +87,8 @@ export class RenderService extends Disposable implements IRenderService {
'fontSize', 'fontSize',
'fontWeight', 'fontWeight',
'fontWeightBold', 'fontWeightBold',
'minimumContrastRatio' 'minimumContrastRatio',
'rescaleOverlappingGlyphs'
], () => { ], () => {
this.clear(); this.clear();
this.handleResize(bufferService.cols, bufferService.rows); this.handleResize(bufferService.cols, bufferService.rows);
+5 -2
View File
@@ -3,7 +3,6 @@
* @license MIT * @license MIT
*/ */
import { isNode } from 'common/Platform';
import { IColor, IColorRGB } from 'common/Types'; import { IColor, IColorRGB } from 'common/Types';
let $r = 0; let $r = 0;
@@ -117,9 +116,10 @@ export namespace color {
* '#rrggbbaa'). * '#rrggbbaa').
*/ */
export namespace css { export namespace css {
// Attempt to set get the shared canvas context
let $ctx: CanvasRenderingContext2D | undefined; let $ctx: CanvasRenderingContext2D | undefined;
let $litmusColor: CanvasGradient | undefined; let $litmusColor: CanvasGradient | undefined;
if (!isNode) { try {
// This is guaranteed to run in the first window, so document should be correct // This is guaranteed to run in the first window, so document should be correct
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
canvas.width = 1; canvas.width = 1;
@@ -133,6 +133,9 @@ export namespace css {
$litmusColor = $ctx.createLinearGradient(0, 0, 1, 1); $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 * Converts a css string to an IColor, this should handle all valid CSS color strings and will
+1 -1
View File
@@ -2979,7 +2979,7 @@ export class InputHandler extends Disposable implements IInputHandler {
if (args[1]) { if (args[1]) {
return this._createHyperlink(args[0], args[1]); return this._createHyperlink(args[0], args[1]);
} }
if (args[0]) { if (args[0].trim()) {
return false; return false;
} }
return this._finishHyperlink(); return this._finishHyperlink();
+1 -1
View File
@@ -14,7 +14,7 @@ interface INavigator {
declare const navigator: INavigator; declare const navigator: INavigator;
declare const process: unknown; 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 userAgent = (isNode) ? 'node' : navigator.userAgent;
const platform = (isNode) ? 'node' : navigator.platform; const platform = (isNode) ? 'node' : navigator.platform;
+86 -10
View File
@@ -3,16 +3,27 @@
* @license MIT * @license MIT
*/ */
import { IdleTaskQueue } from 'common/TaskQueue';
// Work variables to avoid garbage collection. // Work variables to avoid garbage collection.
let i = 0; let i = 0;
/** /**
* A generic list that is maintained in sorted order and allows values with duplicate keys. This * A generic list that is maintained in sorted order and allows values with duplicate keys. Deferred
* list is based on binary search and as such locating a key will take O(log n) amortized, this * batch insertion and deletion is used to significantly reduce the time it takes to insert and
* includes the by key iterator. * 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<T> { export class SortedList<T> {
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( constructor(
private readonly _getKey: (value: T) => number private readonly _getKey: (value: T) => number
@@ -21,18 +32,50 @@ export class SortedList<T> {
public clear(): void { public clear(): void {
this._array.length = 0; 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 { public insert(value: T): void {
if (this._array.length === 0) { this._flushCleanupDeleted();
this._array.push(value); if (this._insertedValues.length === 0) {
return; 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 { public delete(value: T): boolean {
this._flushCleanupInserted();
if (this._array.length === 0) { if (this._array.length === 0) {
return false; return false;
} }
@@ -49,14 +92,43 @@ export class SortedList<T> {
} }
do { do {
if (this._array[i] === value) { 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; return true;
} }
} while (++i < this._array.length && this._getKey(this._array[i]) === key); } while (++i < this._array.length && this._getKey(this._array[i]) === key);
return false; 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<T> { public *getKeyIterator(key: number): IterableIterator<T> {
this._flushCleanupInserted();
this._flushCleanupDeleted();
if (this._array.length === 0) { if (this._array.length === 0) {
return; return;
} }
@@ -73,6 +145,8 @@ export class SortedList<T> {
} }
public forEachByKey(key: number, callback: (value: T) => void): void { public forEachByKey(key: number, callback: (value: T) => void): void {
this._flushCleanupInserted();
this._flushCleanupDeleted();
if (this._array.length === 0) { if (this._array.length === 0) {
return; return;
} }
@@ -89,6 +163,8 @@ export class SortedList<T> {
} }
public values(): IterableIterator<T> { public values(): IterableIterator<T> {
this._flushCleanupInserted();
this._flushCleanupDeleted();
// Duplicate the array to avoid issues when _array changes while iterating // Duplicate the array to avoid issues when _array changes while iterating
return [...this._array].values(); return [...this._array].values();
} }
+2 -1
View File
@@ -45,7 +45,8 @@ export class DecorationService extends Disposable implements IDecorationService
const decoration = new Decoration(options); const decoration = new Decoration(options);
if (decoration) { if (decoration) {
const markerDispose = decoration.marker.onDispose(() => decoration.dispose()); const markerDispose = decoration.marker.onDispose(() => decoration.dispose());
decoration.onDispose(() => { const listener = decoration.onDispose(() => {
listener.dispose();
if (decoration) { if (decoration) {
if (this._decorations.delete(decoration)) { if (this._decorations.delete(decoration)) {
this._onDecorationRemoved.fire(decoration); this._onDecorationRemoved.fire(decoration);
+1
View File
@@ -44,6 +44,7 @@ export const DEFAULT_OPTIONS: Readonly<Required<ITerminalOptions>> = {
allowTransparency: false, allowTransparency: false,
tabStopWidth: 8, tabStopWidth: 8,
theme: {}, theme: {},
rescaleOverlappingGlyphs: false,
rightClickSelectsWord: isMac, rightClickSelectsWord: isMac,
windowOptions: {}, windowOptions: {},
windowsMode: false, windowsMode: false,
+1
View File
@@ -234,6 +234,7 @@ export interface ITerminalOptions {
macOptionIsMeta?: boolean; macOptionIsMeta?: boolean;
macOptionClickForcesSelection?: boolean; macOptionClickForcesSelection?: boolean;
minimumContrastRatio?: number; minimumContrastRatio?: number;
rescaleOverlappingGlyphs?: boolean;
rightClickSelectsWord?: boolean; rightClickSelectsWord?: boolean;
rows?: number; rows?: number;
screenReaderMode?: boolean; screenReaderMode?: boolean;
+1
View File
@@ -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 onResize(): IEvent<{ cols: number, rows: number }> { return this._core.onResize; }
public get onScroll(): IEvent<number> { return this._core.onScroll; } public get onScroll(): IEvent<number> { return this._core.onScroll; }
public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; } public get onTitleChange(): IEvent<string> { return this._core.onTitleChange; }
public get onWriteParsed(): IEvent<void> { return this._core.onWriteParsed; }
public get parser(): IParser { public get parser(): IParser {
this._checkProposedApi(); this._checkProposedApi();
+1 -1
View File
@@ -6,7 +6,7 @@
import { IImage32, decodePng } from '@lunapaint/png-codec'; import { IImage32, decodePng } from '@lunapaint/png-codec';
import { LocatorScreenshotOptions, test } from '@playwright/test'; import { LocatorScreenshotOptions, test } from '@playwright/test';
import { ITheme } from '@xterm/xterm'; 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 { export interface ISharedRendererTestContext {
value: ITestContext; value: ITestContext;
+29 -1
View File
@@ -140,6 +140,23 @@ declare module '@xterm/headless' {
*/ */
minimumContrastRatio?: number; 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 * Whether to select the word under the cursor on right click, this is
* standard behavior in a lot of macOS applications. * 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 * The amount of scrollback in the terminal. Scrollback is the amount of
* rows that are retained when lines are scrolled beyond the initial * rows that are retained when lines are scrolled beyond the initial
* viewport. * viewport. Defaults to 1000.
*/ */
scrollback?: number; scrollback?: number;
@@ -697,6 +714,17 @@ declare module '@xterm/headless' {
*/ */
onLineFeed: IEvent<void>; onLineFeed: IEvent<void>;
/**
* 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<void>;
/** /**
* Adds an event listener for when the terminal is resized. The event value * Adds an event listener for when the terminal is resized. The event value
* contains the new size. * contains the new size.
+18 -1
View File
@@ -211,6 +211,23 @@ declare module '@xterm/xterm' {
*/ */
minimumContrastRatio?: number; 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 * Whether to select the word under the cursor on right click, this is
* standard behavior in a lot of macOS applications. * standard behavior in a lot of macOS applications.
@@ -227,7 +244,7 @@ declare module '@xterm/xterm' {
/** /**
* The amount of scrollback in the terminal. Scrollback is the amount of * The amount of scrollback in the terminal. Scrollback is the amount of
* rows that are retained when lines are scrolled beyond the initial * rows that are retained when lines are scrolled beyond the initial
* viewport. * viewport. Defaults to 1000.
*/ */
scrollback?: number; scrollback?: number;