diff --git a/.eslintrc.json b/.eslintrc.json index 427c7a16d6..3a60f5bc77 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -14,6 +14,7 @@ "test/benchmark/tsconfig.json", "addons/xterm-addon-attach/src/tsconfig.json", "addons/xterm-addon-attach/test/tsconfig.json", + "addons/xterm-addon-canvas/src/tsconfig.json", "addons/xterm-addon-fit/src/tsconfig.json", "addons/xterm-addon-fit/test/tsconfig.json", "addons/xterm-addon-ligatures/src/tsconfig.json", diff --git a/README.md b/README.md index 2d1cba47ae..77a73cfe96 100644 --- a/README.md +++ b/README.md @@ -87,8 +87,6 @@ The xterm.js team maintains the following addons, but anyone can build them: Since xterm.js is typically implemented as a developer tool, only modern browsers are supported officially. Specifically the latest versions of *Chrome*, *Edge*, *Firefox*, and *Safari*. -We also partially support *Internet Explorer 11*, meaning xterm.js should work for the most part, but we reserve the right to not provide workarounds specifically for it unless it's absolutely necessary to get the basic input/output flow working. - Xterm.js works seamlessly in [Electron](https://electronjs.org/) apps and may even work on earlier versions of the browsers. These are the versions we strive to keep working. ### Node.js Support diff --git a/addons/xterm-addon-attach/test/AttachAddon.api.ts b/addons/xterm-addon-attach/test/AttachAddon.api.ts index 8335cf0f3f..a4827656a4 100644 --- a/addons/xterm-addon-attach/test/AttachAddon.api.ts +++ b/addons/xterm-addon-attach/test/AttachAddon.api.ts @@ -28,7 +28,7 @@ describe('AttachAddon', () => { beforeEach(async () => await page.goto(APP)); it('string', async function(): Promise { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); const port = 8080; const server = new WebSocket.Server({ port }); server.on('connection', socket => socket.send('foo')); @@ -38,7 +38,7 @@ describe('AttachAddon', () => { }); it('utf8', async function(): Promise { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); const port = 8080; const server = new WebSocket.Server({ port }); const data = new Uint8Array([102, 111, 111]); diff --git a/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts index 1aa213574c..2956b9d906 100644 --- a/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts +++ b/addons/xterm-addon-attach/typings/xterm-addon-attach.d.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm'; +import { Terminal, ITerminalAddon } from 'xterm'; declare module 'xterm-addon-attach' { export interface IAttachOptions { diff --git a/addons/xterm-addon-canvas/.gitignore b/addons/xterm-addon-canvas/.gitignore new file mode 100644 index 0000000000..a9f4ed5456 --- /dev/null +++ b/addons/xterm-addon-canvas/.gitignore @@ -0,0 +1,2 @@ +lib +node_modules \ No newline at end of file diff --git a/addons/xterm-addon-canvas/.npmignore b/addons/xterm-addon-canvas/.npmignore new file mode 100644 index 0000000000..b203232aff --- /dev/null +++ b/addons/xterm-addon-canvas/.npmignore @@ -0,0 +1,29 @@ +# Blacklist - exclude everything except npm defaults such as LICENSE, etc +* +!*/ + +# Whitelist - lib/ +!lib/**/*.d.ts + +!lib/**/*.js +!lib/**/*.js.map + +!lib/**/*.css + +# Whitelist - src/ +!src/**/*.ts +!src/**/*.d.ts + +!src/**/*.js +!src/**/*.js.map + +!src/**/*.css + +# Blacklist - src/ test files +src/**/*.test.ts +src/**/*.test.d.ts +src/**/*.test.js +src/**/*.test.js.map + +# Whitelist - typings/ +!typings/*.d.ts diff --git a/addons/xterm-addon-canvas/LICENSE b/addons/xterm-addon-canvas/LICENSE new file mode 100644 index 0000000000..e597698cc6 --- /dev/null +++ b/addons/xterm-addon-canvas/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2017, The xterm.js authors (https://github.com/xtermjs/xterm.js) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/addons/xterm-addon-canvas/README.md b/addons/xterm-addon-canvas/README.md new file mode 100644 index 0000000000..ed65967d38 --- /dev/null +++ b/addons/xterm-addon-canvas/README.md @@ -0,0 +1,23 @@ +## xterm-addon-canvas + +An addon for [xterm.js](https://github.com/xtermjs/xterm.js) that enables a canvas-based renderer using a 2d context to draw. This addon requires xterm.js v5+. + + +### Install + +```bash +npm install --save xterm-addon-canvas +``` + +### Usage + +```ts +import { Terminal } from 'xterm'; +import { CanvasAddon } from 'xterm-addon-canvas'; + +const terminal = new Terminal(); +terminal.open(element); +terminal.loadAddon(new CanvasAddon()); +``` + +See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts) for more advanced usage. diff --git a/addons/xterm-addon-canvas/package.json b/addons/xterm-addon-canvas/package.json new file mode 100644 index 0000000000..3d08e9db11 --- /dev/null +++ b/addons/xterm-addon-canvas/package.json @@ -0,0 +1,27 @@ +{ + "name": "xterm-addon-canvas", + "version": "0.12.0", + "author": { + "name": "The xterm.js authors", + "url": "https://xtermjs.org/" + }, + "main": "lib/xterm-addon-canvas.js", + "types": "typings/xterm-addon-canvas.d.ts", + "repository": "https://github.com/xtermjs/xterm.js", + "license": "MIT", + "keywords": [ + "terminal", + "canvas", + "xterm", + "xterm.js" + ], + "scripts": { + "build": "../../node_modules/.bin/tsc -p .", + "prepackage": "npm run build", + "package": "../../node_modules/.bin/webpack", + "prepublishOnly": "npm run package" + }, + "peerDependencies": { + "xterm": "^4.0.0" + } +} diff --git a/src/browser/renderer/BaseRenderLayer.ts b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts similarity index 98% rename from src/browser/renderer/BaseRenderLayer.ts rename to addons/xterm-addon-canvas/src/BaseRenderLayer.ts index 0a9b805701..9a6464c108 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/BaseRenderLayer.ts @@ -3,13 +3,14 @@ * @license MIT */ -import { IRenderDimensions, IRenderLayer } from 'browser/renderer/Types'; +import { IRenderDimensions } from 'browser/renderer/Types'; +import { IRenderLayer } from './Types'; import { ICellData, IColor } from 'common/Types'; import { DEFAULT_COLOR, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE, Attributes } from 'common/buffer/Constants'; -import { IGlyphIdentifier } from 'browser/renderer/atlas/Types'; -import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/atlas/Constants'; -import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas'; -import { acquireCharAtlas } from 'browser/renderer/atlas/CharAtlasCache'; +import { IGlyphIdentifier } from './atlas/Types'; +import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants'; +import { BaseCharAtlas } from './atlas/BaseCharAtlas'; +import { acquireCharAtlas } from './atlas/CharAtlasCache'; import { AttributeData } from 'common/buffer/AttributeData'; import { IColorSet } from 'browser/Types'; import { CellData } from 'common/buffer/CellData'; diff --git a/addons/xterm-addon-canvas/src/CanvasAddon.ts b/addons/xterm-addon-canvas/src/CanvasAddon.ts new file mode 100644 index 0000000000..8b8dcee698 --- /dev/null +++ b/addons/xterm-addon-canvas/src/CanvasAddon.ts @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2022 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IRenderService } from 'browser/services/Services'; +import { IColorSet } from 'browser/Types'; +import { CanvasRenderer } from './CanvasRenderer'; +import { IBufferService, IInstantiationService } from 'common/services/Services'; +import { ITerminalAddon, Terminal } from 'xterm'; + +export class CanvasAddon implements ITerminalAddon { + private _terminal?: Terminal; + private _renderer?: CanvasRenderer; + + public activate(terminal: Terminal): void { + if (!terminal.element) { + throw new Error('Cannot activate CanvasAddon before Terminal.open'); + } + this._terminal = terminal; + const instantiationService: IInstantiationService = (terminal as any)._core._instantiationService; + const bufferService: IBufferService = (terminal as any)._core._renderService; + const renderService: IRenderService = (terminal as any)._core._renderService; + const colors: IColorSet = (terminal as any)._core._colorManager.colors; + const screenElement: HTMLElement = (terminal as any)._core.screenElement; + const linkifier = (terminal as any)._core.linkifier2; + this._renderer = instantiationService.createInstance(CanvasRenderer, colors, screenElement, linkifier); + renderService.setRenderer(this._renderer); + renderService.onResize(bufferService.cols, bufferService.rows); + } + + public dispose(): void { + if (!this._terminal) { + throw new Error('Cannot dispose CanvasAddon because it is activated'); + } + const renderService: IRenderService = (this._terminal as any)._core._renderService; + renderService.setRenderer((this._terminal as any)._core._createRenderer()); + renderService.onResize(this._terminal.cols, this._terminal.rows); + this._renderer?.dispose(); + this._renderer = undefined; + } +} diff --git a/src/browser/renderer/Renderer.ts b/addons/xterm-addon-canvas/src/CanvasRenderer.ts similarity index 92% rename from src/browser/renderer/Renderer.ts rename to addons/xterm-addon-canvas/src/CanvasRenderer.ts index 8bc3227828..88c81ea751 100644 --- a/src/browser/renderer/Renderer.ts +++ b/addons/xterm-addon-canvas/src/CanvasRenderer.ts @@ -3,21 +3,22 @@ * @license MIT */ -import { TextRenderLayer } from 'browser/renderer/TextRenderLayer'; -import { SelectionRenderLayer } from 'browser/renderer/SelectionRenderLayer'; -import { CursorRenderLayer } from 'browser/renderer/CursorRenderLayer'; -import { IRenderLayer, IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; -import { LinkRenderLayer } from 'browser/renderer/LinkRenderLayer'; +import { TextRenderLayer } from './TextRenderLayer'; +import { SelectionRenderLayer } from './SelectionRenderLayer'; +import { CursorRenderLayer } from './CursorRenderLayer'; +import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; +import { IRenderLayer } from './Types'; +import { LinkRenderLayer } from './LinkRenderLayer'; import { Disposable } from 'common/Lifecycle'; -import { IColorSet, ILinkifier, ILinkifier2 } from 'browser/Types'; +import { IColorSet, ILinkifier2 } from 'browser/Types'; import { ICharSizeService } from 'browser/services/Services'; import { IBufferService, IOptionsService, IInstantiationService } from 'common/services/Services'; -import { removeTerminalFromCache } from 'browser/renderer/atlas/CharAtlasCache'; +import { removeTerminalFromCache } from './atlas/CharAtlasCache'; import { EventEmitter, IEvent } from 'common/EventEmitter'; let nextRendererId = 1; -export class Renderer extends Disposable implements IRenderer { +export class CanvasRenderer extends Disposable implements IRenderer { private _id = nextRendererId++; private _renderLayers: IRenderLayer[]; @@ -31,7 +32,6 @@ export class Renderer extends Disposable implements IRenderer { constructor( private _colors: IColorSet, private readonly _screenElement: HTMLElement, - linkifier: ILinkifier, linkifier2: ILinkifier2, @IInstantiationService instantiationService: IInstantiationService, @IBufferService private readonly _bufferService: IBufferService, @@ -43,7 +43,7 @@ export class Renderer extends Disposable implements IRenderer { this._renderLayers = [ instantiationService.createInstance(TextRenderLayer, this._screenElement, 0, this._colors, allowTransparency, this._id), instantiationService.createInstance(SelectionRenderLayer, this._screenElement, 1, this._colors, this._id), - instantiationService.createInstance(LinkRenderLayer, this._screenElement, 2, this._colors, this._id, linkifier, linkifier2), + instantiationService.createInstance(LinkRenderLayer, this._screenElement, 2, this._colors, this._id, linkifier2), instantiationService.createInstance(CursorRenderLayer, this._screenElement, 3, this._colors, this._id, this._onRequestRedraw) ]; this.dimensions = { diff --git a/src/browser/renderer/CursorRenderLayer.ts b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts similarity index 99% rename from src/browser/renderer/CursorRenderLayer.ts rename to addons/xterm-addon-canvas/src/CursorRenderLayer.ts index 3fa576a935..60c1301d39 100644 --- a/src/browser/renderer/CursorRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/CursorRenderLayer.ts @@ -4,7 +4,7 @@ */ import { IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; -import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; +import { BaseRenderLayer } from './BaseRenderLayer'; import { ICellData } from 'common/Types'; import { CellData } from 'common/buffer/CellData'; import { IColorSet } from 'browser/Types'; diff --git a/src/browser/renderer/GridCache.test.ts b/addons/xterm-addon-canvas/src/GridCache.test.ts similarity index 96% rename from src/browser/renderer/GridCache.test.ts rename to addons/xterm-addon-canvas/src/GridCache.test.ts index 30d22e8113..c4b1c220ee 100644 --- a/src/browser/renderer/GridCache.test.ts +++ b/addons/xterm-addon-canvas/src/GridCache.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { GridCache } from 'browser/renderer/GridCache'; +import { GridCache } from './GridCache'; describe('GridCache', () => { let grid: GridCache; diff --git a/src/browser/renderer/GridCache.ts b/addons/xterm-addon-canvas/src/GridCache.ts similarity index 100% rename from src/browser/renderer/GridCache.ts rename to addons/xterm-addon-canvas/src/GridCache.ts diff --git a/src/browser/renderer/LinkRenderLayer.ts b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts similarity index 86% rename from src/browser/renderer/LinkRenderLayer.ts rename to addons/xterm-addon-canvas/src/LinkRenderLayer.ts index 15086d9a2e..f92f6d54cb 100644 --- a/src/browser/renderer/LinkRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/LinkRenderLayer.ts @@ -5,9 +5,9 @@ import { IRenderDimensions } from 'browser/renderer/Types'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; -import { is256Color } from 'browser/renderer/atlas/CharAtlasUtils'; -import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; +import { is256Color } from './atlas/CharAtlasUtils'; +import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; export class LinkRenderLayer extends BaseRenderLayer { @@ -18,15 +18,12 @@ export class LinkRenderLayer extends BaseRenderLayer { zIndex: number, colors: IColorSet, rendererId: number, - linkifier: ILinkifier, linkifier2: ILinkifier2, @IBufferService bufferService: IBufferService, @IOptionsService optionsService: IOptionsService, @IDecorationService decorationService: IDecorationService ) { super(container, 'link', zIndex, true, colors, rendererId, bufferService, optionsService, decorationService); - linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); - linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); diff --git a/src/browser/renderer/SelectionRenderLayer.ts b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts similarity index 98% rename from src/browser/renderer/SelectionRenderLayer.ts rename to addons/xterm-addon-canvas/src/SelectionRenderLayer.ts index ce4fe0714e..2fa82c0d32 100644 --- a/src/browser/renderer/SelectionRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/SelectionRenderLayer.ts @@ -4,7 +4,7 @@ */ import { IRenderDimensions } from 'browser/renderer/Types'; -import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; +import { BaseRenderLayer } from './BaseRenderLayer'; import { IColorSet } from 'browser/Types'; import { IBufferService, IDecorationService, IOptionsService } from 'common/services/Services'; diff --git a/src/browser/renderer/TextRenderLayer.ts b/addons/xterm-addon-canvas/src/TextRenderLayer.ts similarity index 98% rename from src/browser/renderer/TextRenderLayer.ts rename to addons/xterm-addon-canvas/src/TextRenderLayer.ts index ef5a9b62ea..ea5fea0b75 100644 --- a/src/browser/renderer/TextRenderLayer.ts +++ b/addons/xterm-addon-canvas/src/TextRenderLayer.ts @@ -5,8 +5,8 @@ import { IRenderDimensions } from 'browser/renderer/Types'; import { CharData, ICellData } from 'common/Types'; -import { GridCache } from 'browser/renderer/GridCache'; -import { BaseRenderLayer } from 'browser/renderer/BaseRenderLayer'; +import { GridCache } from './GridCache'; +import { BaseRenderLayer } from './BaseRenderLayer'; import { AttributeData } from 'common/buffer/AttributeData'; import { NULL_CELL_CODE, Content } from 'common/buffer/Constants'; import { IColorSet } from 'browser/Types'; diff --git a/addons/xterm-addon-canvas/src/Types.d.ts b/addons/xterm-addon-canvas/src/Types.d.ts new file mode 100644 index 0000000000..6f5aff8500 --- /dev/null +++ b/addons/xterm-addon-canvas/src/Types.d.ts @@ -0,0 +1,110 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { IDisposable } from 'common/Types'; +import { IColorSet } from 'browser/Types'; +import { IEvent } from 'common/EventEmitter'; + +// TODO: Use core interfaces +export interface IRenderDimensions { + scaledCharWidth: number; + scaledCharHeight: number; + scaledCellWidth: number; + scaledCellHeight: number; + scaledCharLeft: number; + scaledCharTop: number; + scaledCanvasWidth: number; + scaledCanvasHeight: number; + canvasWidth: number; + canvasHeight: number; + actualCellWidth: number; + actualCellHeight: number; +} + +export interface IRequestRedrawEvent { + start: number; + end: number; +} + +/** + * Note that IRenderer implementations should emit the refresh event after + * rendering rows to the screen. + */ +export interface IRenderer extends IDisposable { + readonly dimensions: IRenderDimensions; + + /** + * Fires when the renderer is requesting to be redrawn on the next animation + * frame but is _not_ a result of content changing (eg. selection changes). + */ + readonly onRequestRedraw: IEvent; + + dispose(): void; + setColors(colors: IColorSet): void; + onDevicePixelRatioChange(): void; + onResize(cols: number, rows: number): void; + onCharSizeChanged(): void; + onBlur(): void; + onFocus(): void; + onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; + onCursorMove(): void; + onOptionsChanged(): void; + clear(): void; + renderRows(start: number, end: number): void; + clearTextureAtlas?(): void; +} + +export interface IRenderLayer extends IDisposable { + /** + * Called when the terminal loses focus. + */ + onBlur(): void; + + /** + * * Called when the terminal gets focus. + */ + onFocus(): void; + + /** + * Called when the cursor is moved. + */ + onCursorMove(): void; + + /** + * Called when options change. + */ + onOptionsChanged(): void; + + /** + * Called when the theme changes. + */ + setColors(colorSet: IColorSet): void; + + /** + * Called when the data in the grid has changed (or needs to be rendered + * again). + */ + onGridChanged(startRow: number, endRow: number): void; + + /** + * Calls when the selection changes. + */ + onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; + + /** + * Resize the render layer. + */ + resize(dim: IRenderDimensions): void; + + /** + * Clear the state of the render layer. + */ + reset(): void; + + /** + * Clears the texture atlas. + */ + clearTextureAtlas(): void; +} diff --git a/src/browser/renderer/atlas/BaseCharAtlas.ts b/addons/xterm-addon-canvas/src/atlas/BaseCharAtlas.ts similarity index 96% rename from src/browser/renderer/atlas/BaseCharAtlas.ts rename to addons/xterm-addon-canvas/src/atlas/BaseCharAtlas.ts index 83c30d2f06..03cf0285f7 100644 --- a/src/browser/renderer/atlas/BaseCharAtlas.ts +++ b/addons/xterm-addon-canvas/src/atlas/BaseCharAtlas.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IGlyphIdentifier } from 'browser/renderer/atlas/Types'; +import { IGlyphIdentifier } from './Types'; import { IDisposable } from 'common/Types'; export abstract class BaseCharAtlas implements IDisposable { diff --git a/src/browser/renderer/atlas/CharAtlasCache.ts b/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts similarity index 89% rename from src/browser/renderer/atlas/CharAtlasCache.ts rename to addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts index 257835ba05..7d020dcc42 100644 --- a/src/browser/renderer/atlas/CharAtlasCache.ts +++ b/addons/xterm-addon-canvas/src/atlas/CharAtlasCache.ts @@ -3,10 +3,10 @@ * @license MIT */ -import { generateConfig, configEquals } from 'browser/renderer/atlas/CharAtlasUtils'; -import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas'; -import { DynamicCharAtlas } from 'browser/renderer/atlas/DynamicCharAtlas'; -import { ICharAtlasConfig } from 'browser/renderer/atlas/Types'; +import { generateConfig, configEquals } from './CharAtlasUtils'; +import { BaseCharAtlas } from './BaseCharAtlas'; +import { DynamicCharAtlas } from './DynamicCharAtlas'; +import { ICharAtlasConfig } from './Types'; import { IColorSet } from 'browser/Types'; import { ITerminalOptions } from 'common/services/Services'; diff --git a/src/browser/renderer/atlas/CharAtlasUtils.ts b/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts similarity index 96% rename from src/browser/renderer/atlas/CharAtlasUtils.ts rename to addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts index 696c6c12c6..4c84c86a75 100644 --- a/src/browser/renderer/atlas/CharAtlasUtils.ts +++ b/addons/xterm-addon-canvas/src/atlas/CharAtlasUtils.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ICharAtlasConfig } from 'browser/renderer/atlas/Types'; +import { ICharAtlasConfig } from './Types'; import { DEFAULT_COLOR } from 'common/buffer/Constants'; import { IColorSet, IPartialColorSet } from 'browser/Types'; import { ITerminalOptions } from 'common/services/Services'; diff --git a/src/browser/renderer/atlas/DynamicCharAtlas.ts b/addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts similarity index 98% rename from src/browser/renderer/atlas/DynamicCharAtlas.ts rename to addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts index 590698798e..f5fc2c9997 100644 --- a/src/browser/renderer/atlas/DynamicCharAtlas.ts +++ b/addons/xterm-addon-canvas/src/atlas/DynamicCharAtlas.ts @@ -3,11 +3,11 @@ * @license MIT */ -import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/atlas/Constants'; -import { IGlyphIdentifier, ICharAtlasConfig } from 'browser/renderer/atlas/Types'; -import { BaseCharAtlas } from 'browser/renderer/atlas/BaseCharAtlas'; +import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, TEXT_BASELINE } from 'browser/renderer/Constants'; +import { IGlyphIdentifier, ICharAtlasConfig } from './Types'; +import { BaseCharAtlas } from './BaseCharAtlas'; import { DEFAULT_ANSI_COLORS } from 'browser/ColorManager'; -import { LRUMap } from 'browser/renderer/atlas/LRUMap'; +import { LRUMap } from './LRUMap'; import { isFirefox, isSafari } from 'common/Platform'; import { IColor } from 'common/Types'; import { throwIfFalsy } from 'browser/renderer/RendererUtils'; diff --git a/src/browser/renderer/atlas/LRUMap.test.ts b/addons/xterm-addon-canvas/src/atlas/LRUMap.test.ts similarity index 96% rename from src/browser/renderer/atlas/LRUMap.test.ts rename to addons/xterm-addon-canvas/src/atlas/LRUMap.test.ts index 792f85a718..b24ad2af03 100644 --- a/src/browser/renderer/atlas/LRUMap.test.ts +++ b/addons/xterm-addon-canvas/src/atlas/LRUMap.test.ts @@ -4,7 +4,7 @@ */ import { assert } from 'chai'; -import { LRUMap } from 'browser/renderer/atlas/LRUMap'; +import { LRUMap } from './LRUMap'; describe('LRUMap', () => { it('can be used to store and retrieve values', () => { diff --git a/src/browser/renderer/atlas/LRUMap.ts b/addons/xterm-addon-canvas/src/atlas/LRUMap.ts similarity index 100% rename from src/browser/renderer/atlas/LRUMap.ts rename to addons/xterm-addon-canvas/src/atlas/LRUMap.ts diff --git a/src/browser/renderer/atlas/Types.d.ts b/addons/xterm-addon-canvas/src/atlas/Types.d.ts similarity index 100% rename from src/browser/renderer/atlas/Types.d.ts rename to addons/xterm-addon-canvas/src/atlas/Types.d.ts diff --git a/addons/xterm-addon-canvas/src/tsconfig.json b/addons/xterm-addon-canvas/src/tsconfig.json new file mode 100644 index 0000000000..d954ec4983 --- /dev/null +++ b/addons/xterm-addon-canvas/src/tsconfig.json @@ -0,0 +1,41 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "lib": [ + "dom", + "es6" + ], + "rootDir": ".", + "outDir": "../out", + "sourceMap": true, + "removeComments": true, + "baseUrl": ".", + "paths": { + "common/*": [ + "../../../src/common/*" + ], + "browser/*": [ + "../../../src/browser/*" + ] + }, + "strict": true, + "downlevelIteration": true, + "experimentalDecorators": true, + "types": [ + "../../../node_modules/@types/mocha" + ] + }, + "include": [ + "./**/*", + "../../../typings/xterm.d.ts" + ], + "references": [ + { + "path": "../../../src/common" + }, + { + "path": "../../../src/browser" + } + ] +} diff --git a/addons/xterm-addon-canvas/tsconfig.json b/addons/xterm-addon-canvas/tsconfig.json new file mode 100644 index 0000000000..b711f30a6e --- /dev/null +++ b/addons/xterm-addon-canvas/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "include": [], + "references": [ + { "path": "./src" } + ] +} diff --git a/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts b/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts new file mode 100644 index 0000000000..7bbbd63db4 --- /dev/null +++ b/addons/xterm-addon-canvas/typings/xterm-addon-canvas.d.ts @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal, ITerminalAddon, IEvent } from 'xterm'; + +declare module 'xterm-addon-canvas' { + /** + * An xterm.js addon that provides search functionality. + */ + export class CanvasAddon implements ITerminalAddon { + public textureAtlas?: HTMLCanvasElement; + + constructor(); + + /** + * Activates the addon. + * @param terminal The terminal the addon is being loaded in. + */ + public activate(terminal: Terminal): void; + + /** + * Disposes the addon. + */ + public dispose(): void; + + /** + * Clears the terminal's texture atlas and triggers a redraw. + */ + public clearTextureAtlas(): void; + } +} diff --git a/addons/xterm-addon-canvas/webpack.config.js b/addons/xterm-addon-canvas/webpack.config.js new file mode 100644 index 0000000000..9c98760a1e --- /dev/null +++ b/addons/xterm-addon-canvas/webpack.config.js @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +const path = require('path'); + +const addonName = 'CanvasAddon'; +const mainFile = 'xterm-addon-canvas.js'; + +module.exports = { + entry: `./out/${addonName}.js`, + devtool: 'source-map', + module: { + rules: [ + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre", + exclude: /node_modules/ + } + ] + }, + resolve: { + modules: ['./node_modules'], + extensions: [ '.js' ], + alias: { + common: path.resolve('../../out/common'), + browser: path.resolve('../../out/browser') + } + }, + output: { + filename: mainFile, + path: path.resolve('./lib'), + library: addonName, + libraryTarget: 'umd' + }, + mode: 'production' +}; diff --git a/addons/xterm-addon-fit/test/FitAddon.api.ts b/addons/xterm-addon-fit/test/FitAddon.api.ts index 987092adcb..ef4618ea54 100644 --- a/addons/xterm-addon-fit/test/FitAddon.api.ts +++ b/addons/xterm-addon-fit/test/FitAddon.api.ts @@ -48,7 +48,8 @@ describe('FitAddon', () => { it('default', async function(): Promise { await loadFit(); const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`); - assert.equal(dimensions.cols, 87); + assert.isAbove(dimensions.cols, 85); + assert.isBelow(dimensions.cols, 88); assert.isAbove(dimensions.rows, 24); assert.isBelow(dimensions.rows, 29); }); @@ -56,7 +57,8 @@ describe('FitAddon', () => { it('width', async function(): Promise { await loadFit(1008); const dimensions: {cols: number, rows: number} = await page.evaluate(`window.fit.proposeDimensions()`); - assert.equal(dimensions.cols, 110); + assert.isAbove(dimensions.cols, 108); + assert.isBelow(dimensions.cols, 111); assert.isAbove(dimensions.rows, 24); assert.isBelow(dimensions.rows, 29); }); @@ -89,7 +91,8 @@ describe('FitAddon', () => { await page.evaluate(`window.fit.fit()`); const cols: number = await page.evaluate(`window.term.cols`); const rows: number = await page.evaluate(`window.term.rows`); - assert.equal(cols, 87); + assert.isAbove(cols, 85); + assert.isBelow(cols, 88); assert.isAbove(rows, 24); assert.isBelow(rows, 29); }); @@ -99,7 +102,8 @@ describe('FitAddon', () => { await page.evaluate(`window.fit.fit()`); const cols: number = await page.evaluate(`window.term.cols`); const rows: number = await page.evaluate(`window.term.rows`); - assert.equal(cols, 110); + assert.isAbove(cols, 108); + assert.isBelow(cols, 111); assert.isAbove(rows, 24); assert.isBelow(rows, 29); }); diff --git a/addons/xterm-addon-search/src/SearchAddon.ts b/addons/xterm-addon-search/src/SearchAddon.ts index 2553d11a51..689899ef19 100644 --- a/addons/xterm-addon-search/src/SearchAddon.ts +++ b/addons/xterm-addon-search/src/SearchAddon.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, IDisposable, ITerminalAddon, ISelectionPosition, IDecoration } from 'xterm'; +import { Terminal, IDisposable, ITerminalAddon, IBufferRange, IDecoration } from 'xterm'; import { EventEmitter } from 'common/EventEmitter'; export interface ISearchOptions { @@ -235,14 +235,14 @@ export class SearchAddon implements ITerminalAddon { let startCol = 0; let startRow = 0; - let currentSelection: ISelectionPosition | undefined; + let currentSelection: IBufferRange | undefined; if (this._terminal.hasSelection()) { const incremental = searchOptions ? searchOptions.incremental : false; // Start from the selection end if there is a selection // For incremental search, use existing row currentSelection = this._terminal.getSelectionPosition()!; - startRow = incremental ? currentSelection.startRow : currentSelection.endRow; - startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn; + startRow = incremental ? currentSelection.start.y : currentSelection.end.y; + startCol = incremental ? currentSelection.start.x : currentSelection.end.x; } this._initLinesCache(); @@ -282,7 +282,7 @@ export class SearchAddon implements ITerminalAddon { // If there is only one result, wrap back and return selection if it exists. if (!result && currentSelection) { - searchPosition.startRow = currentSelection.startRow; + searchPosition.startRow = currentSelection.start.y; searchPosition.startCol = 0; result = this._findInLine(term, searchPosition, searchOptions); } @@ -357,12 +357,12 @@ export class SearchAddon implements ITerminalAddon { const isReverseSearch = true; const incremental = searchOptions ? searchOptions.incremental : false; - let currentSelection: ISelectionPosition | undefined; + let currentSelection: IBufferRange | undefined; if (this._terminal.hasSelection()) { currentSelection = this._terminal.getSelectionPosition()!; // Start from selection start if there is a selection - startRow = currentSelection.startRow; - startCol = currentSelection.startColumn; + startRow = currentSelection.start.y; + startCol = currentSelection.start.x; } this._initLinesCache(); @@ -378,8 +378,8 @@ export class SearchAddon implements ITerminalAddon { if (!isOldResultHighlighted) { // If selection was not able to be expanded to the right, then try reverse search if (currentSelection) { - searchPosition.startRow = currentSelection.endRow; - searchPosition.startCol = currentSelection.endColumn; + searchPosition.startRow = currentSelection.end.y; + searchPosition.startCol = currentSelection.end.x; } result = this._findInLine(term, searchPosition, searchOptions, true); } diff --git a/addons/xterm-addon-search/test/SearchAddon.api.ts b/addons/xterm-addon-search/test/SearchAddon.api.ts index 4a80177649..f17ecd1b50 100644 --- a/addons/xterm-addon-search/test/SearchAddon.api.ts +++ b/addons/xterm-addon-search/test/SearchAddon.api.ts @@ -60,35 +60,35 @@ describe('Search Tests', function (): void { await page.evaluate(`window.term.writeln('package.jsonc\\n')`); await writeSync(page, 'package.json pack package.lock'); await page.evaluate(`window.search.findPrevious('pack', {incremental: true})`); - let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`); - let selectionPosition: { startColumn: number, startRow: number, endColumn: number, endRow: number } = await page.evaluate(`window.term.getSelectionPosition()`); + let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().start.y).translateToString()`); + let selectionPosition: { start: { x: number, y: number }, end: { x: number, y: number } } = await page.evaluate(`window.term.getSelectionPosition()`); // We look further ahead in the line to ensure that pack was selected from package.lock - assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock'); + assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 8), 'package.lock'); await page.evaluate(`window.search.findPrevious('package.j', {incremental: true})`); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json'); + assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 3), 'package.json'); await page.evaluate(`window.search.findPrevious('package.jsonc', {incremental: true})`); // We have to reevaluate line because it should have switched starting rows at this point - line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`); + line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().start.y).translateToString()`); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc'); + assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x), 'package.jsonc'); }); it('Incremental Find Next', async () => { await page.evaluate(`window.term.writeln('package.lock pack package.json package.ups\\n')`); await writeSync(page, 'package.jsonc'); await page.evaluate(`window.search.findNext('pack', {incremental: true})`); - let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`); - let selectionPosition: { startColumn: number, startRow: number, endColumn: number, endRow: number } = await page.evaluate(`window.term.getSelectionPosition()`); + let line: string = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().start.y).translateToString()`); + let selectionPosition: { start: { x: number, y: number }, end: { x: number, y: number } } = await page.evaluate(`window.term.getSelectionPosition()`); // We look further ahead in the line to ensure that pack was selected from package.lock - assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 8), 'package.lock'); + assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 8), 'package.lock'); await page.evaluate(`window.search.findNext('package.j', {incremental: true})`); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn + 3), 'package.json'); + assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x + 3), 'package.json'); await page.evaluate(`window.search.findNext('package.jsonc', {incremental: true})`); // We have to reevaluate line because it should have switched starting rows at this point - line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().startRow).translateToString()`); + line = await page.evaluate(`window.term.buffer.active.getLine(window.term.getSelectionPosition().start.y).translateToString()`); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(line.substring(selectionPosition.startColumn, selectionPosition.endColumn), 'package.jsonc'); + assert.deepEqual(line.substring(selectionPosition.start.x, selectionPosition.end.x), 'package.jsonc'); }); it('Simple Regex', async () => { await writeSync(page, 'abc123defABCD'); @@ -116,10 +116,14 @@ describe('Search Tests', function (): void { assert.deepEqual(await page.evaluate(`window.term.getSelection()`), '𝄞'); assert.deepEqual(await page.evaluate(`window.search.findNext('𝄞')`), true); assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), { - startRow: 0, - endRow: 0, - startColumn: 7, - endColumn: 8 + start: { + x: 7, + y: 0 + }, + end: { + x: 8, + y: 0 + } }); }); @@ -313,69 +317,70 @@ describe('Search Tests', function (): void { await writeSync(page, fixture); assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 }); + assert.deepEqual(selectionPosition, { start: { x: 24, y: 53 }, end: { x: 30, y: 53 } }); assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 }); + assert.deepEqual(selectionPosition, { start: { x: 24, y: 76 }, end: { x: 30, y: 76 } }); assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 }); + assert.deepEqual(selectionPosition, { start: { x: 24, y: 96 }, end: { x: 30, y: 96 } }); assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 }); + assert.deepEqual(selectionPosition, { start: { x: 1, y: 114 }, end: { x: 7, y: 114 } }); assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 }); + assert.deepEqual(selectionPosition, { start: { x: 11, y: 115 }, end: { x: 17, y: 115 } }); assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 }); + assert.deepEqual(selectionPosition, { start: { x: 1, y: 126 }, end: { x: 7, y: 126 } }); assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 }); + assert.deepEqual(selectionPosition, { start: { x: 11, y: 127 }, end: { x: 17, y: 127 } }); assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 }); + assert.deepEqual(selectionPosition, { start: { x: 1, y: 135 }, end: { x: 7, y: 135 } }); assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 }); + assert.deepEqual(selectionPosition, { start: { x: 11, y: 136 }, end: { x: 17, y: 136 } }); // Wrap around to first result assert.deepEqual(await page.evaluate(`window.search.findNext('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 }); + assert.deepEqual(selectionPosition, { start: { x: 24, y: 53 }, end: { x: 30, y: 53 } }); }); - it('should find all occurrences using findPrevious', async () => { + + it('should y all occurrences using findPrevious', async () => { await writeSync(page, fixture); assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); let selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 }); + assert.deepEqual(selectionPosition, { start: { x: 11, y: 136 }, end: { x: 17, y: 136 } }); assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 135, endColumn: 7, endRow: 135 }); + assert.deepEqual(selectionPosition, { start: { x: 1, y: 135 }, end: { x: 7, y: 135 } }); assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 127, endColumn: 17, endRow: 127 }); + assert.deepEqual(selectionPosition, { start: { x: 11, y: 127 }, end: { x: 17, y: 127 } }); assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 126, endColumn: 7, endRow: 126 }); + assert.deepEqual(selectionPosition, { start: { x: 1, y: 126 }, end: { x: 7, y: 126 } }); assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 115, endColumn: 17, endRow: 115 }); + assert.deepEqual(selectionPosition, { start: { x: 11, y: 115 }, end: { x: 17, y: 115 } }); assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 1, startRow: 114, endColumn: 7, endRow: 114 }); + assert.deepEqual(selectionPosition, { start: { x: 1, y: 114 }, end: { x: 7, y: 114 } }); assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 96, endColumn: 30, endRow: 96 }); + assert.deepEqual(selectionPosition, { start: { x: 24, y: 96 }, end: { x: 30, y: 96 } }); assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 76, endColumn: 30, endRow: 76 }); + assert.deepEqual(selectionPosition, { start: { x: 24, y: 76 }, end: { x: 30, y: 76 } }); assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 24, startRow: 53, endColumn: 30, endRow: 53 }); + assert.deepEqual(selectionPosition, { start: { x: 24, y: 53 }, end: { x: 30, y: 53 } }); // Wrap around to first result assert.deepEqual(await page.evaluate(`window.search.findPrevious('opencv')`), true); selectionPosition = await page.evaluate(`window.term.getSelectionPosition()`); - assert.deepEqual(selectionPosition, { startColumn: 11, startRow: 136, endColumn: 17, endRow: 136 }); + assert.deepEqual(selectionPosition, { start: { x: 11, y: 136 }, end: { x: 17, y: 136 } }); }); }); }); diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts index 67b838845f..05f2c61c7a 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.test.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.test.ts @@ -74,7 +74,7 @@ describe('xterm-addon-serialize', () => { } }); - terminal = new Terminal({ cols: 10, rows: 2 }); + terminal = new Terminal({ cols: 10, rows: 2, allowProposedApi: true }); terminal.loadAddon(serializeAddon); selectionService = new TestSelectionService((terminal as any)._core._bufferService); diff --git a/addons/xterm-addon-serialize/src/SerializeAddon.ts b/addons/xterm-addon-serialize/src/SerializeAddon.ts index ed71e4e13b..9996737482 100644 --- a/addons/xterm-addon-serialize/src/SerializeAddon.ts +++ b/addons/xterm-addon-serialize/src/SerializeAddon.ts @@ -443,8 +443,8 @@ export class SerializeAddon implements ITerminalAddon { const selection = this._terminal?.getSelectionPosition(); if (selection !== undefined) { return handler.serialize({ - start: { x: selection.startRow, y: selection.startColumn }, - end: { x: selection.endRow, y: selection.endColumn } + start: { x: selection.start.y, y: selection.start.x }, + end: { x: selection.end.y, y: selection.end.x } }); } diff --git a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts index b86b066bdb..157b7072fc 100644 --- a/addons/xterm-addon-serialize/test/SerializeAddon.api.ts +++ b/addons/xterm-addon-serialize/test/SerializeAddon.api.ts @@ -42,7 +42,7 @@ describe('SerializeAddon', () => { page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); await page.goto(APP); - await openTerminal(page, { rows: 10, cols: 10, rendererType: 'dom' }); + await openTerminal(page, { rows: 10, cols: 10 }); await page.evaluate(` window.serializeAddon = new SerializeAddon(); window.term.loadAddon(window.serializeAddon); diff --git a/addons/xterm-addon-web-links/src/WebLinksAddon.ts b/addons/xterm-addon-web-links/src/WebLinksAddon.ts index 285ef5dc51..1e3c877d1d 100644 --- a/addons/xterm-addon-web-links/src/WebLinksAddon.ts +++ b/addons/xterm-addon-web-links/src/WebLinksAddon.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ILinkMatcherOptions, ITerminalAddon, IDisposable } from 'xterm'; +import { Terminal, ITerminalAddon, IDisposable } from 'xterm'; import { ILinkProviderOptions, WebLinkProvider } from './WebLinkProvider'; const protocolClause = '(https?:\\/\\/)'; @@ -41,37 +41,23 @@ function handleLink(event: MouseEvent, uri: string): void { } export class WebLinksAddon implements ITerminalAddon { - private _linkMatcherId: number | undefined; private _terminal: Terminal | undefined; private _linkProvider: IDisposable | undefined; constructor( private _handler: (event: MouseEvent, uri: string) => void = handleLink, - private _options: ILinkMatcherOptions | ILinkProviderOptions = {}, - private _useLinkProvider: boolean = false + private _options: ILinkProviderOptions = {} ) { } public activate(terminal: Terminal): void { this._terminal = terminal; - - if (this._useLinkProvider && 'registerLinkProvider' in this._terminal) { - const options = this._options as ILinkProviderOptions; - const regex = options.urlRegex || strictUrlRegex; - this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, regex, this._handler, options)); - } else { - // TODO: This should be removed eventually - const options = this._options as ILinkMatcherOptions; - options.matchIndex = 1; - this._linkMatcherId = (this._terminal as Terminal).registerLinkMatcher(strictUrlRegex, this._handler, options); - } + const options = this._options as ILinkProviderOptions; + const regex = options.urlRegex || strictUrlRegex; + this._linkProvider = this._terminal.registerLinkProvider(new WebLinkProvider(this._terminal, regex, this._handler, options)); } public dispose(): void { - if (this._linkMatcherId !== undefined && this._terminal !== undefined) { - this._terminal.deregisterLinkMatcher(this._linkMatcherId); - } - this._linkProvider?.dispose(); } } diff --git a/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts b/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts index fe44dc31e1..bc97808567 100644 --- a/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts +++ b/addons/xterm-addon-web-links/test/WebLinksAddon.api.ts @@ -38,7 +38,7 @@ describe('WebLinksAddon', () => { }); async function testHostName(hostname: string): Promise { - await openTerminal(page, { rendererType: 'dom', cols: 40 }); + await openTerminal(page, { cols: 40 }); await page.evaluate(`window.term.loadAddon(new window.WebLinksAddon())`); const data = ` http://${hostname} \\r\\n` + ` http://${hostname}/a~b#c~d?e~f \\r\\n` + diff --git a/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts b/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts index 5e6c266d40..dc69d73240 100644 --- a/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts +++ b/addons/xterm-addon-web-links/typings/xterm-addon-web-links.d.ts @@ -4,7 +4,7 @@ */ -import { Terminal, ILinkMatcherOptions, ITerminalAddon, IViewportRange } from 'xterm'; +import { Terminal, ITerminalAddon, IViewportRange } from 'xterm'; declare module 'xterm-addon-web-links' { /** @@ -14,13 +14,9 @@ declare module 'xterm-addon-web-links' { /** * Creates a new web links addon. * @param handler The callback when the link is called. - * @param options Options for the link matcher. - * @param useLinkProvider Whether to use the new link provider API to create - * the links. This is an option because use of both link matcher (old) and - * link provider (new) may cause issues. Link provider will eventually be - * the default and only option. + * @param options Options for the link provider. */ - constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions | ILinkProviderOptions, useLinkProvider?: boolean); + constructor(handler?: (event: MouseEvent, uri: string) => void, options?: ILinkProviderOptions); /** * Activates the addon @@ -39,8 +35,7 @@ declare module 'xterm-addon-web-links' { */ export interface ILinkProviderOptions { /** - * A callback that fires when the mouse hovers over a link for a period of - * time (defined by {@link ITerminalOptions.linkTooltipHoverDuration}). + * A callback that fires when the mouse hovers over a link. */ hover?(event: MouseEvent, text: string, location: IViewportRange): void; @@ -50,7 +45,7 @@ declare module 'xterm-addon-web-links' { */ leave?(event: MouseEvent, text: string): void; - /** + /** * A callback to use instead of the default one. */ urlRegex?: RegExp; diff --git a/addons/xterm-addon-webgl/README.md b/addons/xterm-addon-webgl/README.md index da63e743ab..2519fb7b67 100644 --- a/addons/xterm-addon-webgl/README.md +++ b/addons/xterm-addon-webgl/README.md @@ -16,6 +16,7 @@ import { Terminal } from 'xterm'; import { WebglAddon } from 'xterm-addon-webgl'; const terminal = new Terminal(); +terminal.open(element); terminal.loadAddon(new WebglAddon()); ``` diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 05751c4216..8294c65113 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -4,14 +4,14 @@ */ import { ICharAtlasConfig } from './Types'; -import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/atlas/Constants'; +import { DIM_OPACITY, TEXT_BASELINE } from 'browser/renderer/Constants'; import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from '../Types'; import { DEFAULT_COLOR, Attributes } from 'common/buffer/Constants'; import { throwIfFalsy } from '../WebglUtils'; import { IColor } from 'common/Types'; import { IDisposable } from 'xterm'; import { AttributeData } from 'common/buffer/AttributeData'; -import { channels, color, rgba } from 'common/Color'; +import { color, rgba } from 'common/Color'; import { tryDrawCustomChar } from 'browser/renderer/CustomGlyphs'; import { excludeFromContrastRatioDemands, isPowerlineGlyph } from 'browser/renderer/RendererUtils'; diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts index 0a5a0065eb..0aa2049c34 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -7,7 +7,7 @@ import { IRenderLayer } from './Types'; import { acquireCharAtlas } from '../atlas/CharAtlasCache'; import { Terminal } from 'xterm'; import { IColorSet } from 'browser/Types'; -import { TEXT_BASELINE } from 'browser/renderer/atlas/Constants'; +import { TEXT_BASELINE } from 'browser/renderer/Constants'; import { IRenderDimensions } from 'browser/renderer/Types'; import { CellData } from 'common/buffer/CellData'; import { WebglCharAtlas } from 'atlas/WebglCharAtlas'; diff --git a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts index 8f6b3e6dab..440dbe7b30 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/LinkRenderLayer.ts @@ -5,7 +5,7 @@ import { Terminal } from 'xterm'; import { BaseRenderLayer } from './BaseRenderLayer'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; import { is256Color } from '../atlas/CharAtlasUtils'; import { ITerminal, IColorSet, ILinkifierEvent } from 'browser/Types'; import { IRenderDimensions } from 'browser/renderer/Types'; @@ -15,8 +15,6 @@ export class LinkRenderLayer extends BaseRenderLayer { constructor(container: HTMLElement, zIndex: number, colors: IColorSet, terminal: ITerminal) { super(container, 'link', zIndex, true, colors); - terminal.linkifier.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); - terminal.linkifier.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); terminal.linkifier2.onShowLinkUnderline(e => this._onShowLinkUnderline(e)); terminal.linkifier2.onHideLinkUnderline(e => this._onHideLinkUnderline(e)); diff --git a/addons/xterm-addon-webgl/src/tsconfig.json b/addons/xterm-addon-webgl/src/tsconfig.json index b0c9f6be8d..206d52ae8a 100644 --- a/addons/xterm-addon-webgl/src/tsconfig.json +++ b/addons/xterm-addon-webgl/src/tsconfig.json @@ -4,7 +4,7 @@ "target": "es5", "lib": [ "dom", - "es6", + "es6" ], "rootDir": ".", "outDir": "../out", diff --git a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts index 13a15cf340..797f0531bf 100644 --- a/addons/xterm-addon-webgl/test/WebglRenderer.api.ts +++ b/addons/xterm-addon-webgl/test/WebglRenderer.api.ts @@ -860,7 +860,7 @@ describe('WebGL Renderer Integration Tests', async () => { describe('allowTransparency', async () => { if (areTestsEnabled) { - before(async () => setupBrowser({ rendererType: 'dom', allowTransparency: true })); + before(async () => setupBrowser({ allowTransparency: true })); after(async () => browser.close()); beforeEach(async () => page.evaluate(`window.term.reset()`)); } @@ -879,7 +879,7 @@ describe('WebGL Renderer Integration Tests', async () => { describe('selectionForeground', () => { if (areTestsEnabled) { - before(async () => setupBrowser({ rendererType: 'dom' })); + before(async () => setupBrowser()); after(async () => browser.close()); beforeEach(async () => page.evaluate(`window.term.reset()`)); } @@ -898,7 +898,7 @@ describe('WebGL Renderer Integration Tests', async () => { describe('decoration color overrides', async () => { if (areTestsEnabled) { - before(async () => setupBrowser({ rendererType: 'dom' })); + before(async () => setupBrowser()); after(async () => browser.close()); beforeEach(async () => page.evaluate(`window.term.reset()`)); } @@ -1014,7 +1014,7 @@ async function getCellPixels(col: number, row: number): Promise { return await page.evaluate(`Array.from(window.result)`); } -async function setupBrowser(options: ITerminalOptions = { rendererType: 'dom' }): Promise { +async function setupBrowser(options: ITerminalOptions = {}): Promise { browser = await launchBrowser(); page = await (await browser.newContext()).newPage(); await page.setViewportSize({ width, height }); diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 34a17ca956..1710922e9a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -1,7 +1,10 @@ -# Node.js -# Build a general Node.js application with npm. -# Add steps that analyze code, save build artifacts, deploy, and more: -# https://docs.microsoft.com/vsts/pipelines/languages/javascript +pr: + branches: + include: ["main", "v5"] + +trigger: + branches: + include: ["main", "v5"] jobs: - job: Linux diff --git a/bin/publish.js b/bin/publish.js index a43b8cd406..cb6c836a43 100644 --- a/bin/publish.js +++ b/bin/publish.js @@ -28,6 +28,7 @@ if (changedFiles.some(e => e.search(/^addons\//) === -1)) { // Publish addons if any files were changed inside of the addon const addonPackageDirs = [ path.resolve(__dirname, '../addons/xterm-addon-attach'), + path.resolve(__dirname, '../addons/xterm-addon-canvas'), path.resolve(__dirname, '../addons/xterm-addon-fit'), path.resolve(__dirname, '../addons/xterm-addon-ligatures'), path.resolve(__dirname, '../addons/xterm-addon-search'), @@ -97,8 +98,9 @@ function getNextBetaVersion(packageJson) { process.exit(1); } const tag = 'beta'; - const stableVersion = packageJson.version.split('.'); - const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`; + // const stableVersion = packageJson.version.split('.'); + // const nextStableVersion = `${stableVersion[0]}.${parseInt(stableVersion[1]) + 1}.0`; + const nextStableVersion = `5.0.0`; const publishedVersions = getPublishedVersions(packageJson, nextStableVersion, tag); if (publishedVersions.length === 0) { return `${nextStableVersion}-${tag}.1`; diff --git a/demo/client.ts b/demo/client.ts index cfab9aa860..3ee533d0e5 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -10,6 +10,7 @@ // Use tsc version (yarn watch) import { Terminal } from '../out/browser/public/Terminal'; import { AttachAddon } from '../addons/xterm-addon-attach/out/AttachAddon'; +import { CanvasAddon } from '../addons/xterm-addon-canvas/out/CanvasAddon'; import { FitAddon } from '../addons/xterm-addon-fit/out/FitAddon'; import { SearchAddon, ISearchOptions } from '../addons/xterm-addon-search/out/SearchAddon'; import { SerializeAddon } from '../addons/xterm-addon-serialize/out/SerializeAddon'; @@ -53,13 +54,14 @@ let socketURL; let socket; let pid; -type AddonType = 'attach' | 'fit' | 'search' | 'serialize' | 'unicode11' | 'web-links' | 'webgl' | 'ligatures'; +type AddonType = 'attach' | 'canvas' | 'fit' | 'search' | 'serialize' | 'unicode11' | 'web-links' | 'webgl' | 'ligatures'; interface IDemoAddon { name: T; canChange: boolean; ctor: T extends 'attach' ? typeof AttachAddon : + T extends 'canvas' ? typeof CanvasAddon : T extends 'fit' ? typeof FitAddon : T extends 'search' ? typeof SearchAddon : T extends 'serialize' ? typeof SerializeAddon : @@ -69,6 +71,7 @@ interface IDemoAddon { typeof WebglAddon; instance?: T extends 'attach' ? AttachAddon : + T extends 'canvas' ? CanvasAddon : T extends 'fit' ? FitAddon : T extends 'search' ? SearchAddon : T extends 'serialize' ? SerializeAddon : @@ -81,6 +84,7 @@ interface IDemoAddon { const addons: { [T in AddonType]: IDemoAddon} = { attach: { name: 'attach', ctor: AttachAddon, canChange: false }, + canvas: { name: 'canvas', ctor: CanvasAddon, canChange: true }, fit: { name: 'fit', ctor: FitAddon, canChange: false }, search: { name: 'search', ctor: SearchAddon, canChange: true }, serialize: { name: 'serialize', ctor: SerializeAddon, canChange: true }, @@ -150,6 +154,7 @@ const disposeRecreateButtonHandler = () => { window.term = null; socket = null; addons.attach.instance = undefined; + addons.canvas.instance = undefined; addons.fit.instance = undefined; addons.search.instance = undefined; addons.serialize.instance = undefined; @@ -194,6 +199,7 @@ function createTerminal(): void { const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0; term = new Terminal({ + allowProposedApi: true, allowTransparency: true, windowsMode: isWindows, fontFamily: 'Fira Code, courier-new, courier, monospace', @@ -324,15 +330,12 @@ function initOptions(term: TerminalType): void { 'windowOptions' ]; const stringOptions = { - bellSound: null, - bellStyle: ['none', 'sound'], cursorStyle: ['block', 'underline', 'bar'], fastScrollModifier: ['alt', 'ctrl', 'shift', undefined], fontFamily: null, fontWeight: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], fontWeightBold: ['normal', 'bold', '100', '200', '300', '400', '500', '600', '700', '800', '900'], logLevel: ['debug', 'info', 'warn', 'error', 'off'], - rendererType: ['dom', 'canvas'], theme: ['default', 'xtermjs', 'sapphire', 'light'], wordSeparator: null }; @@ -629,7 +632,7 @@ function writeCustomGlyphHandler() { } function loadTest() { - const isWebglEnabled = !!addons.webgl.instance; + const rendererName = addons.webgl.instance ? 'webgl' : !!addons.canvas.instance ? 'canvas' : 'dom'; const testData = []; let byteCount = 0; for (let i = 0; i < 50; i++) { @@ -655,7 +658,7 @@ function loadTest() { term.write('', () => { const time = Math.round(performance.now() - start); const mbs = ((byteCount / 1024) * (1 / (time / 1000))).toFixed(2); - term.write(`\n\r\nWrote ${byteCount}kB in ${time}ms (${mbs}MB/s) using the (${isWebglEnabled ? 'webgl' : 'canvas'} renderer)`); + term.write(`\n\r\nWrote ${byteCount}kB in ${time}ms (${mbs}MB/s) using the (${rendererName} renderer)`); // Send ^C to get a new prompt term._core._onData.fire('\x03'); }); @@ -737,7 +740,7 @@ function powerlineSymbolTest() { function addDecoration() { term.options['overviewRulerWidth'] = 15; - const marker = term.addMarker(1); + const marker = term.registerMarker(1); const decoration = term.registerDecoration({ marker, backgroundColor: '#00FF00', @@ -752,13 +755,13 @@ function addDecoration() { function addOverviewRuler() { term.options['overviewRulerWidth'] = 15; - term.registerDecoration({marker: term.addMarker(1), overviewRulerOptions: { color: '#ef2929' }}); - term.registerDecoration({marker: term.addMarker(3), overviewRulerOptions: { color: '#8ae234' }}); - term.registerDecoration({marker: term.addMarker(5), overviewRulerOptions: { color: '#729fcf' }}); - term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#ef2929', position: 'left' }}); - term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#8ae234', position: 'center' }}); - term.registerDecoration({marker: term.addMarker(7), overviewRulerOptions: { color: '#729fcf', position: 'right' }}); - term.registerDecoration({marker: term.addMarker(10), overviewRulerOptions: { color: '#8ae234', position: 'center' }}); - term.registerDecoration({marker: term.addMarker(10), overviewRulerOptions: { color: '#ffffff80', position: 'full' }}); + term.registerDecoration({marker: term.registerMarker(1), overviewRulerOptions: { color: '#ef2929' }}); + term.registerDecoration({marker: term.registerMarker(3), overviewRulerOptions: { color: '#8ae234' }}); + term.registerDecoration({marker: term.registerMarker(5), overviewRulerOptions: { color: '#729fcf' }}); + term.registerDecoration({marker: term.registerMarker(7), overviewRulerOptions: { color: '#ef2929', position: 'left' }}); + term.registerDecoration({marker: term.registerMarker(7), overviewRulerOptions: { color: '#8ae234', position: 'center' }}); + term.registerDecoration({marker: term.registerMarker(7), overviewRulerOptions: { color: '#729fcf', position: 'right' }}); + term.registerDecoration({marker: term.registerMarker(10), overviewRulerOptions: { color: '#8ae234', position: 'center' }}); + term.registerDecoration({marker: term.registerMarker(10), overviewRulerOptions: { color: '#ffffff80', position: 'full' }}); } diff --git a/src/browser/Linkifier.test.ts b/src/browser/Linkifier.test.ts deleted file mode 100644 index a07f69ed23..0000000000 --- a/src/browser/Linkifier.test.ts +++ /dev/null @@ -1,244 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; -import { IMouseZoneManager, IMouseZone, IRegisteredLinkMatcher } from 'browser/Types'; -import { IBufferLine } from 'common/Types'; -import { Linkifier } from 'browser/Linkifier'; -import { BufferLine } from 'common/buffer/BufferLine'; -import { CellData } from 'common/buffer/CellData'; -import { MockLogService, MockBufferService } from 'common/TestUtils.test'; -import { IBufferService } from 'common/services/Services'; -import { UnicodeService } from 'common/services/UnicodeService'; - -class TestLinkifier extends Linkifier { - constructor(bufferService: IBufferService) { - super(bufferService, new MockLogService(), new UnicodeService()); - Linkifier._timeBeforeLatency = 0; - } - - public get linkMatchers(): IRegisteredLinkMatcher[] { return this._linkMatchers; } - public linkifyRows(): void { super.linkifyRows(0, this._bufferService.buffer.lines.length - 1); } -} - -class TestMouseZoneManager implements IMouseZoneManager { - public dispose(): void { - } - public clears: number = 0; - public zones: IMouseZone[] = []; - public add(zone: IMouseZone): void { - this.zones.push(zone); - } - public clearAll(): void { - this.clears++; - } -} - -describe('Linkifier', () => { - let bufferService: IBufferService; - let linkifier: TestLinkifier; - let mouseZoneManager: TestMouseZoneManager; - - beforeEach(() => { - bufferService = new MockBufferService(100, 10); - linkifier = new TestLinkifier(bufferService); - mouseZoneManager = new TestMouseZoneManager(); - }); - - function stringToRow(text: string): IBufferLine { - const result = new BufferLine(text.length); - for (let i = 0; i < text.length; i++) { - result.setCell(i, CellData.fromCharData([0, text.charAt(i), 1, text.charCodeAt(i)])); - } - return result; - } - - function addRow(text: string): void { - bufferService.buffer.lines.push(stringToRow(text)); - } - - function assertLinkifiesRow(rowText: string, linkMatcherRegex: RegExp, links: {x: number, length: number}[], done: Mocha.Done): void { - addRow(rowText); - linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); - linkifier.linkifyRows(); - // Allow linkify to happen - setTimeout(() => { - assert.equal(mouseZoneManager.zones.length, links.length); - links.forEach((l, i) => { - assert.equal(mouseZoneManager.zones[i].x1, l.x + 1); - assert.equal(mouseZoneManager.zones[i].x2, l.x + l.length + 1); - assert.equal(mouseZoneManager.zones[i].y1, bufferService.buffer.lines.length); - assert.equal(mouseZoneManager.zones[i].y2, bufferService.buffer.lines.length); - }); - done(); - }, 0); - } - - function assertLinkifiesMultiLineLink(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: Mocha.Done): void { - addRow(rowText); - linkifier.registerLinkMatcher(linkMatcherRegex, () => {}); - linkifier.linkifyRows(); - // Allow linkify to happen - setTimeout(() => { - assert.equal(mouseZoneManager.zones.length, links.length); - links.forEach((l, i) => { - assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1); - assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1); - assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1); - assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1); - }); - done(); - }, 0); - } - - describe('before attachToDom', () => { - it('should allow link matcher registration', done => { - assert.doesNotThrow(() => { - const linkMatcherId = linkifier.registerLinkMatcher(/foo/, () => {}); - assert.isTrue(linkifier.deregisterLinkMatcher(linkMatcherId)); - done(); - }); - }); - }); - - describe('after attachToDom', () => { - beforeEach(() => { - linkifier.attachToDom({} as any, mouseZoneManager); - }); - - describe('link matcher', () => { - it('should match a single link', done => { - assertLinkifiesRow('foo', /foo/, [{x: 0, length: 3}], done); - }); - it('should match a single link at the start of a text node', done => { - assertLinkifiesRow('foo bar', /foo/, [{x: 0, length: 3}], done); - }); - it('should match a single link in the middle of a text node', done => { - assertLinkifiesRow('foo bar baz', /bar/, [{x: 4, length: 3}], done); - }); - it('should match a single link at the end of a text node', done => { - assertLinkifiesRow('foo bar', /bar/, [{x: 4, length: 3}], done); - }); - it('should match a link after a link at the start of a text node', done => { - assertLinkifiesRow('foo bar', /foo|bar/, [{x: 0, length: 3}, {x: 4, length: 3}], done); - }); - it('should match a link after a link in the middle of a text node', done => { - assertLinkifiesRow('foo bar baz', /bar|baz/, [{x: 4, length: 3}, {x: 8, length: 3}], done); - }); - it('should match a link immediately after a link at the end of a text node', done => { - assertLinkifiesRow('foo barbaz', /bar|baz/, [{x: 4, length: 3}, {x: 7, length: 3}], done); - }); - it('should not duplicate text after a unicode character (wrapped in a span)', done => { - // This is a regression test for an issue that came about when using - // an oh-my-zsh theme that added the large blue diamond unicode - // character (U+1F537) which caused the path to be duplicated. See #642. - assertLinkifiesRow('echo \'🔷foo\'', /foo/, [{x: 8, length: 3}], done); - }); - describe('multi-line links', () => { - it('should match links that start on line 1/2 of a wrapped line and end on the last character of line 1/2', done => { - bufferService.resize(4, bufferService.rows); - bufferService.buffer.lines.length = 0; - assertLinkifiesMultiLineLink('12345', /1234/, [{x1: 0, x2: 4, y1: 0, y2: 0}], done); - }); - it('should match links that start on line 1/2 of a wrapped line and wrap to line 2/2', done => { - bufferService.resize(4, bufferService.rows); - bufferService.buffer.lines.length = 0; - assertLinkifiesMultiLineLink('12345', /12345/, [{x1: 0, x2: 1, y1: 0, y2: 1}], done); - }); - it('should match links that start and end on line 2/2 of a wrapped line', done => { - bufferService.resize(4, bufferService.rows); - bufferService.buffer.lines.length = 0; - assertLinkifiesMultiLineLink('12345678', /5678/, [{x1: 0, x2: 4, y1: 1, y2: 1}], done); - }); - it('should match links that start on line 2/3 of a wrapped line and wrap to line 3/3', done => { - bufferService.resize(4, bufferService.rows); - bufferService.buffer.lines.length = 0; - assertLinkifiesMultiLineLink('123456789', /56789/, [{x1: 0, x2: 1, y1: 1, y2: 2}], done); - }); - }); - }); - - describe('validationCallback', () => { - it('should enable link if true', done => { - bufferService.buffer.lines.length = 0; - addRow('test'); - linkifier.registerLinkMatcher(/test/, () => done(), { - validationCallback: (url, cb) => { - assert.equal(mouseZoneManager.zones.length, 0); - cb(true); - assert.equal(mouseZoneManager.zones.length, 1); - assert.equal(mouseZoneManager.zones[0].x1, 1); - assert.equal(mouseZoneManager.zones[0].x2, 5); - assert.equal(mouseZoneManager.zones[0].y1, 1); - assert.equal(mouseZoneManager.zones[0].y2, 1); - // Fires done() - mouseZoneManager.zones[0].clickCallback({} as any); - } - }); - linkifier.linkifyRows(); - }); - - it('should validate the uri, not the row', done => { - addRow('abc test abc'); - linkifier.registerLinkMatcher(/test/, () => done(), { - validationCallback: (uri, cb) => { - assert.equal(uri, 'test'); - done(); - } - }); - linkifier.linkifyRows(); - }); - - it('should disable link if false', done => { - addRow('test'); - linkifier.registerLinkMatcher(/test/, () => assert.fail(), { - validationCallback: (url, cb) => { - assert.equal(mouseZoneManager.zones.length, 0); - cb(false); - assert.equal(mouseZoneManager.zones.length, 0); - } - }); - linkifier.linkifyRows(); - // Allow time for the validation callback to be performed - setTimeout(() => done(), 10); - }); - - it('should trigger for multiple link matches on one row', done => { - addRow('test test'); - let count = 0; - linkifier.registerLinkMatcher(/test/, () => assert.fail(), { - validationCallback: (url, cb) => { - count++; - if (count === 2) { - done(); - } - cb(false); - } - }); - linkifier.linkifyRows(); - }); - }); - - describe('priority', () => { - it('should order the list from highest priority to lowest #1', () => { - const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 1 }); - const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: -1 }); - assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]); - }); - - it('should order the list from highest priority to lowest #2', () => { - const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: -1 }); - const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 1 }); - assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [bId, aId]); - }); - - it('should order items of equal priority in the order they are added', () => { - const aId = linkifier.registerLinkMatcher(/a/, () => {}, { priority: 0 }); - const bId = linkifier.registerLinkMatcher(/b/, () => {}, { priority: 0 }); - assert.deepEqual(linkifier.linkMatchers.map(lm => lm.id), [aId, bId]); - }); - }); - }); -}); diff --git a/src/browser/Linkifier.ts b/src/browser/Linkifier.ts deleted file mode 100644 index b17d66a86e..0000000000 --- a/src/browser/Linkifier.ts +++ /dev/null @@ -1,356 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { ILinkifierEvent, ILinkMatcher, LinkMatcherHandler, ILinkMatcherOptions, ILinkifier, IMouseZoneManager, IMouseZone, IRegisteredLinkMatcher } from 'browser/Types'; -import { IBufferStringIteratorResult } from 'common/buffer/Types'; -import { EventEmitter, IEvent } from 'common/EventEmitter'; -import { ILogService, IBufferService, IOptionsService, IUnicodeService } from 'common/services/Services'; - -/** - * Limit of the unwrapping line expansion (overscan) at the top and bottom - * of the actual viewport in ASCII characters. - * A limit of 2000 should match most sane urls. - */ -const OVERSCAN_CHAR_LIMIT = 2000; - -/** - * The Linkifier applies links to rows shortly after they have been refreshed. - */ -export class Linkifier implements ILinkifier { - /** - * The time to wait after a row is changed before it is linkified. This prevents - * the costly operation of searching every row multiple times, potentially a - * huge amount of times. - */ - protected static _timeBeforeLatency = 200; - - protected _linkMatchers: IRegisteredLinkMatcher[] = []; - - private _mouseZoneManager: IMouseZoneManager | undefined; - private _element: HTMLElement | undefined; - - private _rowsTimeoutId: number | undefined; - private _nextLinkMatcherId = 0; - private _rowsToLinkify: { start: number | undefined, end: number | undefined }; - - private _onShowLinkUnderline = new EventEmitter(); - public get onShowLinkUnderline(): IEvent { return this._onShowLinkUnderline.event; } - private _onHideLinkUnderline = new EventEmitter(); - public get onHideLinkUnderline(): IEvent { return this._onHideLinkUnderline.event; } - private _onLinkTooltip = new EventEmitter(); - public get onLinkTooltip(): IEvent { return this._onLinkTooltip.event; } - - constructor( - @IBufferService protected readonly _bufferService: IBufferService, - @ILogService private readonly _logService: ILogService, - @IUnicodeService private readonly _unicodeService: IUnicodeService - ) { - this._rowsToLinkify = { - start: undefined, - end: undefined - }; - } - - /** - * Attaches the linkifier to the DOM, enabling linkification. - * @param mouseZoneManager The mouse zone manager to register link zones with. - */ - public attachToDom(element: HTMLElement, mouseZoneManager: IMouseZoneManager): void { - this._element = element; - this._mouseZoneManager = mouseZoneManager; - } - - /** - * Queue linkification on a set of rows. - * @param start The row to linkify from (inclusive). - * @param end The row to linkify to (inclusive). - */ - public linkifyRows(start: number, end: number): void { - // Don't attempt linkify if not yet attached to DOM - if (!this._mouseZoneManager) { - return; - } - - // Increase range to linkify - if (this._rowsToLinkify.start === undefined || this._rowsToLinkify.end === undefined) { - this._rowsToLinkify.start = start; - this._rowsToLinkify.end = end; - } else { - this._rowsToLinkify.start = Math.min(this._rowsToLinkify.start, start); - this._rowsToLinkify.end = Math.max(this._rowsToLinkify.end, end); - } - - // Clear out any existing links on this row range - this._mouseZoneManager.clearAll(start, end); - - // Restart timer - if (this._rowsTimeoutId) { - clearTimeout(this._rowsTimeoutId); - } - - // Cannot use window.setTimeout since tests need to run in node - this._rowsTimeoutId = setTimeout(() => this._linkifyRows(), Linkifier._timeBeforeLatency) as any as number; - } - - /** - * Linkifies the rows requested. - */ - private _linkifyRows(): void { - this._rowsTimeoutId = undefined; - const buffer = this._bufferService.buffer; - - if (this._rowsToLinkify.start === undefined || this._rowsToLinkify.end === undefined) { - this._logService.debug('_rowToLinkify was unset before _linkifyRows was called'); - return; - } - - // Ensure the start row exists - const absoluteRowIndexStart = buffer.ydisp + this._rowsToLinkify.start; - if (absoluteRowIndexStart >= buffer.lines.length) { - return; - } - - // Invalidate bad end row values (if a resize happened) - const absoluteRowIndexEnd = buffer.ydisp + Math.min(this._rowsToLinkify.end, this._bufferService.rows) + 1; - - // Iterate over the range of unwrapped content strings within start..end - // (excluding). - // _doLinkifyRow gets full unwrapped lines with the start row as buffer offset - // for every matcher. - // The unwrapping is needed to also match content that got wrapped across - // several buffer lines. To avoid a worst case scenario where the whole buffer - // contains just a single unwrapped string we limit this line expansion beyond - // the viewport to +OVERSCAN_CHAR_LIMIT chars (overscan) at top and bottom. - // This comes with the tradeoff that matches longer than OVERSCAN_CHAR_LIMIT - // chars will not match anymore at the viewport borders. - const overscanLineLimit = Math.ceil(OVERSCAN_CHAR_LIMIT / this._bufferService.cols); - const iterator = this._bufferService.buffer.iterator( - false, absoluteRowIndexStart, absoluteRowIndexEnd, overscanLineLimit, overscanLineLimit); - while (iterator.hasNext()) { - const lineData: IBufferStringIteratorResult = iterator.next(); - for (let i = 0; i < this._linkMatchers.length; i++) { - this._doLinkifyRow(lineData.range.first, lineData.content, this._linkMatchers[i]); - } - } - - this._rowsToLinkify.start = undefined; - this._rowsToLinkify.end = undefined; - } - - /** - * Registers a link matcher, allowing custom link patterns to be matched and - * handled. - * @param regex The regular expression to search for. Specifically, this - * searches the textContent of the rows. You will want to use \s to match a - * space ' ' character for example. - * @param handler The callback when the link is called. - * @param options Options for the link matcher. - * @return The ID of the new matcher, this can be used to deregister. - */ - public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options: ILinkMatcherOptions = {}): number { - if (!handler) { - throw new Error('handler must be defined'); - } - const matcher: IRegisteredLinkMatcher = { - id: this._nextLinkMatcherId++, - regex, - handler, - matchIndex: options.matchIndex, - validationCallback: options.validationCallback, - hoverTooltipCallback: options.tooltipCallback, - hoverLeaveCallback: options.leaveCallback, - willLinkActivate: options.willLinkActivate, - priority: options.priority || 0 - }; - this._addLinkMatcherToList(matcher); - return matcher.id; - } - - /** - * Inserts a link matcher to the list in the correct position based on the - * priority of each link matcher. New link matchers of equal priority are - * considered after older link matchers. - * @param matcher The link matcher to be added. - */ - private _addLinkMatcherToList(matcher: IRegisteredLinkMatcher): void { - if (this._linkMatchers.length === 0) { - this._linkMatchers.push(matcher); - return; - } - - for (let i = this._linkMatchers.length - 1; i >= 0; i--) { - if (matcher.priority <= this._linkMatchers[i].priority) { - this._linkMatchers.splice(i + 1, 0, matcher); - return; - } - } - - this._linkMatchers.splice(0, 0, matcher); - } - - /** - * Deregisters a link matcher if it has been registered. - * @param matcherId The link matcher's ID (returned after register) - * @return Whether a link matcher was found and deregistered. - */ - public deregisterLinkMatcher(matcherId: number): boolean { - for (let i = 0; i < this._linkMatchers.length; i++) { - if (this._linkMatchers[i].id === matcherId) { - this._linkMatchers.splice(i, 1); - return true; - } - } - return false; - } - - /** - * Linkifies a row given a specific handler. - * @param rowIndex The row index to linkify (absolute index). - * @param text string content of the unwrapped row. - * @param matcher The link matcher for this line. - */ - private _doLinkifyRow(rowIndex: number, text: string, matcher: ILinkMatcher): void { - // clone regex to do a global search on text - const rex = new RegExp(matcher.regex.source, (matcher.regex.flags || '') + 'g'); - let match; - let stringIndex = -1; - while ((match = rex.exec(text)) !== null) { - const uri = match[typeof matcher.matchIndex !== 'number' ? 0 : matcher.matchIndex]; - if (!uri) { - // something matched but does not comply with the given matchIndex - // since this is most likely a bug the regex itself we simply do nothing here - this._logService.debug('match found without corresponding matchIndex', match, matcher); - break; - } - - // Get index, match.index is for the outer match which includes negated chars - // therefore we cannot use match.index directly, instead we search the position - // of the match group in text again - // also correct regex and string search offsets for the next loop run - stringIndex = text.indexOf(uri, stringIndex + 1); - rex.lastIndex = stringIndex + uri.length; - if (stringIndex < 0) { - // invalid stringIndex (should not have happened) - break; - } - - // get the buffer index as [absolute row, col] for the match - const bufferIndex = this._bufferService.buffer.stringIndexToBufferIndex(rowIndex, stringIndex); - if (bufferIndex[0] < 0) { - // invalid bufferIndex (should not have happened) - break; - } - - const line = this._bufferService.buffer.lines.get(bufferIndex[0]); - if (!line) { - break; - } - - const attr = line.getFg(bufferIndex[1]); - const fg = attr ? (attr >> 9) & 0x1ff : undefined; - - if (matcher.validationCallback) { - matcher.validationCallback(uri, isValid => { - // Discard link if the line has already changed - if (this._rowsTimeoutId) { - return; - } - if (isValid) { - this._addLink(bufferIndex[1], bufferIndex[0] - this._bufferService.buffer.ydisp, uri, matcher, fg); - } - }); - } else { - this._addLink(bufferIndex[1], bufferIndex[0] - this._bufferService.buffer.ydisp, uri, matcher, fg); - } - } - } - - /** - * Registers a link to the mouse zone manager. - * @param x The column the link starts. - * @param y The row the link is on. - * @param uri The URI of the link. - * @param matcher The link matcher for the link. - * @param fg The link color for hover event. - */ - private _addLink(x: number, y: number, uri: string, matcher: ILinkMatcher, fg: number | undefined): void { - if (!this._mouseZoneManager || !this._element) { - return; - } - // FIXME: get cell length from buffer to avoid mismatch after Unicode version change - const width = this._unicodeService.getStringCellWidth(uri); - const x1 = x % this._bufferService.cols; - const y1 = y + Math.floor(x / this._bufferService.cols); - let x2 = (x1 + width) % this._bufferService.cols; - let y2 = y1 + Math.floor((x1 + width) / this._bufferService.cols); - if (x2 === 0) { - x2 = this._bufferService.cols; - y2--; - } - - this._mouseZoneManager.add(new MouseZone( - x1 + 1, - y1 + 1, - x2 + 1, - y2 + 1, - e => { - if (matcher.handler) { - return matcher.handler(e, uri); - } - const newWindow = window.open(); - if (newWindow) { - newWindow.opener = null; - newWindow.location.href = uri; - } else { - console.warn('Opening link blocked as opener could not be cleared'); - } - }, - () => { - this._onShowLinkUnderline.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg)); - this._element!.classList.add('xterm-cursor-pointer'); - }, - e => { - this._onLinkTooltip.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg)); - if (matcher.hoverTooltipCallback) { - // Note that IViewportRange use 1-based coordinates to align with escape sequences such - // as CUP which use 1,1 as the default for row/col - matcher.hoverTooltipCallback(e, uri, { start: { x: x1, y: y1 }, end: { x: x2, y: y2 } }); - } - }, - () => { - this._onHideLinkUnderline.fire(this._createLinkHoverEvent(x1, y1, x2, y2, fg)); - this._element!.classList.remove('xterm-cursor-pointer'); - if (matcher.hoverLeaveCallback) { - matcher.hoverLeaveCallback(); - } - }, - e => { - if (matcher.willLinkActivate) { - return matcher.willLinkActivate(e, uri); - } - return true; - } - )); - } - - private _createLinkHoverEvent(x1: number, y1: number, x2: number, y2: number, fg: number | undefined): ILinkifierEvent { - return { x1, y1, x2, y2, cols: this._bufferService.cols, fg }; - } -} - -export class MouseZone implements IMouseZone { - constructor( - public x1: number, - public y1: number, - public x2: number, - public y2: number, - public clickCallback: (e: MouseEvent) => any, - public hoverCallback: (e: MouseEvent) => any, - public tooltipCallback: (e: MouseEvent) => any, - public leaveCallback: () => void, - public willLinkActivate: (e: MouseEvent) => boolean - ) { - } -} diff --git a/src/browser/MouseZoneManager.ts b/src/browser/MouseZoneManager.ts deleted file mode 100644 index 71ffe7c5e3..0000000000 --- a/src/browser/MouseZoneManager.ts +++ /dev/null @@ -1,236 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { Disposable } from 'common/Lifecycle'; -import { addDisposableDomListener } from 'browser/Lifecycle'; -import { IMouseService, ISelectionService } from 'browser/services/Services'; -import { IMouseZoneManager, IMouseZone } from 'browser/Types'; -import { IBufferService, IOptionsService } from 'common/services/Services'; - -/** - * The MouseZoneManager allows components to register zones within the terminal - * that trigger hover and click callbacks. - * - * This class was intentionally made not so robust initially as the only case it - * needed to support was single-line links which never overlap. Improvements can - * be made in the future. - */ -export class MouseZoneManager extends Disposable implements IMouseZoneManager { - private _zones: IMouseZone[] = []; - - private _areZonesActive: boolean = false; - private _mouseMoveListener: (e: MouseEvent) => any; - private _mouseLeaveListener: (e: MouseEvent) => any; - private _clickListener: (e: MouseEvent) => any; - - private _tooltipTimeout: number | undefined; - private _currentZone: IMouseZone | undefined; - private _lastHoverCoords: [number | undefined, number | undefined] = [undefined, undefined]; - private _initialSelectionLength: number = 0; - - constructor( - private readonly _element: HTMLElement, - private readonly _screenElement: HTMLElement, - @IBufferService private readonly _bufferService: IBufferService, - @IMouseService private readonly _mouseService: IMouseService, - @ISelectionService private readonly _selectionService: ISelectionService, - @IOptionsService private readonly _optionsService: IOptionsService - ) { - super(); - - this.register(addDisposableDomListener(this._element, 'mousedown', e => this._onMouseDown(e))); - - // These events are expensive, only listen to it when mouse zones are active - this._mouseMoveListener = e => this._onMouseMove(e); - this._mouseLeaveListener = e => this._onMouseLeave(e); - this._clickListener = e => this._onClick(e); - } - - public dispose(): void { - super.dispose(); - this._deactivate(); - } - - public add(zone: IMouseZone): void { - this._zones.push(zone); - if (this._zones.length === 1) { - this._activate(); - } - } - - public clearAll(start?: number, end?: number): void { - // Exit if there's nothing to clear - if (this._zones.length === 0) { - return; - } - - // Clear all if start/end weren't set - if (!start || !end) { - start = 0; - end = this._bufferService.rows - 1; - } - - // Iterate through zones and clear them out if they're within the range - for (let i = 0; i < this._zones.length; i++) { - const zone = this._zones[i]; - if ((zone.y1 > start && zone.y1 <= end + 1) || - (zone.y2 > start && zone.y2 <= end + 1) || - (zone.y1 < start && zone.y2 > end + 1)) { - if (this._currentZone && this._currentZone === zone) { - this._currentZone.leaveCallback(); - this._currentZone = undefined; - } - this._zones.splice(i--, 1); - } - } - - // Deactivate the mouse zone manager if all the zones have been removed - if (this._zones.length === 0) { - this._deactivate(); - } - } - - private _activate(): void { - if (!this._areZonesActive) { - this._areZonesActive = true; - this._element.addEventListener('mousemove', this._mouseMoveListener); - this._element.addEventListener('mouseleave', this._mouseLeaveListener); - this._element.addEventListener('click', this._clickListener); - } - } - - private _deactivate(): void { - if (this._areZonesActive) { - this._areZonesActive = false; - this._element.removeEventListener('mousemove', this._mouseMoveListener); - this._element.removeEventListener('mouseleave', this._mouseLeaveListener); - this._element.removeEventListener('click', this._clickListener); - } - } - - private _onMouseMove(e: MouseEvent): void { - // TODO: Ideally this would only clear the hover state when the mouse moves - // outside of the mouse zone - if (this._lastHoverCoords[0] !== e.pageX || this._lastHoverCoords[1] !== e.pageY) { - this._onHover(e); - // Record the current coordinates - this._lastHoverCoords = [e.pageX, e.pageY]; - } - } - - private _onHover(e: MouseEvent): void { - const zone = this._findZoneEventAt(e); - - // Do nothing if the zone is the same - if (zone === this._currentZone) { - return; - } - - // Fire the hover end callback and cancel any existing timer if a new zone - // is being hovered - if (this._currentZone) { - this._currentZone.leaveCallback(); - this._currentZone = undefined; - if (this._tooltipTimeout) { - clearTimeout(this._tooltipTimeout); - } - } - - // Exit if there is not zone - if (!zone) { - return; - } - this._currentZone = zone; - - // Trigger the hover callback - if (zone.hoverCallback) { - zone.hoverCallback(e); - } - - // Restart the tooltip timeout - this._tooltipTimeout = window.setTimeout(() => this._onTooltip(e), this._optionsService.rawOptions.linkTooltipHoverDuration); - } - - private _onTooltip(e: MouseEvent): void { - this._tooltipTimeout = undefined; - const zone = this._findZoneEventAt(e); - zone?.tooltipCallback(e); - } - - private _onMouseDown(e: MouseEvent): void { - // Store current terminal selection length, to check if we're performing - // a selection operation - this._initialSelectionLength = this._getSelectionLength(); - - // Ignore the event if there are no zones active - if (!this._areZonesActive) { - return; - } - - // Find the active zone, prevent event propagation if found to prevent other - // components from handling the mouse event. - const zone = this._findZoneEventAt(e); - if (zone?.willLinkActivate(e)) { - e.preventDefault(); - e.stopImmediatePropagation(); - } - } - - private _onMouseLeave(e: MouseEvent): void { - // Fire the hover end callback and cancel any existing timer if the mouse - // leaves the terminal element - if (this._currentZone) { - this._currentZone.leaveCallback(); - this._currentZone = undefined; - if (this._tooltipTimeout) { - clearTimeout(this._tooltipTimeout); - } - } - } - - private _onClick(e: MouseEvent): void { - // Find the active zone and click it if found and no selection was - // being performed - const zone = this._findZoneEventAt(e); - const currentSelectionLength = this._getSelectionLength(); - - if (zone && currentSelectionLength === this._initialSelectionLength) { - zone.clickCallback(e); - e.preventDefault(); - e.stopImmediatePropagation(); - } - } - - private _getSelectionLength(): number { - const selectionText = this._selectionService.selectionText; - return selectionText ? selectionText.length : 0; - } - - private _findZoneEventAt(e: MouseEvent): IMouseZone | undefined { - const coords = this._mouseService.getCoords(e, this._screenElement, this._bufferService.cols, this._bufferService.rows); - if (!coords) { - return undefined; - } - const x = coords[0]; - const y = coords[1]; - for (let i = 0; i < this._zones.length; i++) { - const zone = this._zones[i]; - if (zone.y1 === zone.y2) { - // Single line link - if (y === zone.y1 && x >= zone.x1 && x < zone.x2) { - return zone; - } - } else { - // Multi-line link - if ((y === zone.y1 && x >= zone.x1) || - (y === zone.y2 && x < zone.x2) || - (y > zone.y1 && y < zone.y2)) { - return zone; - } - } - } - return undefined; - } -} diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts index 3eafb26150..eaf420eda0 100644 --- a/src/browser/Terminal.test.ts +++ b/src/browser/Terminal.test.ts @@ -7,11 +7,8 @@ import { assert } from 'chai'; import { MockViewport, MockCompositionHelper, MockRenderer, TestTerminal } from 'browser/TestUtils.test'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { CellData } from 'common/buffer/CellData'; -import { IBufferService, IUnicodeService } from 'common/services/Services'; -import { Linkifier } from 'browser/Linkifier'; -import { MockLogService, MockUnicodeService } from 'common/TestUtils.test'; -import { IRegisteredLinkMatcher, IMouseZoneManager, IMouseZone } from 'browser/Types'; -import { IMarker, ITerminalOptions } from 'common/Types'; +import { MockUnicodeService } from 'common/TestUtils.test'; +import { IMarker } from 'common/Types'; const INIT_COLS = 80; const INIT_ROWS = 24; @@ -472,7 +469,7 @@ describe('Terminal', () => { describe('when scrollback === 0', () => { beforeEach(() => { - term.optionsService.setOption('scrollback', 0); + term.optionsService.options.scrollback = 0; assert.equal(term.buffer.lines.maxLength, INIT_ROWS); }); @@ -1044,105 +1041,6 @@ describe('Terminal', () => { }); }); - describe('Linkifier unicode handling', () => { - let terminal: TestTerminal; - let linkifier: TestLinkifier; - let mouseZoneManager: TestMouseZoneManager; - - // other than the tests above unicode testing needs the full terminal instance - // to get the special handling of fullwidth, surrogate and combining chars in the input handler - beforeEach(() => { - terminal = new TestTerminal({ cols: 10, rows: 5 }); - linkifier = new TestLinkifier((terminal as any)._bufferService, terminal.unicodeService); - mouseZoneManager = new TestMouseZoneManager(); - linkifier.attachToDom({} as any, mouseZoneManager); - }); - - function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: { x1: number, y1: number, x2: number, y2: number }[]): Promise { - return new Promise(async r => { - await terminal.writeP(rowText); - linkifier.registerLinkMatcher(linkMatcherRegex, () => { }); - linkifier.linkifyRows(); - // Allow linkify to happen - setTimeout(() => { - assert.equal(mouseZoneManager.zones.length, links.length); - links.forEach((l, i) => { - assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1); - assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1); - assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1); - assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1); - }); - r(); - }, 0); - }); - } - - describe('unicode before the match', () => { - it('combining - match within one line', () => { - return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]); - }); - it('combining - match over two lines', () => { - return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); - }); - it('surrogate - match within one line', () => { - return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]); - }); - it('surrogate - match over two lines', () => { - return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); - }); - it('combining surrogate - match within one line', () => { - return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]); - }); - it('combining surrogate - match over two lines', () => { - return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); - }); - it('fullwidth - match within one line', () => { - return assertLinkifiesInTerminal('12 foo', /foo/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); - }); - it('fullwidth - match over two lines', () => { - return assertLinkifiesInTerminal('12 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); - }); - it('combining fullwidth - match within one line', () => { - return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); - }); - it('combining fullwidth - match over two lines', () => { - return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); - }); - }); - describe('unicode within the match', () => { - it('combining - match within one line', () => { - return assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]); - }); - it('combining - match over two lines', () => { - return assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]); - }); - it('surrogate - match within one line', () => { - return assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); - }); - it('surrogate - match over two lines', () => { - return assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{ x1: 9, x2: 2, y1: 0, y2: 1 }]); - }); - it('combining surrogate - match within one line', () => { - return assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); - }); - it('combining surrogate - match over two lines', () => { - return assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{ x1: 9, x2: 2, y1: 0, y2: 1 }]); - }); - it('fullwidth - match within one line', () => { - return assertLinkifiesInTerminal('test a1b', /a1b/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]); - }); - it('fullwidth - match over two lines', () => { - return assertLinkifiesInTerminal('testtest a1b', /a1b/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]); - }); - it('combining fullwidth - match within one line', () => { - return assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]); - }); - it('combining fullwidth - match over two lines', () => { - return assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]); - }); - }); - }); - describe('Buffer.stringIndexToBufferIndex', () => { let terminal: TestTerminal; @@ -1448,7 +1346,7 @@ describe('Terminal', () => { term = new TestTerminal({}); markers = []; disposeStack = []; - term.optionsService.setOption('scrollback', 1); + term.optionsService.options.scrollback = 1; term.resize(10, 5); markers.push(term.buffers.active.addMarker(term.buffers.active.y)); await term.writeP('\x1b[r0\r\n'); @@ -1518,26 +1416,3 @@ describe('Terminal', () => { }); }); }); - -class TestLinkifier extends Linkifier { - constructor(bufferService: IBufferService, unicodeService: IUnicodeService) { - super(bufferService, new MockLogService(), unicodeService); - Linkifier._timeBeforeLatency = 0; - } - - public get linkMatchers(): IRegisteredLinkMatcher[] { return this._linkMatchers; } - public linkifyRows(): void { super.linkifyRows(0, this._bufferService.buffer.lines.length - 1); } -} - -class TestMouseZoneManager implements IMouseZoneManager { - public dispose(): void { - } - public clears: number = 0; - public zones: IMouseZone[] = []; - public add(zone: IMouseZone): void { - this.zones.push(zone); - } - public clearAll(): void { - this.clears++; - } -} diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index bd82325d19..ac6f887e54 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -21,23 +21,19 @@ * http://linux.die.net/man/7/urxvt */ -import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, ILinkifier, IMouseZoneManager, LinkMatcherHandler, ILinkMatcherOptions, IViewport, ILinkifier2, CharacterJoinerHandler } from 'browser/Types'; +import { ICompositionHelper, ITerminal, IBrowser, CustomKeyEventHandler, IViewport, ILinkifier2, CharacterJoinerHandler, IBufferRange } from 'browser/Types'; import { IRenderer } from 'browser/renderer/Types'; import { CompositionHelper } from 'browser/input/CompositionHelper'; import { Viewport } from 'browser/Viewport'; import { rightClickHandler, moveTextAreaUnderMouseCursor, handlePasteEvent, copyHandler, paste } from 'browser/Clipboard'; import { C0, C1_ESCAPED } from 'common/data/EscapeSequences'; import { WindowsOptionsReportType } from '../common/InputHandler'; -import { Renderer } from 'browser/renderer/Renderer'; -import { Linkifier } from 'browser/Linkifier'; import { SelectionService } from 'browser/services/SelectionService'; import * as Browser from 'common/Platform'; import { addDisposableDomListener } from 'browser/Lifecycle'; import * as Strings from 'browser/LocalizableStrings'; -import { SoundService } from 'browser/services/SoundService'; -import { MouseZoneManager } from 'browser/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; -import { ITheme, IMarker, IDisposable, ISelectionPosition, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm'; +import { ITheme, IMarker, IDisposable, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm'; import { DomRenderer } from 'browser/renderer/dom/DomRenderer'; import { KeyboardResultType, CoreMouseEventType, CoreMouseButton, CoreMouseAction, ITerminalOptions, ScrollSource, IColorEvent, ColorIndex, ColorRequestType } from 'common/Types'; import { evaluateKeyboardEvent } from 'common/input/Keyboard'; @@ -45,7 +41,7 @@ import { EventEmitter, IEvent, forwardEvent } from 'common/EventEmitter'; import { DEFAULT_ATTR_DATA } from 'common/buffer/BufferLine'; import { ColorManager } from 'browser/ColorManager'; import { RenderService } from 'browser/services/RenderService'; -import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ISoundService, ICoreBrowserService, ICharacterJoinerService } from 'browser/services/Services'; +import { ICharSizeService, IRenderService, IMouseService, ISelectionService, ICoreBrowserService, ICharacterJoinerService } from 'browser/services/Services'; import { CharSizeService } from 'browser/services/CharSizeService'; import { IBuffer } from 'common/buffer/Types'; import { MouseService } from 'browser/services/MouseService'; @@ -89,7 +85,6 @@ export class Terminal extends CoreTerminal implements ITerminal { private _renderService: IRenderService | undefined; private _characterJoinerService: ICharacterJoinerService | undefined; private _selectionService: ISelectionService | undefined; - private _soundService: ISoundService | undefined; /** * Records whether the keydown event has already been handled and triggered a data event, if so @@ -118,11 +113,9 @@ export class Terminal extends CoreTerminal implements ITerminal { */ private _unprocessedDeadKey: boolean = false; - public linkifier: ILinkifier; public linkifier2: ILinkifier2; public viewport: IViewport | undefined; private _compositionHelper: ICompositionHelper | undefined; - private _mouseZoneManager: IMouseZoneManager | undefined; private _accessibilityManager: AccessibilityManager | undefined; private _colorManager: ColorManager | undefined; private _theme: ITheme | undefined; @@ -168,13 +161,12 @@ export class Terminal extends CoreTerminal implements ITerminal { this._setup(); - this.linkifier = this._instantiationService.createInstance(Linkifier); this.linkifier2 = this.register(this._instantiationService.createInstance(Linkifier2)); this._decorationService = this._instantiationService.createInstance(DecorationService); this._instantiationService.setService(IDecorationService, this._decorationService); // Setup InputHandler listeners - this.register(this._inputHandler.onRequestBell(() => this.bell())); + this.register(this._inputHandler.onRequestBell(() => this._onBell.fire())); this.register(this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end))); this.register(this._inputHandler.onRequestSendFocus(() => this._reportFocus())); this.register(this._inputHandler.onRequestReset(() => this.reset())); @@ -303,12 +295,6 @@ export class Terminal extends CoreTerminal implements ITerminal { this.refresh(0, this.rows - 1); } break; - case 'rendererType': - if (this._renderService) { - this._renderService.setRenderer(this._createRenderer()); - this._renderService.onResize(this.cols, this.rows); - } - break; case 'scrollback': this.viewport?.syncScrollArea(); break; @@ -450,7 +436,6 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(addDisposableDomListener(this.textarea!, 'compositionend', () => this._compositionHelper!.compositionend())); this.register(addDisposableDomListener(this.textarea!, 'input', (ev: InputEvent) => this._inputEvent(ev), true)); this.register(this.onRender(() => this._compositionHelper!.updateCompositionElements())); - this.register(this.onRender(e => this._queueLinkification(e.start, e.end))); } /** @@ -537,8 +522,6 @@ export class Terminal extends CoreTerminal implements ITerminal { // Performance: Add viewport and helper elements from the fragment this.element.appendChild(fragment); - this._soundService = this._instantiationService.createInstance(SoundService); - this._instantiationService.setService(ISoundService, this._soundService); this._mouseService = this._instantiationService.createInstance(MouseService); this._instantiationService.setService(IMouseService, this._mouseService); @@ -584,13 +567,8 @@ export class Terminal extends CoreTerminal implements ITerminal { })); this.register(addDisposableDomListener(this._viewportElement, 'scroll', () => this._selectionService!.refresh())); - this._mouseZoneManager = this._instantiationService.createInstance(MouseZoneManager, this.element, this.screenElement); - this.register(this._mouseZoneManager); - this.register(this.onScroll(() => this._mouseZoneManager!.clearAll())); - this.linkifier.attachToDom(this.element, this._mouseZoneManager); this.linkifier2.attachToDom(this.screenElement, this._mouseService, this._renderService); this.register(this._instantiationService.createInstance(BufferDecorationRenderer, this.screenElement)); - // This event listener must be registered aftre MouseZoneManager is created this.register(addDisposableDomListener(this.element, 'mousedown', (e: MouseEvent) => this._selectionService!.onMouseDown(e))); // apply mouse event classes set by escape codes before terminal was attached @@ -630,11 +608,7 @@ export class Terminal extends CoreTerminal implements ITerminal { } private _createRenderer(): IRenderer { - switch (this.options.rendererType) { - case 'canvas': return this._instantiationService.createInstance(Renderer, this._colorManager!.colors, this.screenElement!, this.linkifier, this.linkifier2); - case 'dom': return this._instantiationService.createInstance(DomRenderer, this._colorManager!.colors, this.element!, this.screenElement!, this._viewportElement!, this.linkifier, this.linkifier2); - default: throw new Error(`Unrecognized rendererType "${this.options.rendererType}"`); - } + return this._instantiationService.createInstance(DomRenderer, this._colorManager!.colors, this.element!, this.screenElement!, this._viewportElement!, this.linkifier2); } /** @@ -912,15 +886,6 @@ export class Terminal extends CoreTerminal implements ITerminal { this._renderService?.refreshRows(start, end); } - /** - * Queues linkification for the specified rows. - * @param start The row to start from (between 0 and this.rows - 1). - * @param end The row to end at (between start and this.rows - 1). - */ - private _queueLinkification(start: number, end: number): void { - this.linkifier?.linkifyRows(start, end); - } - /** * Change the cursor style for different selection modes */ @@ -964,32 +929,6 @@ export class Terminal extends CoreTerminal implements ITerminal { this._customKeyEventHandler = customKeyEventHandler; } - /** - * Registers a link matcher, allowing custom link patterns to be matched and - * handled. - * @param regex The regular expression to search for, specifically - * this searches the textContent of the rows. You will want to use \s to match - * a space ' ' character for example. - * @param handler The callback when the link is called. - * @param options Options for the link matcher. - * @return The ID of the new matcher, this can be used to deregister. - */ - public registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number { - const matcherId = this.linkifier.registerLinkMatcher(regex, handler, options); - this.refresh(0, this.rows - 1); - return matcherId; - } - - /** - * Deregisters a link matcher if it has been registered. - * @param matcherId The link matcher's ID (returned after register) - */ - public deregisterLinkMatcher(matcherId: number): void { - if (this.linkifier.deregisterLinkMatcher(matcherId)) { - this.refresh(0, this.rows - 1); - } - } - public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { return this.linkifier2.registerLinkProvider(linkProvider); } @@ -1054,16 +993,20 @@ export class Terminal extends CoreTerminal implements ITerminal { return this._selectionService ? this._selectionService.selectionText : ''; } - public getSelectionPosition(): ISelectionPosition | undefined { + public getSelectionPosition(): IBufferRange | undefined { if (!this._selectionService || !this._selectionService.hasSelection) { return undefined; } return { - startColumn: this._selectionService.selectionStart![0], - startRow: this._selectionService.selectionStart![1], - endColumn: this._selectionService.selectionEnd![0], - endRow: this._selectionService.selectionEnd![1] + start: { + x: this._selectionService.selectionStart![0], + y: this._selectionService.selectionStart![1] + }, + end: { + x: this._selectionService.selectionEnd![0], + y: this._selectionService.selectionEnd![1] + } }; } @@ -1285,26 +1228,6 @@ export class Terminal extends CoreTerminal implements ITerminal { return false; } - /** - * Ring the bell. - * Note: We could do sweet things with webaudio here - */ - public bell(): void { - if (this._soundBell()) { - this._soundService?.playBellSound(); - } - - this._onBell.fire(); - - // if (this._visualBell()) { - // this.element.classList.add('visual-bell-active'); - // clearTimeout(this._visualBellTimer); - // this._visualBellTimer = window.setTimeout(() => { - // this.element.classList.remove('visual-bell-active'); - // }, 200); - // } - } - /** * Resizes the terminal. * @@ -1422,18 +1345,6 @@ export class Terminal extends CoreTerminal implements ITerminal { ev.stopPropagation(); return false; } - - private _visualBell(): boolean { - return false; - // return this.options.bellStyle === 'visual' || - // this.options.bellStyle === 'both'; - } - - private _soundBell(): boolean { - return this.options.bellStyle === 'sound'; - // return this.options.bellStyle === 'sound' || - // this.options.bellStyle === 'both'; - } } /** diff --git a/src/browser/TestUtils.test.ts b/src/browser/TestUtils.test.ts index 92f90a1d4e..7c509160d4 100644 --- a/src/browser/TestUtils.test.ts +++ b/src/browser/TestUtils.test.ts @@ -3,11 +3,11 @@ * @license MIT */ -import { IDisposable, IMarker, ISelectionPosition, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm'; +import { IDisposable, IMarker, ILinkProvider, IDecorationOptions, IDecoration } from 'xterm'; import { IEvent, EventEmitter } from 'common/EventEmitter'; import { ICharacterJoinerService, ICharSizeService, IMouseService, IRenderService, ISelectionService } from 'browser/services/Services'; import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/renderer/Types'; -import { IColorSet, ILinkMatcherOptions, ITerminal, ILinkifier, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IRenderDebouncer } from 'browser/Types'; +import { IColorSet, ITerminal, ILinkifier2, IBrowser, IViewport, IColorManager, ICompositionHelper, CharacterJoinerHandler, IRenderDebouncer, IBufferRange } from 'browser/Types'; import { IBuffer, IBufferStringIterator, IBufferSet } from 'common/buffer/Types'; import { IBufferLine, ICellData, IAttributeData, ICircularList, XtermListener, ICharset, ITerminalOptions } from 'common/Types'; import { Buffer } from 'common/buffer/Buffer'; @@ -95,12 +95,6 @@ export class MockTerminal implements ITerminal { public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable { throw new Error('Method not implemented.'); } - public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number { - throw new Error('Method not implemented.'); - } - public deregisterLinkMatcher(matcherId: number): void { - throw new Error('Method not implemented.'); - } public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { throw new Error('Method not implemented.'); } @@ -113,7 +107,7 @@ export class MockTerminal implements ITerminal { public getSelection(): string { throw new Error('Method not implemented.'); } - public getSelectionPosition(): ISelectionPosition | undefined { + public getSelectionPosition(): IBufferRange | undefined { throw new Error('Method not implemented.'); } public clearSelection(): void { @@ -143,12 +137,8 @@ export class MockTerminal implements ITerminal { public write(data: string): void { throw new Error('Method not implemented.'); } - public writeUtf8(data: Uint8Array): void { - throw new Error('Method not implemented.'); - } public bracketedPasteMode!: boolean; public renderer!: IRenderer; - public linkifier!: ILinkifier; public linkifier2!: ILinkifier2; public isFocused!: boolean; public options: ITerminalOptions = {}; diff --git a/src/browser/Types.d.ts b/src/browser/Types.d.ts index 41992a8b4b..f701120099 100644 --- a/src/browser/Types.d.ts +++ b/src/browser/Types.d.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { IDecorationOptions, IDecoration, IDisposable, IMarker, ISelectionPosition } from 'xterm'; +import { IDecorationOptions, IDecoration, IDisposable, IMarker } from 'xterm'; import { IEvent } from 'common/EventEmitter'; import { ICoreTerminal, CharData, ITerminalOptions, IColor } from 'common/Types'; import { IMouseService, IRenderService } from './services/Services'; @@ -17,7 +17,6 @@ export interface ITerminal extends IPublicTerminal, ICoreTerminal { buffer: IBuffer; viewport: IViewport | undefined; options: ITerminalOptions; - linkifier: ILinkifier; linkifier2: ILinkifier2; onBlur: IEvent; @@ -56,8 +55,6 @@ export interface IPublicTerminal extends IDisposable { registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise): IDisposable; registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise): IDisposable; registerOscHandler(ident: number, callback: (data: string) => boolean | Promise): IDisposable; - registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; - deregisterLinkMatcher(matcherId: number): void; registerLinkProvider(linkProvider: ILinkProvider): IDisposable; registerCharacterJoiner(handler: (text: string) => [number, number][]): number; deregisterCharacterJoiner(joinerId: number): void; @@ -65,7 +62,7 @@ export interface IPublicTerminal extends IDisposable { registerDecoration(decorationOptions: IDecorationOptions): IDecoration | undefined; hasSelection(): boolean; getSelection(): string; - getSelectionPosition(): ISelectionPosition | undefined; + getSelectionPosition(): IBufferRange | undefined; clearSelection(): void; select(column: number, row: number, length: number): void; selectAll(): void; @@ -153,36 +150,6 @@ export interface IViewport extends IDisposable { onThemeChange(colors: IColorSet): void; } -export interface IViewportRange { - start: IViewportRangePosition; - end: IViewportRangePosition; -} - -export interface IViewportRangePosition { - x: number; - y: number; -} - -export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void; -export type LinkMatcherHoverTooltipCallback = (event: MouseEvent, uri: string, position: IViewportRange) => void; -export type LinkMatcherValidationCallback = (uri: string, callback: (isValid: boolean) => void) => void; - -export interface ILinkMatcher { - id: number; - regex: RegExp; - handler: LinkMatcherHandler; - hoverTooltipCallback?: LinkMatcherHoverTooltipCallback; - hoverLeaveCallback?: () => void; - matchIndex?: number; - validationCallback?: LinkMatcherValidationCallback; - priority?: number; - willLinkActivate?: (event: MouseEvent, uri: string) => boolean; -} - -export interface IRegisteredLinkMatcher extends ILinkMatcher { - priority: number; -} - export interface ILinkifierEvent { x1: number; y1: number; @@ -192,17 +159,6 @@ export interface ILinkifierEvent { fg: number | undefined; } -export interface ILinkifier { - onShowLinkUnderline: IEvent; - onHideLinkUnderline: IEvent; - onLinkTooltip: IEvent; - - attachToDom(element: HTMLElement, mouseZoneManager: IMouseZoneManager): void; - linkifyRows(start: number, end: number): void; - registerLinkMatcher(regex: RegExp, handler: LinkMatcherHandler, options?: ILinkMatcherOptions): number; - deregisterLinkMatcher(matcherId: number): boolean; -} - interface ILinkState { decorations: ILinkDecorations; isHovered: boolean; @@ -221,57 +177,6 @@ export interface ILinkifier2 { registerLinkProvider(linkProvider: ILinkProvider): IDisposable; } -export interface ILinkMatcherOptions { - /** - * The index of the link from the regex.match(text) call. This defaults to 0 - * (for regular expressions without capture groups). - */ - matchIndex?: number; - /** - * A callback that validates an individual link, returning true if valid and - * false if invalid. - */ - validationCallback?: LinkMatcherValidationCallback; - /** - * A callback that fires when the mouse hovers over a link. - */ - tooltipCallback?: LinkMatcherHoverTooltipCallback; - /** - * A callback that fires when the mouse leaves a link that was hovered. - */ - leaveCallback?: () => void; - /** - * The priority of the link matcher, this defines the order in which the link - * matcher is evaluated relative to others, from highest to lowest. The - * default value is 0. - */ - priority?: number; - /** - * A callback that fires when the mousedown and click events occur that - * determines whether a link will be activated upon click. This enables - * only activating a link when a certain modifier is held down, if not the - * mouse event will continue propagation (eg. double click to select word). - */ - willLinkActivate?: (event: MouseEvent, uri: string) => boolean; -} - -export interface IMouseZoneManager extends IDisposable { - add(zone: IMouseZone): void; - clearAll(start?: number, end?: number): void; -} - -export interface IMouseZone { - x1: number; - x2: number; - y1: number; - y2: number; - clickCallback: (e: MouseEvent) => any; - hoverCallback: (e: MouseEvent) => any | undefined; - tooltipCallback: (e: MouseEvent) => any | undefined; - leaveCallback: () => any | undefined; - willLinkActivate: (e: MouseEvent) => boolean; -} - interface ILinkProvider { provideLinks(y: number, callback: (links: ILink[] | undefined) => void): void; } diff --git a/src/browser/public/Terminal.ts b/src/browser/public/Terminal.ts index 187bd3b51b..57efdbd025 100644 --- a/src/browser/public/Terminal.ts +++ b/src/browser/public/Terminal.ts @@ -3,8 +3,8 @@ * @license MIT */ -import { Terminal as ITerminalApi, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight, IModes, IDecorationOptions, IDecoration } from 'xterm'; -import { ITerminal } from 'browser/Types'; +import { Terminal as ITerminalApi, IMarker, IDisposable, ILocalizableStrings, ITerminalAddon, IBufferNamespace as IBufferNamespaceApi, IParser, ILinkProvider, IUnicodeHandling, FontWeight, IModes, IDecorationOptions, IDecoration } from 'xterm'; +import { IBufferRange, ITerminal } from 'browser/Types'; import { Terminal as TerminalCore } from 'browser/Terminal'; import * as Strings from 'browser/LocalizableStrings'; import { IEvent } from 'common/EventEmitter'; @@ -147,14 +147,6 @@ export class Terminal implements ITerminalApi { public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void { this._core.attachCustomKeyEventHandler(customKeyEventHandler); } - public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number { - this._checkProposedApi(); - return this._core.registerLinkMatcher(regex, handler, options); - } - public deregisterLinkMatcher(matcherId: number): void { - this._checkProposedApi(); - this._core.deregisterLinkMatcher(matcherId); - } public registerLinkProvider(linkProvider: ILinkProvider): IDisposable { this._checkProposedApi(); return this._core.registerLinkProvider(linkProvider); @@ -168,7 +160,6 @@ export class Terminal implements ITerminalApi { this._core.deregisterCharacterJoiner(joinerId); } public registerMarker(cursorYOffset: number = 0): IMarker | undefined { - this._checkProposedApi(); this._verifyIntegers(cursorYOffset); return this._core.addMarker(cursorYOffset); } @@ -177,9 +168,6 @@ export class Terminal implements ITerminalApi { this._verifyPositiveIntegers(decorationOptions.x ?? 0, decorationOptions.width ?? 0, decorationOptions.height ?? 0); return this._core.registerDecoration(decorationOptions); } - public addMarker(cursorYOffset: number): IMarker | undefined { - return this.registerMarker(cursorYOffset); - } public hasSelection(): boolean { return this._core.hasSelection(); } @@ -190,7 +178,7 @@ export class Terminal implements ITerminalApi { public getSelection(): string { return this._core.getSelection(); } - public getSelectionPosition(): ISelectionPosition | undefined { + public getSelectionPosition(): IBufferRange | undefined { return this._core.getSelectionPosition(); } public clearSelection(): void { @@ -231,9 +219,6 @@ export class Terminal implements ITerminalApi { public write(data: string | Uint8Array, callback?: () => void): void { this._core.write(data, callback); } - public writeUtf8(data: Uint8Array, callback?: () => void): void { - this._core.write(data, callback); - } public writeln(data: string | Uint8Array, callback?: () => void): void { this._core.write(data); this._core.write('\r\n', callback); @@ -241,28 +226,6 @@ export class Terminal implements ITerminalApi { public paste(data: string): void { this._core.paste(data); } - public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string; - public getOption(key: 'allowTransparency' | 'altClickMovesCursor' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell'): boolean; - public getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; - public getOption(key: 'fontWeight' | 'fontWeightBold'): FontWeight; - public getOption(key: string): any; - public getOption(key: any): any { - return this._core.optionsService.getOption(key); - } - public setOption(key: 'bellSound' | 'fontFamily' | 'termName' | 'wordSeparator', value: string): void; - public setOption(key: 'fontWeight' | 'fontWeightBold', value: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number): void; - public setOption(key: 'logLevel', value: 'debug' | 'info' | 'warn' | 'error' | 'off'): void; - public setOption(key: 'bellStyle', value: 'none' | 'visual' | 'sound' | 'both'): void; - public setOption(key: 'cursorStyle', value: 'block' | 'underline' | 'bar'): void; - public setOption(key: 'allowTransparency' | 'altClickMovesCursor' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell', value: boolean): void; - public setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void; - public setOption(key: 'theme', value: ITheme): void; - public setOption(key: 'cols' | 'rows', value: number): void; - public setOption(key: string, value: any): void; - public setOption(key: any, value: any): void { - this._checkReadonlyOptions(key); - this._core.optionsService.setOption(key, value); - } public refresh(start: number, end: number): void { this._verifyIntegers(start, end); this._core.refresh(start, end); diff --git a/src/browser/renderer/atlas/Constants.ts b/src/browser/renderer/Constants.ts similarity index 93% rename from src/browser/renderer/atlas/Constants.ts rename to src/browser/renderer/Constants.ts index c1701e97e5..ac698b85bc 100644 --- a/src/browser/renderer/atlas/Constants.ts +++ b/src/browser/renderer/Constants.ts @@ -6,10 +6,9 @@ import { isFirefox, isLegacyEdge } from 'common/Platform'; export const INVERTED_DEFAULT_COLOR = 257; + export const DIM_OPACITY = 0.5; // The text baseline is set conditionally by browser. Using 'ideographic' for Firefox or Legacy Edge would // result in truncated text (Issue 3353). Using 'bottom' for Chrome would result in slightly // unaligned Powerline fonts (PR 3356#issuecomment-850928179). export const TEXT_BASELINE: CanvasTextBaseline = isFirefox || isLegacyEdge ? 'bottom' : 'ideographic'; - -export const CHAR_ATLAS_CELL_SPACING = 1; diff --git a/src/browser/renderer/Types.d.ts b/src/browser/renderer/Types.d.ts index 6818a92673..cb1a85b46a 100644 --- a/src/browser/renderer/Types.d.ts +++ b/src/browser/renderer/Types.d.ts @@ -54,56 +54,3 @@ export interface IRenderer extends IDisposable { renderRows(start: number, end: number): void; clearTextureAtlas?(): void; } - -export interface IRenderLayer extends IDisposable { - /** - * Called when the terminal loses focus. - */ - onBlur(): void; - - /** - * * Called when the terminal gets focus. - */ - onFocus(): void; - - /** - * Called when the cursor is moved. - */ - onCursorMove(): void; - - /** - * Called when options change. - */ - onOptionsChanged(): void; - - /** - * Called when the theme changes. - */ - setColors(colorSet: IColorSet): void; - - /** - * Called when the data in the grid has changed (or needs to be rendered - * again). - */ - onGridChanged(startRow: number, endRow: number): void; - - /** - * Calls when the selection changes. - */ - onSelectionChanged(start: [number, number] | undefined, end: [number, number] | undefined, columnSelectMode: boolean): void; - - /** - * Resize the render layer. - */ - resize(dim: IRenderDimensions): void; - - /** - * Clear the state of the render layer. - */ - reset(): void; - - /** - * Clears the texture atlas. - */ - clearTextureAtlas(): void; -} diff --git a/src/browser/renderer/dom/DomRenderer.ts b/src/browser/renderer/dom/DomRenderer.ts index 248f37ac4b..991938e499 100644 --- a/src/browser/renderer/dom/DomRenderer.ts +++ b/src/browser/renderer/dom/DomRenderer.ts @@ -5,9 +5,9 @@ import { IRenderer, IRenderDimensions, IRequestRedrawEvent } from 'browser/renderer/Types'; import { BOLD_CLASS, ITALIC_CLASS, CURSOR_CLASS, CURSOR_STYLE_BLOCK_CLASS, CURSOR_BLINK_CLASS, CURSOR_STYLE_BAR_CLASS, CURSOR_STYLE_UNDERLINE_CLASS, DomRendererRowFactory } from 'browser/renderer/dom/DomRendererRowFactory'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; import { Disposable } from 'common/Lifecycle'; -import { IColorSet, ILinkifierEvent, ILinkifier, ILinkifier2 } from 'browser/Types'; +import { IColorSet, ILinkifierEvent, ILinkifier2 } from 'browser/Types'; import { ICharSizeService } from 'browser/services/Services'; import { IOptionsService, IBufferService, IInstantiationService, IDecorationService } from 'common/services/Services'; import { EventEmitter, IEvent } from 'common/EventEmitter'; @@ -47,7 +47,6 @@ export class DomRenderer extends Disposable implements IRenderer { private readonly _element: HTMLElement, private readonly _screenElement: HTMLElement, private readonly _viewportElement: HTMLElement, - private readonly _linkifier: ILinkifier, private readonly _linkifier2: ILinkifier2, @IInstantiationService instantiationService: IInstantiationService, @ICharSizeService private readonly _charSizeService: ICharSizeService, @@ -87,9 +86,6 @@ export class DomRenderer extends Disposable implements IRenderer { this._screenElement.appendChild(this._rowContainer); this._screenElement.appendChild(this._selectionContainer); - this.register(this._linkifier.onShowLinkUnderline(e => this._onLinkHover(e))); - this.register(this._linkifier.onHideLinkUnderline(e => this._onLinkLeave(e))); - this.register(this._linkifier2.onShowLinkUnderline(e => this._onLinkHover(e))); this.register(this._linkifier2.onHideLinkUnderline(e => this._onLinkLeave(e))); } diff --git a/src/browser/renderer/dom/DomRendererRowFactory.ts b/src/browser/renderer/dom/DomRendererRowFactory.ts index fadf50325f..35b8b0e7ef 100644 --- a/src/browser/renderer/dom/DomRendererRowFactory.ts +++ b/src/browser/renderer/dom/DomRendererRowFactory.ts @@ -4,13 +4,13 @@ */ import { IBufferLine, ICellData, IColor } from 'common/Types'; -import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/atlas/Constants'; +import { INVERTED_DEFAULT_COLOR } from 'browser/renderer/Constants'; import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR, Attributes } from 'common/buffer/Constants'; import { CellData } from 'common/buffer/CellData'; -import { IBufferService, ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; +import { ICoreService, IDecorationService, IOptionsService } from 'common/services/Services'; import { color, rgba } from 'common/Color'; import { IColorSet } from 'browser/Types'; -import { ICharacterJoinerService, ISelectionService } from 'browser/services/Services'; +import { ICharacterJoinerService } from 'browser/services/Services'; import { JoinedCellData } from 'browser/services/CharacterJoinerService'; import { excludeFromContrastRatioDemands } from 'browser/renderer/RendererUtils'; diff --git a/src/browser/services/SelectionService.ts b/src/browser/services/SelectionService.ts index 6b5c142563..aad2c4664a 100644 --- a/src/browser/services/SelectionService.ts +++ b/src/browser/services/SelectionService.ts @@ -694,7 +694,7 @@ export class SelectionService extends Disposable implements ISelectionService { this._removeMouseDownListeners(); - if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME && event.altKey && this._optionsService.getOption('altClickMovesCursor')) { + if (this.selectionText.length <= 1 && timeElapsed < ALT_CLICK_MOVE_CURSOR_TIME && event.altKey && this._optionsService.rawOptions.altClickMovesCursor) { if (this._bufferService.buffer.ybase === this._bufferService.buffer.ydisp) { const coordinates = this._mouseService.getCoords( event, diff --git a/src/browser/services/Services.ts b/src/browser/services/Services.ts index e1fb5dbd5e..9f2263388c 100644 --- a/src/browser/services/Services.ts +++ b/src/browser/services/Services.ts @@ -105,14 +105,6 @@ export interface ISelectionService { isCellInSelection(x: number, y: number): boolean; } -export const ISoundService = createDecorator('SoundService'); -export interface ISoundService { - serviceBrand: undefined; - - playBellSound(): void; -} - - export const ICharacterJoinerService = createDecorator('CharacterJoinerService'); export interface ICharacterJoinerService { serviceBrand: undefined; diff --git a/src/browser/services/SoundService.ts b/src/browser/services/SoundService.ts deleted file mode 100644 index a3b6800d42..0000000000 --- a/src/browser/services/SoundService.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { IOptionsService } from 'common/services/Services'; -import { ISoundService } from 'browser/services/Services'; - -export class SoundService implements ISoundService { - public serviceBrand: undefined; - - private static _audioContext: AudioContext; - - public static get audioContext(): AudioContext | null { - if (!SoundService._audioContext) { - const audioContextCtor: typeof AudioContext = (window as any).AudioContext || (window as any).webkitAudioContext; - if (!audioContextCtor) { - console.warn('Web Audio API is not supported by this browser. Consider upgrading to the latest version'); - return null; - } - SoundService._audioContext = new audioContextCtor(); - } - return SoundService._audioContext; - } - - constructor( - @IOptionsService private _optionsService: IOptionsService - ) { - } - - public playBellSound(): void { - const ctx = SoundService.audioContext; - if (!ctx) { - return; - } - const bellAudioSource = ctx.createBufferSource(); - ctx.decodeAudioData(this._base64ToArrayBuffer(this._removeMimeType(this._optionsService.rawOptions.bellSound)), (buffer) => { - bellAudioSource.buffer = buffer; - bellAudioSource.connect(ctx.destination); - bellAudioSource.start(0); - }); - } - - private _base64ToArrayBuffer(base64: string): ArrayBuffer { - const binaryString = window.atob(base64); - const len = binaryString.length; - const bytes = new Uint8Array(len); - - for (let i = 0; i < len; i++) { - bytes[i] = binaryString.charCodeAt(i); - } - - return bytes.buffer; - } - - private _removeMimeType(dataURI: string): string { - // Split the input to get the mime-type and the data itself - const splitUri = dataURI.split(','); - - // Return only the data - return splitUri[1]; - } -} diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 11d9a8c580..48f3a69e02 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -136,12 +136,6 @@ export class MockOptionsService implements IOptionsService { this.options[key] = options[key]; } } - public setOption(key: string, value: T): void { - throw new Error('Method not implemented.'); - } - public getOption(key: string): T { - throw new Error('Method not implemented.'); - } } // defaults to V6 always to keep tests passing diff --git a/src/common/services/OptionsService.test.ts b/src/common/services/OptionsService.test.ts index ae9e77ec06..a65cb986b6 100644 --- a/src/common/services/OptionsService.test.ts +++ b/src/common/services/OptionsService.test.ts @@ -17,16 +17,16 @@ describe('OptionsService', () => { }); it('uses default value if invalid constructor option values passed for cols/rows', () => { const optionsService = new OptionsService({ cols: undefined, rows: undefined }); - assert.equal(optionsService.getOption('rows'), DEFAULT_OPTIONS.rows); - assert.equal(optionsService.getOption('cols'), DEFAULT_OPTIONS.cols); + assert.equal(optionsService.options.rows, DEFAULT_OPTIONS.rows); + assert.equal(optionsService.options.cols, DEFAULT_OPTIONS.cols); }); it('uses values from constructor option values if correctly passed', () => { const optionsService = new OptionsService({ cols: 80, rows: 25 }); - assert.equal(optionsService.getOption('rows'), 25); - assert.equal(optionsService.getOption('cols'), 80); + assert.equal(optionsService.options.rows, 25); + assert.equal(optionsService.options.cols, 80); }); it('uses default value if invalid constructor option value passed', () => { - assert.equal(new OptionsService({ tabStopWidth: 0 }).getOption('tabStopWidth'), DEFAULT_OPTIONS.tabStopWidth); + assert.equal(new OptionsService({ tabStopWidth: 0 }).options.tabStopWidth, DEFAULT_OPTIONS.tabStopWidth); }); it('object.keys return the correct number of options', () => { const optionsService = new OptionsService({ cols: 80, rows: 25 }); @@ -39,36 +39,36 @@ describe('OptionsService', () => { service = new OptionsService({}); }); it('applies valid fontWeight option values', () => { - service.setOption('fontWeight', 'bold'); - assert.equal(service.getOption('fontWeight'), 'bold', '"bold" keyword value should be applied'); + service.options.fontWeight = 'bold'; + assert.equal(service.options.fontWeight, 'bold', '"bold" keyword value should be applied'); - service.setOption('fontWeight', 'normal'); - assert.equal(service.getOption('fontWeight'), 'normal', '"normal" keyword value should be applied'); + service.options.fontWeight = 'normal'; + assert.equal(service.options.fontWeight, 'normal', '"normal" keyword value should be applied'); - service.setOption('fontWeight', '600'); - assert.equal(service.getOption('fontWeight'), '600', 'String numeric values should be applied'); + service.options.fontWeight = '600'; + assert.equal(service.options.fontWeight, '600', 'String numeric values should be applied'); - service.setOption('fontWeight', 350); - assert.equal(service.getOption('fontWeight'), 350, 'Values between 1 and 1000 should be applied as is'); + service.options.fontWeight = 350; + assert.equal(service.options.fontWeight, 350, 'Values between 1 and 1000 should be applied as is'); - service.setOption('fontWeight', 1); - assert.equal(service.getOption('fontWeight'), 1, 'Range should include minimum value: 1'); + service.options.fontWeight = 1; + assert.equal(service.options.fontWeight, 1, 'Range should include minimum value: 1'); - service.setOption('fontWeight', 1000); - assert.equal(service.getOption('fontWeight'), 1000, 'Range should include maximum value: 1000'); + service.options.fontWeight = 1000; + assert.equal(service.options.fontWeight, 1000, 'Range should include maximum value: 1000'); }); it('normalizes invalid fontWeight option values', () => { - service.setOption('fontWeight', 350); - assert.doesNotThrow(() => service.setOption('fontWeight', 10000), 'fontWeight should be normalized instead of throwing'); - assert.equal(service.getOption('fontWeight'), DEFAULT_OPTIONS.fontWeight, 'Values greater than 1000 should be reset to default'); + service.options.fontWeight = 350; + assert.doesNotThrow(() => service.options.fontWeight = 10000), 'fontWeight should be normalized instead of throwing'; + assert.equal(service.options.fontWeight, DEFAULT_OPTIONS.fontWeight, 'Values greater than 1000 should be reset to default'); - service.setOption('fontWeight', 350); - service.setOption('fontWeight', -10); - assert.equal(service.getOption('fontWeight'), DEFAULT_OPTIONS.fontWeight, 'Values less than 1 should be reset to default'); + service.options.fontWeight = 350; + service.options.fontWeight = -10; + assert.equal(service.options.fontWeight, DEFAULT_OPTIONS.fontWeight, 'Values less than 1 should be reset to default'); - service.setOption('fontWeight', 350); - service.setOption('fontWeight', 'bold700'); - assert.equal(service.getOption('fontWeight'), DEFAULT_OPTIONS.fontWeight, 'Wrong string literals should be reset to default'); + service.options.fontWeight = 350; + service.options.fontWeight = 'bold700' as any; + assert.equal(service.options.fontWeight, DEFAULT_OPTIONS.fontWeight, 'Wrong string literals should be reset to default'); }); }); }); diff --git a/src/common/services/OptionsService.ts b/src/common/services/OptionsService.ts index 4f9600a4c4..550adb3181 100644 --- a/src/common/services/OptionsService.ts +++ b/src/common/services/OptionsService.ts @@ -8,12 +8,6 @@ import { EventEmitter, IEvent } from 'common/EventEmitter'; import { isMac } from 'common/Platform'; import { CursorStyle } from 'common/Types'; -// Source: https://freesound.org/people/altemark/sounds/45759/ -// This sound is released under the Creative Commons Attribution 3.0 Unported -// (CC BY 3.0) license. It was created by 'altemark'. No modifications have been -// made, apart from the conversion to base64. -export const DEFAULT_BELL_SOUND = 'data:audio/mp3;base64,SUQzBAAAAAAAI1RTU0UAAAAPAAADTGF2ZjU4LjMyLjEwNAAAAAAAAAAAAAAA//tQxAADB8AhSmxhIIEVCSiJrDCQBTcu3UrAIwUdkRgQbFAZC1CQEwTJ9mjRvBA4UOLD8nKVOWfh+UlK3z/177OXrfOdKl7pyn3Xf//WreyTRUoAWgBgkOAGbZHBgG1OF6zM82DWbZaUmMBptgQhGjsyYqc9ae9XFz280948NMBWInljyzsNRFLPWdnZGWrddDsjK1unuSrVN9jJsK8KuQtQCtMBjCEtImISdNKJOopIpBFpNSMbIHCSRpRR5iakjTiyzLhchUUBwCgyKiweBv/7UsQbg8isVNoMPMjAAAA0gAAABEVFGmgqK////9bP/6XCykxBTUUzLjEwMKqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'; - export const DEFAULT_OPTIONS: Readonly = { cols: 80, rows: 24, @@ -21,8 +15,6 @@ export const DEFAULT_OPTIONS: Readonly = { cursorStyle: 'block', cursorWidth: 1, customGlyphs: true, - bellSound: DEFAULT_BELL_SOUND, - bellStyle: 'none', drawBoldTextInBrightColors: true, fastScrollModifier: 'alt', fastScrollSensitivity: 5, @@ -31,7 +23,6 @@ export const DEFAULT_OPTIONS: Readonly = { fontWeight: 'normal', fontWeightBold: 'bold', lineHeight: 1.0, - linkTooltipHoverDuration: 500, letterSpacing: 0, logLevel: 'info', scrollback: 1000, @@ -41,12 +32,11 @@ export const DEFAULT_OPTIONS: Readonly = { macOptionClickForcesSelection: false, minimumContrastRatio: 1, disableStdin: false, - allowProposedApi: true, + allowProposedApi: false, allowTransparency: false, tabStopWidth: 8, theme: {}, rightClickSelectsWord: isMac, - rendererType: 'canvas', windowOptions: {}, windowsMode: false, wordSeparator: ' ()[]{}\',"`', @@ -118,10 +108,6 @@ export class OptionsService implements IOptionsService { } } - public setOption(key: string, value: any): void { - this.options[key] = value; - } - private _sanitizeAndValidateOption(key: string, value: any): any { switch (key) { case 'cursorStyle': @@ -132,9 +118,7 @@ export class OptionsService implements IOptionsService { throw new Error(`"${value}" is not a valid value for ${key}`); } break; - case 'bellStyle': case 'cursorStyle': - case 'rendererType': case 'wordSeparator': if (!value) { value = DEFAULT_OPTIONS[key]; @@ -180,10 +164,6 @@ export class OptionsService implements IOptionsService { } return value; } - - public getOption(key: string): any { - return this.options[key]; - } } function isCursorStyle(value: unknown): value is CursorStyle { diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index fab8435a56..709e171f52 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -198,22 +198,15 @@ export interface IOptionsService { readonly options: ITerminalOptions; readonly onOptionChange: IEvent; - - setOption(key: string, value: T): void; - getOption(key: string): T | undefined; } export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number; export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off'; -export type RendererType = 'dom' | 'canvas'; - export interface ITerminalOptions { allowProposedApi: boolean; allowTransparency: boolean; altClickMovesCursor: boolean; - bellSound: string; - bellStyle: 'none' | 'sound' /* | 'visual' | 'both' */; cols: number; convertEol: boolean; cursorBlink: boolean; @@ -230,12 +223,10 @@ export interface ITerminalOptions { fontWeightBold: FontWeight; letterSpacing: number; lineHeight: number; - linkTooltipHoverDuration: number; logLevel: LogLevel; macOptionIsMeta: boolean; macOptionClickForcesSelection: boolean; minimumContrastRatio: number; - rendererType: RendererType; rightClickSelectsWord: boolean; rows: number; screenReaderMode: boolean; diff --git a/src/headless/public/Terminal.test.ts b/src/headless/public/Terminal.test.ts index a65d8775d6..7f341b911c 100644 --- a/src/headless/public/Terminal.test.ts +++ b/src/headless/public/Terminal.test.ts @@ -12,7 +12,7 @@ let term: Terminal; describe('Headless API Tests', function (): void { beforeEach(() => { // Create default terminal to be used by most tests - term = new Terminal(); + term = new Terminal({ allowProposedApi: true }); }); it('Default options', async () => { @@ -102,7 +102,7 @@ describe('Headless API Tests', function (): void { }); it('clear', async () => { - term = new Terminal({ rows: 5 }); + term = new Terminal({ rows: 5, allowProposedApi: true }); for (let i = 0; i < 10; i++) { await writeSync('\n\rtest' + i); } @@ -114,12 +114,6 @@ describe('Headless API Tests', function (): void { } }); - it('getOption, setOption', async () => { - strictEqual(term.getOption('scrollback'), 1000); - term.setOption('scrollback', 50); - strictEqual(term.getOption('scrollback'), 50); - }); - describe('options', () => { const termOptions = { cols: 80, @@ -254,7 +248,7 @@ describe('Headless API Tests', function (): void { describe('buffer', () => { it('cursorX, cursorY', async () => { - term = new Terminal({ rows: 5, cols: 5 }); + term = new Terminal({ rows: 5, cols: 5, allowProposedApi: true }); strictEqual(term.buffer.active.cursorX, 0); strictEqual(term.buffer.active.cursorY, 0); await writeSync('foo'); @@ -275,7 +269,7 @@ describe('Headless API Tests', function (): void { }); it('viewportY', async () => { - term = new Terminal({ rows: 5 }); + term = new Terminal({ rows: 5, allowProposedApi: true }); strictEqual(term.buffer.active.viewportY, 0); await writeSync('\n\n\n\n'); strictEqual(term.buffer.active.viewportY, 0); @@ -290,7 +284,7 @@ describe('Headless API Tests', function (): void { }); it('baseY', async () => { - term = new Terminal({ rows: 5 }); + term = new Terminal({ rows: 5, allowProposedApi: true }); strictEqual(term.buffer.active.baseY, 0); await writeSync('\n\n\n\n'); strictEqual(term.buffer.active.baseY, 0); @@ -305,7 +299,7 @@ describe('Headless API Tests', function (): void { }); it('length', async () => { - term = new Terminal({ rows: 5 }); + term = new Terminal({ rows: 5, allowProposedApi: true }); strictEqual(term.buffer.active.length, 5); await writeSync('\n\n\n\n'); strictEqual(term.buffer.active.length, 5); @@ -317,13 +311,13 @@ describe('Headless API Tests', function (): void { describe('getLine', () => { it('invalid index', async () => { - term = new Terminal({ rows: 5 }); + term = new Terminal({ rows: 5, allowProposedApi: true }); strictEqual(term.buffer.active.getLine(-1), undefined); strictEqual(term.buffer.active.getLine(5), undefined); }); it('isWrapped', async () => { - term = new Terminal({ cols: 5 }); + term = new Terminal({ cols: 5, allowProposedApi: true }); strictEqual(term.buffer.active.getLine(0)!.isWrapped, false); strictEqual(term.buffer.active.getLine(1)!.isWrapped, false); await writeSync('abcde'); @@ -335,7 +329,7 @@ describe('Headless API Tests', function (): void { }); it('translateToString', async () => { - term = new Terminal({ cols: 5 }); + term = new Terminal({ cols: 5, allowProposedApi: true }); strictEqual(term.buffer.active.getLine(0)!.translateToString(), ' '); strictEqual(term.buffer.active.getLine(0)!.translateToString(true), ''); await writeSync('foo'); @@ -350,7 +344,7 @@ describe('Headless API Tests', function (): void { }); it('getCell', async () => { - term = new Terminal({ cols: 5 }); + term = new Terminal({ cols: 5, allowProposedApi: true }); strictEqual(term.buffer.active.getLine(0)!.getCell(-1), undefined); strictEqual(term.buffer.active.getLine(0)!.getCell(5), undefined); strictEqual(term.buffer.active.getLine(0)!.getCell(0)!.getChars(), ''); @@ -366,7 +360,7 @@ describe('Headless API Tests', function (): void { }); it('active, normal, alternate', async () => { - term = new Terminal({ cols: 5 }); + term = new Terminal({ cols: 5, allowProposedApi: true }); strictEqual(term.buffer.active.type, 'normal'); strictEqual(term.buffer.normal.type, 'normal'); strictEqual(term.buffer.alternate.type, 'alternate'); diff --git a/src/headless/public/Terminal.ts b/src/headless/public/Terminal.ts index 5a35fae29f..451d2372ad 100644 --- a/src/headless/public/Terminal.ts +++ b/src/headless/public/Terminal.ts @@ -172,32 +172,10 @@ export class Terminal implements ITerminalApi { public write(data: string | Uint8Array, callback?: () => void): void { this._core.write(data, callback); } - public writeUtf8(data: Uint8Array, callback?: () => void): void { - this._core.write(data, callback); - } public writeln(data: string | Uint8Array, callback?: () => void): void { this._core.write(data); this._core.write('\r\n', callback); } - public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string; - public getOption(key: 'allowTransparency' | 'altClickMovesCursor' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell'): boolean; - public getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; - public getOption(key: string): any; - public getOption(key: any): any { - return this._core.optionsService.getOption(key); - } - public setOption(key: 'bellSound' | 'fontFamily' | 'termName' | 'wordSeparator', value: string): void; - public setOption(key: 'fontWeight' | 'fontWeightBold', value: 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number): void; - public setOption(key: 'logLevel', value: 'debug' | 'info' | 'warn' | 'error' | 'off'): void; - public setOption(key: 'bellStyle', value: 'none' | 'visual' | 'sound' | 'both'): void; - public setOption(key: 'cursorStyle', value: 'block' | 'underline' | 'bar'): void; - public setOption(key: 'allowTransparency' | 'altClickMovesCursor' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell', value: boolean): void; - public setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void; - public setOption(key: 'cols' | 'rows', value: number): void; - public setOption(key: string, value: any): void; - public setOption(key: any, value: any): void { - this._core.optionsService.setOption(key, value); - } public reset(): void { this._core.reset(); } diff --git a/test/api/Terminal.api.ts b/test/api/Terminal.api.ts index 3a023d4a1f..502f438ce0 100644 --- a/test/api/Terminal.api.ts +++ b/test/api/Terminal.api.ts @@ -154,17 +154,9 @@ describe('API Integration Tests', function(): void { } }); - it('getOption, setOption', async () => { - await openTerminal(page); - assert.equal(await page.evaluate(`window.term.getOption('rendererType')`), 'canvas'); - await page.evaluate(`window.term.setOption('rendererType', 'dom')`); - assert.equal(await page.evaluate(`window.term.getOption('rendererType')`), 'dom'); - }); - describe('options', () => { it('getter', async () => { await openTerminal(page); - assert.equal(await page.evaluate(`window.term.options.rendererType`), 'canvas'); assert.equal(await page.evaluate(`window.term.options.cols`), 80); assert.equal(await page.evaluate(`window.term.options.rows`), 24); }); @@ -197,7 +189,7 @@ describe('API Integration Tests', function(): void { describe('renderer', () => { it('foreground', async () => { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); await writeSync(page, '\\x1b[30m0\\x1b[31m1\\x1b[32m2\\x1b[33m3\\x1b[34m4\\x1b[35m5\\x1b[36m6\\x1b[37m7'); await pollFor(page, `document.querySelectorAll('.xterm-rows > :nth-child(1) > *').length`, 9); assert.deepEqual(await page.evaluate(` @@ -222,7 +214,7 @@ describe('API Integration Tests', function(): void { }); it('background', async () => { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); await writeSync(page, '\\x1b[40m0\\x1b[41m1\\x1b[42m2\\x1b[43m3\\x1b[44m4\\x1b[45m5\\x1b[46m6\\x1b[47m7'); await pollFor(page, `document.querySelectorAll('.xterm-rows > :nth-child(1) > *').length`, 9); assert.deepEqual(await page.evaluate(` @@ -260,7 +252,7 @@ describe('API Integration Tests', function(): void { } else { assert.equal(await page.evaluate(`window.term.getSelection()`), '\n\nfoo\n\nbar\n\nbaz'); } - assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), { startColumn: 0, startRow: 0, endColumn: 5, endRow: 6 }); + assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), { start: { x: 0, y: 0 }, end: { x: 5, y: 6 } }); await page.evaluate(`window.term.clearSelection()`); assert.equal(await page.evaluate(`window.term.hasSelection()`), false); assert.equal(await page.evaluate(`window.term.getSelection()`), ''); @@ -268,7 +260,7 @@ describe('API Integration Tests', function(): void { await page.evaluate(`window.term.select(1, 2, 2)`); assert.equal(await page.evaluate(`window.term.hasSelection()`), true); assert.equal(await page.evaluate(`window.term.getSelection()`), 'oo'); - assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), { startColumn: 1, startRow: 2, endColumn: 3, endRow: 2 }); + assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), { start: { x: 1, y: 2 }, end: { x: 3, y: 2 } }); }); it('focus, blur', async () => { @@ -569,11 +561,11 @@ describe('API Integration Tests', function(): void { await writeSync(page, '\\n\\n\\n\\n'); await writeSync(page, '\\n\\n\\n\\n'); await writeSync(page, '\\n\\n\\n\\n'); - await page.evaluate(`window.term.addMarker(1)`); - await page.evaluate(`window.term.addMarker(2)`); + await page.evaluate(`window.term.registerMarker(1)`); + await page.evaluate(`window.term.registerMarker(2)`); await page.evaluate(`window.term.scrollLines(10)`); - await page.evaluate(`window.term.addMarker(3)`); - await page.evaluate(`window.term.addMarker(4)`); + await page.evaluate(`window.term.registerMarker(3)`); + await page.evaluate(`window.term.registerMarker(4)`); await page.evaluate(` for (let i = 0; i < window.term.markers.length; ++i) { const marker = window.term.markers[i]; @@ -734,11 +726,9 @@ describe('API Integration Tests', function(): void { describe('registerDecoration', () => { describe('bufferDecorations', () => { it('should register decorations and render them when terminal open is called', async () => { - await page.evaluate(`window.term = new Terminal({})`); - await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); - await page.waitForSelector('.xterm-text-layer'); - await page.evaluate(`window.marker1 = window.term.addMarker(1)`); - await page.evaluate(`window.marker2 = window.term.addMarker(2)`); + await openTerminal(page); + await page.evaluate(`window.marker1 = window.term.registerMarker(1)`); + await page.evaluate(`window.marker2 = window.term.registerMarker(2)`); await page.evaluate(`window.term.registerDecoration({ marker: window.marker1 })`); await page.evaluate(`window.term.registerDecoration({ marker: window.marker2 })`); await openTerminal(page); @@ -746,13 +736,13 @@ describe('API Integration Tests', function(): void { }); it('should return undefined when the marker has already been disposed of', async () => { await openTerminal(page); - await page.evaluate(`window.marker = window.term.addMarker(1)`); + await page.evaluate(`window.marker = window.term.registerMarker(1)`); await page.evaluate(`window.marker.dispose()`); await pollFor(page, `window.decoration = window.term.registerDecoration({ marker: window.marker });`, undefined); }); it('should throw when a negative x offset is provided', async () => { await openTerminal(page); - await page.evaluate(`window.marker = window.term.addMarker(1)`); + await page.evaluate(`window.marker = window.term.registerMarker(1)`); await page.evaluate(` try { window.decoration = window.term.registerDecoration({ marker: window.marker, x: -2 }); @@ -765,22 +755,18 @@ describe('API Integration Tests', function(): void { }); describe('overviewRulerDecorations', () => { it('should not add an overview ruler when width is not set', async () => { - await page.evaluate(`window.term = new Terminal({})`); - await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); - await page.waitForSelector('.xterm-text-layer'); - await page.evaluate(`window.marker1 = window.term.addMarker(1)`); - await page.evaluate(`window.marker2 = window.term.addMarker(2)`); + await openTerminal(page); + await page.evaluate(`window.marker1 = window.term.registerMarker(1)`); + await page.evaluate(`window.marker2 = window.term.registerMarker(2)`); await page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red', position: 'full' } })`); await page.evaluate(`window.term.registerDecoration({ marker: window.marker2, overviewRulerOptions: { color: 'blue', position: 'full' } })`); await openTerminal(page); await pollFor(page, `document.querySelectorAll('.xterm-decoration-overview-ruler').length`, 0); }); it('should add an overview ruler when width is set', async () => { - await page.evaluate(`window.term = new Terminal({ overviewRulerWidth: 15 })`); - await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); - await page.waitForSelector('.xterm-text-layer'); - await page.evaluate(`window.marker1 = window.term.addMarker(1)`); - await page.evaluate(`window.marker2 = window.term.addMarker(2)`); + await openTerminal(page, { overviewRulerWidth: 15 }); + await page.evaluate(`window.marker1 = window.term.registerMarker(1)`); + await page.evaluate(`window.marker2 = window.term.registerMarker(2)`); await page.evaluate(`window.term.registerDecoration({ marker: window.marker1, overviewRulerOptions: { color: 'red', position: 'full' } })`); await page.evaluate(`window.term.registerDecoration({ marker: window.marker2, overviewRulerOptions: { color: 'blue', position: 'full' } })`); await openTerminal(page); @@ -791,7 +777,7 @@ describe('API Integration Tests', function(): void { describe('registerLinkProvider', () => { it('should fire provideLinks when hovering cells', async () => { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); // Focus the terminal as the cursor will show and trigger a rerender, which can clear the // active link await page.evaluate('window.term.focus()'); @@ -813,7 +799,7 @@ describe('API Integration Tests', function(): void { }); it('should fire hover and leave events on the link', async () => { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); // Focus the terminal as the cursor will show and trigger a rerender, which can clear the // active link await page.evaluate('window.term.focus()'); @@ -851,7 +837,7 @@ describe('API Integration Tests', function(): void { }); it('should work fine when hover and leave callbacks are not provided', async () => { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); // Focus the terminal as the cursor will show and trigger a rerender, which can clear the // active link await page.evaluate('window.term.focus()'); @@ -894,7 +880,7 @@ describe('API Integration Tests', function(): void { }); it('should fire activate events when clicking the link', async () => { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); // Focus the terminal as the cursor will show and trigger a rerender, which can clear the // active link await page.evaluate('window.term.focus()'); @@ -936,7 +922,7 @@ describe('API Integration Tests', function(): void { }); it('should work when multiple links are provided on the same line', async () => { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); // Focus the terminal as the cursor will show and trigger a rerender, which can clear the // active link await page.evaluate('window.term.focus()'); @@ -985,7 +971,7 @@ describe('API Integration Tests', function(): void { }); it('should dispose links when hovering away', async () => { - await openTerminal(page, { rendererType: 'dom' }); + await openTerminal(page); // Focus the terminal as the cursor will show and trigger a rerender, which can clear the // active link await page.evaluate('window.term.focus()'); diff --git a/test/api/TestUtils.ts b/test/api/TestUtils.ts index 2acc4d0953..5731009b79 100644 --- a/test/api/TestUtils.ts +++ b/test/api/TestUtils.ts @@ -44,13 +44,9 @@ export async function timeout(ms: number): Promise { } export async function openTerminal(page: playwright.Page, options: ITerminalOptions = {}): Promise { - await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`); + await page.evaluate(`window.term = new Terminal(${JSON.stringify({ allowProposedApi: true, ...options })})`); await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`); - if (options.rendererType === 'dom') { - await page.waitForSelector('.xterm-rows'); - } else { - await page.waitForSelector('.xterm-text-layer'); - } + await page.waitForSelector('.xterm-rows'); } export function getBrowserType(): playwright.BrowserType | playwright.BrowserType | playwright.BrowserType { @@ -68,7 +64,7 @@ export function getBrowserType(): playwright.BrowserType { const browserType = getBrowserType(); const options: Record = { headless: process.argv.includes('--headless') diff --git a/tsconfig.all.json b/tsconfig.all.json index 6fa02446a2..4d2df3066a 100644 --- a/tsconfig.all.json +++ b/tsconfig.all.json @@ -7,6 +7,7 @@ { "path": "./test/api" }, { "path": "./test/benchmark" }, { "path": "./addons/xterm-addon-attach" }, + { "path": "./addons/xterm-addon-canvas" }, { "path": "./addons/xterm-addon-fit" }, { "path": "./addons/xterm-addon-ligatures" }, { "path": "./addons/xterm-addon-search" }, diff --git a/typings/xterm-headless.d.ts b/typings/xterm-headless.d.ts index 26a01e4c2b..76527e375b 100644 --- a/typings/xterm-headless.d.ts +++ b/typings/xterm-headless.d.ts @@ -38,16 +38,6 @@ declare module 'xterm-headless' { */ altClickMovesCursor?: boolean; - /** - * A data uri of the sound to use for the bell when `bellStyle = 'sound'`. - */ - bellSound?: string; - - /** - * The type of the bell notification the terminal will use. - */ - bellStyle?: 'none' | 'sound'; - /** * When enabled the cursor will be set to the beginning of the next line * with every new line. This is equivalent to sending '\r\n' for each '\n'. @@ -111,13 +101,6 @@ declare module 'xterm-headless' { */ lineHeight?: number; - /** - * The duration in milliseconds before link tooltip events fire when - * hovering on a link. - * @deprecated This will be removed when the link matcher API is removed. - */ - linkTooltipHoverDuration?: number; - /** * What log level to use, this will log for all levels below and including * what is set: @@ -640,18 +623,13 @@ declare module 'xterm-headless' { resize(columns: number, rows: number): void; /** - * (EXPERIMENTAL) Adds a marker to the normal buffer and returns it. If the - * alt buffer is active, undefined is returned. + * Adds a marker to the normal buffer and returns it. If the alt buffer is + * active, undefined is returned. * @param cursorYOffset The y position offset of the marker from the cursor. * @returns The new marker or undefined. */ registerMarker(cursorYOffset?: number): IMarker | undefined; - /** - * @deprecated use `registerMarker` instead. - */ - addMarker(cursorYOffset: number): IMarker | undefined; - /* * Disposes of the terminal, detaching it from the DOM and removing any * active listeners. @@ -711,96 +689,6 @@ declare module 'xterm-headless' { */ writeln(data: string | Uint8Array, callback?: () => void): void; - /** - * Write UTF8 data to the terminal. - * @param data The data to write to the terminal. - * @param callback Optional callback when data was processed. - * @deprecated use `write` instead - */ - writeUtf8(data: Uint8Array, callback?: () => void): void; - - /** - * Retrieves an option's value from the terminal. - * @param key The option key. - */ - getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string; - /** - * Retrieves an option's value from the terminal. - * @param key The option key. - */ - getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell' | 'windowsMode'): boolean; - /** - * Retrieves an option's value from the terminal. - * @param key The option key. - */ - getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; - /** - * Retrieves an option's value from the terminal. - * @param key The option key. - */ - getOption(key: string): any; - - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - */ - setOption(key: 'fontFamily' | 'termName' | 'bellSound' | 'wordSeparator', value: string): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - */ - setOption(key: 'fontWeight' | 'fontWeightBold', value: null | 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - */ - setOption(key: 'logLevel', value: LogLevel): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - */ - setOption(key: 'bellStyle', value: null | 'none' | 'visual' | 'sound' | 'both'): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - */ - setOption(key: 'cursorStyle', value: null | 'block' | 'underline' | 'bar'): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - */ - setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'visualBell' | 'windowsMode', value: boolean): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - */ - setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - */ - setOption(key: 'theme', value: ITheme): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - */ - setOption(key: 'cols' | 'rows', value: number): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - */ - setOption(key: string, value: any): void; - /** * Perform a full reset (RIS, aka '\x1bc'). */ @@ -823,31 +711,6 @@ declare module 'xterm-headless' { activate(terminal: Terminal): void; } - /** - * An object representing a selection within the terminal. - */ - interface ISelectionPosition { - /** - * The start column of the selection. - */ - startColumn: number; - - /** - * The start row of the selection. - */ - startRow: number; - - /** - * The end column of the selection. - */ - endColumn: number; - - /** - * The end row of the selection. - */ - endRow: number; - } - /** * An object representing a range within the viewport of the terminal. */ diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index a3f47300a5..43e52ee6b0 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -20,11 +20,6 @@ declare module 'xterm' { */ export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off'; - /** - * A string representing a renderer type. - */ - export type RendererType = 'dom' | 'canvas'; - /** * An object containing start up options for the terminal. */ @@ -50,16 +45,6 @@ declare module 'xterm' { */ altClickMovesCursor?: boolean; - /** - * A data uri of the sound to use for the bell when `bellStyle = 'sound'`. - */ - bellSound?: string; - - /** - * The type of the bell notification the terminal will use. - */ - bellStyle?: 'none' | 'sound'; - /** * When enabled the cursor will be set to the beginning of the next line * with every new line. This is equivalent to sending '\r\n' for each '\n'. @@ -148,13 +133,6 @@ declare module 'xterm' { */ lineHeight?: number; - /** - * The duration in milliseconds before link tooltip events fire when - * hovering on a link. - * @deprecated This will be removed when the link matcher API is removed. - */ - linkTooltipHoverDuration?: number; - /** * What log level to use, this will log for all levels below and including * what is set: @@ -193,16 +171,6 @@ declare module 'xterm' { */ minimumContrastRatio?: number; - /** - * The type of renderer to use, this allows using the fallback DOM renderer - * when canvas is too slow for the environment. The following features do - * not work when the DOM renderer is used: - * - * - Letter spacing - * - Cursor blink - */ - rendererType?: RendererType; - /** * Whether to select the word under the cursor on right click, this is * standard behavior in a lot of macOS applications. @@ -326,50 +294,6 @@ declare module 'xterm' { extendedAnsi?: string[]; } - /** - * An object containing options for a link matcher. - */ - export interface ILinkMatcherOptions { - /** - * The index of the link from the regex.match(text) call. This defaults to 0 - * (for regular expressions without capture groups). - */ - matchIndex?: number; - - /** - * A callback that validates whether to create an individual link, pass - * whether the link is valid to the callback. - */ - validationCallback?: (uri: string, callback: (isValid: boolean) => void) => void; - - /** - * A callback that fires when the mouse hovers over a link for a period of - * time (defined by {@link ITerminalOptions.linkTooltipHoverDuration}). - */ - tooltipCallback?: (event: MouseEvent, uri: string, location: IViewportRange) => boolean | void; - - /** - * A callback that fires when the mouse leaves a link. Note that this can - * happen even when tooltipCallback hasn't fired for the link yet. - */ - leaveCallback?: () => void; - - /** - * The priority of the link matcher, this defines the order in which the - * link matcher is evaluated relative to others, from highest to lowest. The - * default value is 0. - */ - priority?: number; - - /** - * A callback that fires when the mousedown and click events occur that - * determines whether a link will be activated upon click. This enables - * only activating a link when a certain modifier is held down, if not the - * mouse event will continue propagation (eg. double click to select word). - */ - willLinkActivate?: (event: MouseEvent, uri: string) => boolean; - } - /** * An object that can be disposed via a dispose function. */ @@ -725,9 +649,7 @@ declare module 'xterm' { readonly cols: number; /** - * (EXPERIMENTAL) The terminal's current buffer, this might be either the - * normal buffer or the alt buffer depending on what's running in the - * terminal. + * Access to the terminal's normal and alt buffer. */ readonly buffer: IBufferNamespace; @@ -738,8 +660,7 @@ declare module 'xterm' { readonly markers: ReadonlyArray; /** - * (EXPERIMENTAL) Get the parser interface to register - * custom escape sequence handlers. + * Get the parser interface to register custom escape sequence handlers. */ readonly parser: IParser; @@ -919,28 +840,6 @@ declare module 'xterm' { */ attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void; - /** - * (EXPERIMENTAL) Registers a link matcher, allowing custom link patterns to - * be matched and handled. - * @deprecated The link matcher API is now deprecated in favor of the link - * provider API, see `registerLinkProvider`. - * @param regex The regular expression to search for, specifically this - * searches the textContent of the rows. You will want to use \s to match a - * space ' ' character for example. - * @param handler The callback when the link is called. - * @param options Options for the link matcher. - * @return The ID of the new matcher, this can be used to deregister. - */ - registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number; - - /** - * (EXPERIMENTAL) Deregisters a link matcher if it has been registered. - * @deprecated The link matcher API is now deprecated in favor of the link - * provider API, see `registerLinkProvider`. - * @param matcherId The link matcher's ID (returned after register) - */ - deregisterLinkMatcher(matcherId: number): void; - /** * Registers a link provider, allowing a custom parser to be used to match * and handle links. Multiple link providers can be used, they will be asked @@ -988,18 +887,13 @@ declare module 'xterm' { deregisterCharacterJoiner(joinerId: number): void; /** - * (EXPERIMENTAL) Adds a marker to the normal buffer and returns it. If the - * alt buffer is active, undefined is returned. + * Adds a marker to the normal buffer and returns it. If the alt buffer is + * active, undefined is returned. * @param cursorYOffset The y position offset of the marker from the cursor. * @returns The new marker or undefined. */ registerMarker(cursorYOffset?: number): IMarker | undefined; - /** - * @deprecated use `registerMarker` instead. - */ - addMarker(cursorYOffset: number): IMarker | undefined; - /** * (EXPERIMENTAL) Adds a decoration to the terminal using * @param decorationOptions, which takes a marker and an optional anchor, @@ -1023,7 +917,7 @@ declare module 'xterm' { /** * Gets the selection position or undefined if there is no selection. */ - getSelectionPosition(): ISelectionPosition | undefined; + getSelectionPosition(): IBufferRange | undefined; /** * Clears the current terminal selection. @@ -1109,122 +1003,12 @@ declare module 'xterm' { */ writeln(data: string | Uint8Array, callback?: () => void): void; - /** - * Write UTF8 data to the terminal. - * @param data The data to write to the terminal. - * @param callback Optional callback when data was processed. - * @deprecated use `write` instead - */ - writeUtf8(data: Uint8Array, callback?: () => void): void; - /** * Writes text to the terminal, performing the necessary transformations for pasted text. * @param data The text to write to the terminal. */ paste(data: string): void; - /** - * Retrieves an option's value from the terminal. - * @param key The option key. - * @deprecated Use `options` instead. - */ - getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'logLevel' | 'rendererType' | 'termName' | 'wordSeparator'): string; - /** - * Retrieves an option's value from the terminal. - * @param key The option key. - * @deprecated Use `options` instead. - */ - getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'visualBell' | 'windowsMode'): boolean; - /** - * Retrieves an option's value from the terminal. - * @param key The option key. - * @deprecated Use `options` instead. - */ - getOption(key: 'cols' | 'fontSize' | 'letterSpacing' | 'lineHeight' | 'rows' | 'tabStopWidth' | 'scrollback'): number; - /** - * Retrieves an option's value from the terminal. - * @param key The option key. - * @deprecated Use `options` instead. - */ - getOption(key: 'fontWeight' | 'fontWeightBold'): FontWeight; - /** - * Retrieves an option's value from the terminal. - * @param key The option key. - * @deprecated Use `options` instead. - */ - getOption(key: string): any; - - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - * @deprecated Use `options` instead. - */ - setOption(key: 'fontFamily' | 'termName' | 'bellSound' | 'wordSeparator', value: string): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - * @deprecated Use `options` instead. - */ - setOption(key: 'fontWeight' | 'fontWeightBold', value: null | 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - * @deprecated Use `options` instead. - */ - setOption(key: 'logLevel', value: LogLevel): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - * @deprecated Use `options` instead. - */ - setOption(key: 'bellStyle', value: null | 'none' | 'visual' | 'sound' | 'both'): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - * @deprecated Use `options` instead. - */ - setOption(key: 'cursorStyle', value: null | 'block' | 'underline' | 'bar'): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - * @deprecated Use `options` instead. - */ - setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'disableStdin' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'visualBell' | 'windowsMode', value: boolean): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - * @deprecated Use `options` instead. - */ - setOption(key: 'fontSize' | 'letterSpacing' | 'lineHeight' | 'tabStopWidth' | 'scrollback', value: number): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - * @deprecated Use `options` instead. - */ - setOption(key: 'theme', value: ITheme): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - * @deprecated Use `options` instead. - */ - setOption(key: 'cols' | 'rows', value: number): void; - /** - * Sets an option on the terminal. - * @param key The option key. - * @param value The option value. - * @deprecated Use `options` instead. - */ - setOption(key: string, value: any): void; - /** * Tells the renderer to refresh terminal content between two rows * (inclusive) at the next opportunity. @@ -1263,35 +1047,10 @@ declare module 'xterm' { activate(terminal: Terminal): void; } - /** - * An object representing a selection within the terminal. - */ - interface ISelectionPosition { - /** - * The start column of the selection. - */ - startColumn: number; - - /** - * The start row of the selection. - */ - startRow: number; - - /** - * The end column of the selection. - */ - endColumn: number; - - /** - * The end row of the selection. - */ - endRow: number; - } - /** * An object representing a range within the viewport of the terminal. */ - export interface IViewportRange { + export interface IViewportRange { /** * The start of the range. */