From 290394b401c53e096f4659daea4eb1c3417e9e9d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 3 Jan 2019 20:22:53 -0800 Subject: [PATCH 01/42] New addon prototype Part of #1128 --- demo/client.ts | 5 +- src/Terminal.test.ts | 2 +- src/Terminal.ts | 17 ++++- src/addons/webLinks/webLinks.ts | 20 +++++- src/public/Terminal.ts | 11 +++- src/ui/AddonManager.test.ts | 111 ++++++++++++++++++++++++++++++++ src/ui/AddonManager.ts | 68 +++++++++++++++++++ src/ui/TestUtils.test.ts | 11 +++- typings/xterm.d.ts | 23 +++++++ 9 files changed, 261 insertions(+), 7 deletions(-) create mode 100644 src/ui/AddonManager.test.ts create mode 100644 src/ui/AddonManager.ts diff --git a/demo/client.ts b/demo/client.ts index a3a912f6..ae628baa 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -29,7 +29,7 @@ Terminal.applyAddon(attach); Terminal.applyAddon(fit); Terminal.applyAddon(fullscreen); Terminal.applyAddon(search); -Terminal.applyAddon(webLinks); +// Terminal.applyAddon(webLinks); Terminal.applyAddon(winptyCompat); @@ -84,6 +84,7 @@ function createTerminal(): void { terminalContainer.removeChild(terminalContainer.children[0]); } term = new Terminal({}); + (term as TerminalType).loadAddon(webLinks.WebLinksAddon).init(); window.term = term; // Expose `term` to window for debugging purposes term.on('resize', (size: { cols: number, rows: number }) => { if (!pid) { @@ -100,7 +101,7 @@ function createTerminal(): void { term.open(terminalContainer); term.winptyCompatInit(); - term.webLinksInit(); + // term.webLinksInit(); term.fit(); term.focus(); diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index fdc9678b..4807ddde 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -16,7 +16,7 @@ class TestTerminal extends Terminal { public keyPress(ev: any): boolean { return this._keyPress(ev); } } -describe('term.js addons', () => { +describe('Terminal', () => { let term: TestTerminal; const termOptions = { cols: INIT_COLS, diff --git a/src/Terminal.ts b/src/Terminal.ts index bc97de29..9e0ef6b9 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -45,13 +45,14 @@ import { DEFAULT_ANSI_COLORS } from './renderer/ColorManager'; import { MouseZoneManager } from './ui/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './ui/ScreenDprMonitor'; -import { ITheme, IMarker, IDisposable } from 'xterm'; +import { ITheme, IMarker, IDisposable, ITerminalAddon, ITerminalAddonConstructor } from 'xterm'; import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache'; import { DomRenderer } from './renderer/dom/DomRenderer'; import { IKeyboardEvent } from './common/Types'; import { evaluateKeyboardEvent } from './core/input/Keyboard'; import { KeyboardResultType, ICharset } from './core/Types'; import { clone } from './common/Clone'; +import { AddonManager } from './ui/AddonManager'; // Let it work inside Node.js for automated testing purposes. const document = (typeof window !== 'undefined') ? window.document : null; @@ -203,6 +204,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _mouseZoneManager: IMouseZoneManager; public mouseHelper: MouseHelper; private _accessibilityManager: AccessibilityManager; + private _addonManager: AddonManager; private _screenDprMonitor: ScreenDprMonitor; private _theme: ITheme; @@ -309,6 +311,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.linkifier = this.linkifier || new Linkifier(this); this._mouseZoneManager = this._mouseZoneManager || null; this.soundManager = this.soundManager || new SoundManager(this); + this._addonManager = this._addonManager || new AddonManager(); // Create the terminal's buffers and set the current buffer this.buffers = new BufferSet(this); @@ -1933,6 +1936,18 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // return this.options.bellStyle === 'sound' || // this.options.bellStyle === 'both'; } + + public loadAddon(addonConstructor: ITerminalAddonConstructor): T { + return this._addonManager.loadAddon(this, addonConstructor); + } + + public disposeAddon(addonConstructor: ITerminalAddonConstructor): void { + this._addonManager.disposeAddon(addonConstructor); + } + + public getAddon(addonConstructor: ITerminalAddonConstructor): T { + return this._addonManager.getAddon(addonConstructor); + } } /** diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts index f0d69cc5..6fc6b25d 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/src/addons/webLinks/webLinks.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal, ILinkMatcherOptions } from 'xterm'; +import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm'; const protocolClause = '(https?:\\/\\/)'; const domainCharacterSet = '[\\da-z\\.-]+'; @@ -35,12 +35,30 @@ function handleLink(event: MouseEvent, uri: string): void { * @param options Custom options to use, matchIndex will always be ignored. */ export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void { + // TODO: Remove this options.matchIndex = 1; term.registerLinkMatcher(strictUrlRegex, handler, options); } export function apply(terminalConstructor: typeof Terminal): void { + // TODO: Remove this (terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void { webLinksInit(this, handler, options); }; } + +export class WebLinksAddon implements ITerminalAddon { + private _linkMatcherId: number; + + constructor(private _terminal: Terminal) { + } + + public init(handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void { + options.matchIndex = 1; + this._linkMatcherId = this._terminal.registerLinkMatcher(strictUrlRegex, handler, options); + } + + public dispose(): void { + this._terminal.deregisterLinkMatcher(this._linkMatcherId); + } +} diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 87fcfaef..6f2de10f 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings } from 'xterm'; +import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ITerminalAddonConstructor } from 'xterm'; import { ITerminal } from '../Types'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../Strings'; @@ -154,6 +154,15 @@ export class Terminal implements ITerminalApi { public static applyAddon(addon: any): void { addon.apply(Terminal); } + public loadAddon(addonConstructor: ITerminalAddonConstructor): T { + return this._core.loadAddon(addonConstructor); + } + public getAddon(addonConstructor: ITerminalAddonConstructor): T { + return this._core.getAddon(addonConstructor); + } + public disposeAddon(addonConstructor: ITerminalAddonConstructor): void { + this._core.disposeAddon(addonConstructor); + } public static get strings(): ILocalizableStrings { return Strings; } diff --git a/src/ui/AddonManager.test.ts b/src/ui/AddonManager.test.ts new file mode 100644 index 00000000..ac83917c --- /dev/null +++ b/src/ui/AddonManager.test.ts @@ -0,0 +1,111 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { AddonManager, ILoadedAddon } from './AddonManager'; +import { ITerminalAddon } from 'xterm'; + +class TestAddonManager extends AddonManager { + public get addons(): ILoadedAddon[] { + return this._addons; + } +} + +describe('AddonManager', () => { + let manager: TestAddonManager; + + beforeEach(() => { + manager = new TestAddonManager(); + }); + + describe('loadAddon', () => { + it('should call addon constructor', () => { + let called = false; + class Addon implements ITerminalAddon { + constructor(terminal: any) { + assert.equal(terminal, 'foo', 'The first constructor arg should be Terminal'); + called = true; + } + dispose(): void { } + } + manager.loadAddon('foo' as any, Addon); + assert.equal(called, true); + }); + }); + + describe('getAddon', () => { + it('should fetch registered addons', () => { + class BaseAddon implements ITerminalAddon { + constructor() { } + dispose(): void { } + } + class Addon1 extends BaseAddon { } + class Addon2 extends BaseAddon { } + class Addon3 extends BaseAddon { } + const addon1 = manager.loadAddon(null, Addon1); + assert.equal(manager.getAddon(Addon1), addon1); + assert.equal(manager.addons.length, 1); + const addon2 = manager.loadAddon(null, Addon2); + assert.equal(manager.getAddon(Addon1), addon1); + assert.equal(manager.getAddon(Addon2), addon2); + assert.equal(manager.addons.length, 2); + const addon3 = manager.loadAddon(null, Addon3); + assert.equal(manager.getAddon(Addon1), addon1); + assert.equal(manager.getAddon(Addon2), addon2); + assert.equal(manager.getAddon(Addon3), addon3); + assert.equal(manager.addons.length, 3); + }); + }); + + describe('disposeAddon', () => { + it('should dispose the loaded addon and remove it from the loaded list', () => { + let called = 0; + class BaseAddon implements ITerminalAddon { + constructor() { } + dispose(): void { + called++; + } + } + class Addon1 extends BaseAddon { } + class Addon2 extends BaseAddon { } + class Addon3 extends BaseAddon { } + manager.loadAddon(null, Addon1); + manager.loadAddon(null, Addon2); + manager.loadAddon(null, Addon3); + assert.equal(manager.addons.length, 3); + manager.disposeAddon(Addon1); + assert.equal(called, 1); + assert.equal(manager.addons.length, 2); + manager.disposeAddon(Addon2); + assert.equal(called, 2); + assert.equal(manager.addons.length, 1); + manager.disposeAddon(Addon3); + assert.equal(called, 3); + assert.equal(manager.addons.length, 0); + }); + }); + + describe('dispose', () => { + it('should dispose all loaded addons', () => { + let called = 0; + class BaseAddon implements ITerminalAddon { + constructor() { } + dispose(): void { + called++; + } + } + class Addon1 extends BaseAddon { } + class Addon2 extends BaseAddon { } + class Addon3 extends BaseAddon { } + manager.loadAddon(null, Addon1); + manager.loadAddon(null, Addon2); + manager.loadAddon(null, Addon3); + assert.equal(manager.addons.length, 3); + manager.dispose(); + assert.equal(called, 3); + assert.equal(manager.addons.length, 0); + }); + }); +}); diff --git a/src/ui/AddonManager.ts b/src/ui/AddonManager.ts new file mode 100644 index 00000000..b55bd6a8 --- /dev/null +++ b/src/ui/AddonManager.ts @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2019 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { ITerminalAddon, ITerminalAddonConstructor, IDisposable, Terminal } from 'xterm'; + +export interface ILoadedAddon { + ctor: ITerminalAddonConstructor; + instance: ITerminalAddon; + dispose: () => void; +} + +export class AddonManager implements IDisposable { + protected _addons: ILoadedAddon[] = []; + + constructor() { + } + + public dispose(): void { + for (let i = this._addons.length - 1; i >= 0; i--) { + this._addons[i].instance.dispose(); + } + } + + public loadAddon(terminal: Terminal, addonConstructor: ITerminalAddonConstructor): T { + const instance = new addonConstructor(terminal); + const loadedAddon: ILoadedAddon = { + ctor: addonConstructor, + instance, + dispose: instance.dispose + }; + this._addons.push(loadedAddon); + instance.dispose = () => this._wrappedAddonDispose(loadedAddon); + return instance; + } + + public disposeAddon(addonConstructor: ITerminalAddonConstructor): void { + const match = this._addons.find(value => value.ctor === addonConstructor); + if (!match) { + throw new Error('Could not dispose an addon that has not been loaded'); + } + match.instance.dispose(); + } + + public getAddon(addonConstructor: ITerminalAddonConstructor): T { + const match = this._addons.find(value => value.ctor === addonConstructor); + if (!match) { + return undefined; + } + return match.instance as T; + } + + private _wrappedAddonDispose(loadedAddon: ILoadedAddon): void { + let index = -1; + for (let i = 0; i < this._addons.length; i++) { + if (this._addons[i].ctor === loadedAddon.ctor) { + index = i; + break; + } + } + if (index === -1) { + throw new Error('Could not dispose an addon that has not been loaded'); + } + loadedAddon.dispose(); + this._addons.splice(index, 1); + } +} diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index e6e4aaa3..fdcdd4e5 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -8,7 +8,7 @@ import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuff import { ICircularList, XtermListener } from '../common/Types'; import { Buffer } from '../Buffer'; import * as Browser from '../core/Platform'; -import { ITheme, IDisposable, IMarker } from 'xterm'; +import { ITheme, IDisposable, IMarker, ITerminalAddon, ITerminalAddonConstructor } from 'xterm'; import { Terminal } from '../Terminal'; export class TestTerminal extends Terminal { @@ -19,6 +19,15 @@ export class TestTerminal extends Terminal { } export class MockTerminal implements ITerminal { + loadAddon(addonConstructor: ITerminalAddonConstructor): T { + throw new Error('Method not implemented.'); + } + disposeAddon(addonConstructor: ITerminalAddonConstructor): void { + throw new Error('Method not implemented.'); + } + getAddon(addonConstructor: ITerminalAddonConstructor): T { + throw new Error('Method not implemented.'); + } markers: IMarker[]; addMarker(cursorYOffset: number): IMarker { throw new Error('Method not implemented.'); diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index c5d2b020..d73a9156 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -753,5 +753,28 @@ declare module 'xterm' { * @param addon The addon to apply. */ static applyAddon(addon: any): void; + + loadAddon(addonConstructor: ITerminalAddonConstructor): T; + disposeAddon(addonConstructor: ITerminalAddonConstructor): void; + getAddon(addonConstructor: ITerminalAddonConstructor): T; + } + + export interface ITerminalAddonConstructor { + new(terminal: Terminal): T; + } + + export interface ITerminalAddon { + /** + * This property declares all addon dependencies that must be intialized + * before this addon can be constructed. For addons with no dependencies + * just don't include this property. + */ + // readonly DEPENDENCIES?: ITerminalAddonConstructor[]; + + /** + * This function includes anything that needs to happen to clean up when + * the addon is being disposed. + */ + dispose(): void; } } From 7ba17ca7703b8ef9f682429c8fadee26d0f8df8e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 3 Jan 2019 22:19:30 -0800 Subject: [PATCH 02/42] Convert web links and attach to modules --- demo/client.ts | 17 +-- demo/start.js | 3 +- package.json | 6 +- src/addons/attach/Interfaces.ts | 22 ---- src/addons/attach/attach.test.ts | 20 ---- src/addons/attach/attach.ts | 155 --------------------------- src/addons/attach/index.html | 93 ---------------- src/addons/attach/package.json | 5 - src/addons/attach/tsconfig.json | 20 ---- src/addons/webLinks/package.json | 5 - src/addons/webLinks/tsconfig.json | 23 ---- src/addons/webLinks/webLinks.test.ts | 90 ---------------- src/addons/webLinks/webLinks.ts | 64 ----------- yarn.lock | 10 ++ 14 files changed, 27 insertions(+), 506 deletions(-) delete mode 100644 src/addons/attach/Interfaces.ts delete mode 100644 src/addons/attach/attach.test.ts delete mode 100644 src/addons/attach/attach.ts delete mode 100644 src/addons/attach/index.html delete mode 100644 src/addons/attach/package.json delete mode 100644 src/addons/attach/tsconfig.json delete mode 100644 src/addons/webLinks/package.json delete mode 100644 src/addons/webLinks/tsconfig.json delete mode 100644 src/addons/webLinks/webLinks.test.ts delete mode 100644 src/addons/webLinks/webLinks.ts diff --git a/demo/client.ts b/demo/client.ts index ae628baa..40679477 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -8,11 +8,12 @@ /// import { Terminal } from '../lib/public/Terminal'; -import * as attach from '../lib/addons/attach/attach'; +import { AttachAddon } from 'xterm-addon-attach'; +import { WebLinksAddon } from 'xterm-addon-web-links'; + import * as fit from '../lib/addons/fit/fit'; import * as fullscreen from '../lib/addons/fullscreen/fullscreen'; import * as search from '../lib/addons/search/search'; -import * as webLinks from '../lib/addons/webLinks/webLinks'; import * as winptyCompat from '../lib/addons/winptyCompat/winptyCompat'; import { ISearchOptions } from '../lib/addons/search/Interfaces'; @@ -25,15 +26,14 @@ export interface IWindowWithTerminal extends Window { } declare let window: IWindowWithTerminal; -Terminal.applyAddon(attach); Terminal.applyAddon(fit); Terminal.applyAddon(fullscreen); Terminal.applyAddon(search); -// Terminal.applyAddon(webLinks); Terminal.applyAddon(winptyCompat); let term; +let attachAddon: AttachAddon; let protocol; let socketURL; let socket; @@ -84,7 +84,12 @@ function createTerminal(): void { terminalContainer.removeChild(terminalContainer.children[0]); } term = new Terminal({}); - (term as TerminalType).loadAddon(webLinks.WebLinksAddon).init(); + + // Load addons + const typedTerm = term as TerminalType; + typedTerm.loadAddon(WebLinksAddon).init(); + attachAddon = typedTerm.loadAddon(AttachAddon); + window.term = term; // Expose `term` to window for debugging purposes term.on('resize', (size: { cols: number, rows: number }) => { if (!pid) { @@ -144,7 +149,7 @@ function createTerminal(): void { } function runRealTerminal(): void { - term.attach(socket); + attachAddon.attach(socket); term._initialized = true; } diff --git a/demo/start.js b/demo/start.js index 78f1ff1d..1796dc17 100644 --- a/demo/start.js +++ b/demo/start.js @@ -26,7 +26,8 @@ const clientConfig = { { test: /\.js$/, use: ["source-map-loader"], - enforce: "pre" + enforce: "pre", + exclude: /node_modules/ } ] }, diff --git a/package.json b/package.json index 5ed3a4b8..ab101fca 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,8 @@ "vinyl-source-stream": "^1.1.0", "webpack": "^4.17.1", "webpack-cli": "^3.1.0", + "xterm-addon-attach": "0.1.0-beta4", + "xterm-addon-web-links": "0.1.0-beta3", "zmodem.js": "^0.1.5" }, "scripts": { @@ -58,12 +60,12 @@ "test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha", "mocha": "gulp test", "tsc": "tsc", - "prebuild": "concurrently --kill-others-on-fail --names \"lib,attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem,css\" \"tsc\" \"tsc -p ./src/addons/attach\" \"tsc -p ./src/addons/fit\" \"tsc -p ./src/addons/fullscreen\" \"tsc -p ./src/addons/search\" \"tsc -p ./src/addons/terminado\" \"tsc -p ./src/addons/webLinks\" \"tsc -p ./src/addons/winptyCompat\" \"tsc -p ./src/addons/zmodem\" \"gulp css\"", + "prebuild": "concurrently --kill-others-on-fail --names \"lib,fit,fullscreen,search,terminado,winptyCompat,zmodem,css\" \"tsc\" \"tsc -p ./src/addons/fit\" \"tsc -p ./src/addons/fullscreen\" \"tsc -p ./src/addons/search\" \"tsc -p ./src/addons/terminado\" \"tsc -p ./src/addons/winptyCompat\" \"tsc -p ./src/addons/zmodem\" \"gulp css\"", "build": "gulp build", "prepublish": "npm run build", "coveralls": "nyc report --reporter=text-lcov | coveralls", "watch": "concurrently --kill-others-on-fail --names \"lib,css\" \"tsc -w\" \"gulp watch-css\"", - "watch-addons": "concurrently --kill-others-on-fail --names \"attach,fit,fullscreen,search,terminado,webLinks,winptyCompat,zmodem\" \"tsc -w -p ./src/addons/attach\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/webLinks\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\"", + "watch-addons": "concurrently --kill-others-on-fail --names \"fit,fullscreen,search,terminado,winptyCompat,zmodem\" \"tsc -w -p ./src/addons/fit\" \"tsc -w -p ./src/addons/fullscreen\" \"tsc -w -p ./src/addons/search\" \"tsc -w -p ./src/addons/terminado\" \"tsc -w -p ./src/addons/winptyCompat\" \"tsc -w -p ./src/addons/zmodem\"", "layering": "concurrently --kill-others-on-fail --names \"common,core\" \"tsc -p ./src/common\" \"tsc -p ./src/core\"" } } diff --git a/src/addons/attach/Interfaces.ts b/src/addons/attach/Interfaces.ts deleted file mode 100644 index ab5846f5..00000000 --- a/src/addons/attach/Interfaces.ts +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright (c) 2018 The xterm.js authors. All rights reserved. - * @license MIT - * - * Implements the attach method, that attaches the terminal to a WebSocket stream. - */ - -import { Terminal, IDisposable } from 'xterm'; - -export interface IAttachAddonTerminal extends Terminal { - _core: { - register(d: T): void; - }; - - __socket?: WebSocket; - __attachSocketBuffer?: string; - - __getMessage?(ev: MessageEvent): void; - __flushBuffer?(): void; - __pushToBuffer?(data: string): void; - __sendData?(data: string): void; -} diff --git a/src/addons/attach/attach.test.ts b/src/addons/attach/attach.test.ts deleted file mode 100644 index e280b656..00000000 --- a/src/addons/attach/attach.test.ts +++ /dev/null @@ -1,20 +0,0 @@ -/** - * Copyright (c) 2014 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; - -import * as attach from './attach'; - -class MockTerminal {} - -describe('attach addon', () => { - describe('apply', () => { - it('should do register the `attach` and `detach` methods', () => { - attach.apply(MockTerminal); - assert.equal(typeof (MockTerminal).prototype.attach, 'function'); - assert.equal(typeof (MockTerminal).prototype.detach, 'function'); - }); - }); -}); diff --git a/src/addons/attach/attach.ts b/src/addons/attach/attach.ts deleted file mode 100644 index f121e2e2..00000000 --- a/src/addons/attach/attach.ts +++ /dev/null @@ -1,155 +0,0 @@ -/** - * Copyright (c) 2014 The xterm.js authors. All rights reserved. - * @license MIT - * - * Implements the attach method, that attaches the terminal to a WebSocket stream. - */ - -import { Terminal, IDisposable } from 'xterm'; -import { IAttachAddonTerminal } from './Interfaces'; - -/** - * Attaches the given terminal to the given socket. - * - * @param term The terminal to be attached to the given socket. - * @param socket The socket to attach the current terminal. - * @param bidirectional Whether the terminal should send data to the socket as well. - * @param buffered Whether the rendering of incoming data should happen instantly or at a maximum - * frequency of 1 rendering per 10ms. - */ -export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void { - const addonTerminal = term; - bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional; - addonTerminal.__socket = socket; - - addonTerminal.__flushBuffer = () => { - addonTerminal.write(addonTerminal.__attachSocketBuffer); - addonTerminal.__attachSocketBuffer = null; - }; - - addonTerminal.__pushToBuffer = (data: string) => { - if (addonTerminal.__attachSocketBuffer) { - addonTerminal.__attachSocketBuffer += data; - } else { - addonTerminal.__attachSocketBuffer = data; - setTimeout(addonTerminal.__flushBuffer, 10); - } - }; - - // TODO: This should be typed but there seem to be issues importing the type - let myTextDecoder: any; - - addonTerminal.__getMessage = function(ev: MessageEvent): void { - let str: string; - - if (typeof ev.data === 'object') { - if (!myTextDecoder) { - myTextDecoder = new TextDecoder(); - } - if (ev.data instanceof ArrayBuffer) { - str = myTextDecoder.decode(ev.data); - displayData(str); - } else { - const fileReader = new FileReader(); - - fileReader.addEventListener('load', () => { - str = myTextDecoder.decode(fileReader.result); - displayData(str); - }); - fileReader.readAsArrayBuffer(ev.data); - } - } else if (typeof ev.data === 'string') { - displayData(ev.data); - } else { - throw Error(`Cannot handle "${typeof ev.data}" websocket message.`); - } - }; - - /** - * Push data to buffer or write it in the terminal. - * This is used as a callback for FileReader.onload. - * - * @param str String decoded by FileReader. - * @param data The data of the EventMessage. - */ - function displayData(str?: string, data?: string): void { - if (buffered) { - addonTerminal.__pushToBuffer(str || data); - } else { - addonTerminal.write(str || data); - } - } - - addonTerminal.__sendData = (data: string) => { - if (socket.readyState !== 1) { - return; - } - socket.send(data); - }; - - addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage)); - - if (bidirectional) { - addonTerminal._core.register(addonTerminal.addDisposableListener('data', addonTerminal.__sendData)); - } - - addonTerminal._core.register(addSocketListener(socket, 'close', () => detach(addonTerminal, socket))); - addonTerminal._core.register(addSocketListener(socket, 'error', () => detach(addonTerminal, socket))); -} - -function addSocketListener(socket: WebSocket, type: string, handler: (this: WebSocket, ev: Event) => any): IDisposable { - socket.addEventListener(type, handler); - return { - dispose: () => { - if (!handler) { - // Already disposed - return; - } - socket.removeEventListener(type, handler); - handler = null; - } - }; -} - -/** - * Detaches the given terminal from the given socket - * - * @param term The terminal to be detached from the given socket. - * @param socket The socket from which to detach the current terminal. - */ -export function detach(term: Terminal, socket: WebSocket): void { - const addonTerminal = term; - addonTerminal.off('data', addonTerminal.__sendData); - - socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket; - - if (socket) { - socket.removeEventListener('message', addonTerminal.__getMessage); - } - - delete addonTerminal.__socket; -} - - -export function apply(terminalConstructor: typeof Terminal): void { - /** - * Attaches the current terminal to the given socket - * - * @param socket The socket to attach the current terminal. - * @param bidirectional Whether the terminal should send data to the socket as well. - * @param buffered Whether the rendering of incoming data should happen instantly or at a maximum - * frequency of 1 rendering per 10ms. - */ - (terminalConstructor.prototype).attach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void { - attach(this, socket, bidirectional, buffered); - }; - - /** - * Detaches the current terminal from the given socket. - * - * @param socket The socket from which to detach the current terminal. - */ - (terminalConstructor.prototype).detach = function (socket: WebSocket): void { - detach(this, socket); - }; -} diff --git a/src/addons/attach/index.html b/src/addons/attach/index.html deleted file mode 100644 index b6f853be..00000000 --- a/src/addons/attach/index.html +++ /dev/null @@ -1,93 +0,0 @@ - - - - - - - - - - -
- -

- xterm.js: socket attach -

-

- Attach the terminal to a WebSocket terminal stream with ease. Perfect for attaching to your - Docker containers. -

-

- Socket information -

-
- - -
-
- -
- - - \ No newline at end of file diff --git a/src/addons/attach/package.json b/src/addons/attach/package.json deleted file mode 100644 index 9e45068b..00000000 --- a/src/addons/attach/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "xterm.attach", - "main": "attach.js", - "private": true -} diff --git a/src/addons/attach/tsconfig.json b/src/addons/attach/tsconfig.json deleted file mode 100644 index 359fbd24..00000000 --- a/src/addons/attach/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es5", - "lib": [ - "dom", - "es6", - ], - "rootDir": ".", - "outDir": "../../../lib/addons/attach/", - "sourceMap": true, - "removeComments": true, - "declaration": true, - "preserveWatchOutput": true - }, - "include": [ - "**/*.ts", - "../../../typings/xterm.d.ts" - ] -} diff --git a/src/addons/webLinks/package.json b/src/addons/webLinks/package.json deleted file mode 100644 index f200cab4..00000000 --- a/src/addons/webLinks/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "xterm.weblinks", - "main": "weblinks.js", - "private": true -} diff --git a/src/addons/webLinks/tsconfig.json b/src/addons/webLinks/tsconfig.json deleted file mode 100644 index 18105aa2..00000000 --- a/src/addons/webLinks/tsconfig.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "compilerOptions": { - "module": "commonjs", - "target": "es5", - "lib": [ - "dom", - "es5", - ], - "rootDir": ".", - "outDir": "../../../lib/addons/webLinks/", - "sourceMap": true, - "removeComments": true, - "declaration": true, - "preserveWatchOutput": true, - "types": [ - "../../node_modules/@types/mocha" - ] - }, - "include": [ - "**/*.ts", - "../../../typings/xterm.d.ts" - ] -} diff --git a/src/addons/webLinks/webLinks.test.ts b/src/addons/webLinks/webLinks.test.ts deleted file mode 100644 index 1e8a4ae7..00000000 --- a/src/addons/webLinks/webLinks.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; - -import * as webLinks from './webLinks'; - -class MockTerminal { - public regex: RegExp; - public handler: (event: MouseEvent, uri: string) => void; - public options?: any; - - public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: any): number { - this.regex = regex; - this.handler = handler; - this.options = options; - return 0; - } -} - -describe('webLinks addon', () => { - describe('apply', () => { - it('should do register the `webLinksInit` method', () => { - webLinks.apply(MockTerminal); - assert.equal(typeof (MockTerminal).prototype.webLinksInit, 'function'); - }); - }); - - it('should allow ~ character in URI path', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = ' http://foo.com/a~b#c~d?e~f '; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://foo.com/a~b#c~d?e~f'); - }); - - it('should allow : character in URI path', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = ' http://foo.com/colon:test '; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://foo.com/colon:test'); - }); - - it('should not allow : character at the end of a URI path', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = ' http://foo.com/colon:test: '; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://foo.com/colon:test'); - }); - - it('should not allow " character at the end of a URI enclosed with ""', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = '"http://foo.com/"'; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://foo.com/'); - }); - - it('should not allow \' character at the end of a URI enclosed with \'\'', () => { - const term = new MockTerminal(); - webLinks.webLinksInit(term); - - const row = '\'http://foo.com/\''; - - const match = row.match(term.regex); - const uri = match[term.options.matchIndex]; - - assert.equal(uri, 'http://foo.com/'); - }); -}); diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts deleted file mode 100644 index 6fc6b25d..00000000 --- a/src/addons/webLinks/webLinks.ts +++ /dev/null @@ -1,64 +0,0 @@ -/** - * Copyright (c) 2017 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { Terminal, ILinkMatcherOptions, ITerminalAddon } from 'xterm'; - -const protocolClause = '(https?:\\/\\/)'; -const domainCharacterSet = '[\\da-z\\.-]+'; -const negatedDomainCharacterSet = '[^\\da-z\\.-]+'; -const domainBodyClause = '(' + domainCharacterSet + ')'; -const tldClause = '([a-z\\.]{2,6})'; -const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})'; -const localHostClause = '(localhost)'; -const portClause = '(:\\d{1,5})'; -const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?'; -const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:"\'\\s])'; -const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*'; -const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; -const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; -const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+'; -const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause; -const start = '(?:^|' + negatedDomainCharacterSet + ')('; -const end = ')($|' + negatedPathCharacterSet + ')'; -const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end); - -function handleLink(event: MouseEvent, uri: string): void { - window.open(uri, '_blank'); -} - -/** - * Initialize the web links addon, registering the link matcher. - * @param term The terminal to use web links within. - * @param handler A custom handler to use. - * @param options Custom options to use, matchIndex will always be ignored. - */ -export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void { - // TODO: Remove this - options.matchIndex = 1; - term.registerLinkMatcher(strictUrlRegex, handler, options); -} - -export function apply(terminalConstructor: typeof Terminal): void { - // TODO: Remove this - (terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void { - webLinksInit(this, handler, options); - }; -} - -export class WebLinksAddon implements ITerminalAddon { - private _linkMatcherId: number; - - constructor(private _terminal: Terminal) { - } - - public init(handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void { - options.matchIndex = 1; - this._linkMatcherId = this._terminal.registerLinkMatcher(strictUrlRegex, handler, options); - } - - public dispose(): void { - this._terminal.deregisterLinkMatcher(this._linkMatcherId); - } -} diff --git a/yarn.lock b/yarn.lock index 5555321d..e345f6d2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7201,6 +7201,16 @@ xregexp@4.0.0: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= +xterm-addon-attach@0.1.0-beta4: + version "0.1.0-beta4" + resolved "https://registry.yarnpkg.com/xterm-addon-attach/-/xterm-addon-attach-0.1.0-beta4.tgz#546010f66533f22bfad7605345e44ed95d90a1de" + integrity sha512-HwxNoNS1Fxoo6+MPZJ+5+sMTQHrZEcptL4qstHlaERqxL7ei/lvKMpSRfIo1eRNqxL4vHzYkNWJO7QLATmdalA== + +xterm-addon-web-links@0.1.0-beta3: + version "0.1.0-beta3" + resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.1.0-beta3.tgz#bd2d45d399340bd1b5bbf44850a0be9c91a1451e" + integrity sha512-nkgwAYZXS97zL650MTl6RnA/iXYAD0yOVf/28+ZTlLQZJVYH2DP22rhOK0aqO+tWX91WUrvkcqxFCY965fomaQ== + y18n@^3.2.0, y18n@^3.2.1: version "3.2.1" resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" From 6bdd38089015c5a1e76e04059d99893bff06bbf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 20 Jan 2019 21:54:58 +0100 Subject: [PATCH 03/42] add utf8 decoder --- package.json | 2 + src/core/input/TextDecoder.test.ts | 136 +++++++++++++++- src/core/input/TextDecoder.ts | 245 ++++++++++++++++++++++++++++- 3 files changed, 375 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 5ed3a4b8..b223c55f 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "@types/jsdom": "11.0.1", "@types/mocha": "^2.2.33", "@types/node": "6.0.108", + "@types/utf8": "^2.1.6", "@types/webpack": "^4.4.11", "browserify": "^13.3.0", "chai": "3.5.0", @@ -39,6 +40,7 @@ "tslint": "^5.9.1", "tslint-consistent-codestyle": "^1.13.0", "typescript": "3.1", + "utf8": "^3.0.0", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", "webpack": "^4.17.1", diff --git a/src/core/input/TextDecoder.test.ts b/src/core/input/TextDecoder.test.ts index f69fbded..f38a5569 100644 --- a/src/core/input/TextDecoder.test.ts +++ b/src/core/input/TextDecoder.test.ts @@ -4,7 +4,8 @@ */ import { assert } from 'chai'; -import { StringToUtf32, stringFromCodePoint } from './TextDecoder'; +import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from './TextDecoder'; +import { encode } from 'utf8'; // convert UTF32 codepoints to string @@ -19,7 +20,30 @@ function toString(data: Uint32Array, length: number): string { return result; } -describe('StringToUtf32 Decoder', () => { +// convert "bytestring" (charCode 0-255) to bytes +function fromByteString(s: string): Uint8Array { + const result = new Uint8Array(s.length); + for (let i = 0; i < s.length; ++i) { + result[i] = s.charCodeAt(i); + } + return result; +} + + +const TEST_STRINGS = [ + 'Лорем ипсум долор сит амет, ех сеа аццусам диссентиет. Ан еос стет еирмод витуперата. Иус дицерет урбанитас ет. Ан при алтера долорес сплендиде, цу яуо интегре денияуе, игнота волуптариа инструцтиор цу вим.', + 'ლორემ იფსუმ დოლორ სით ამეთ, ფაცერ მუციუს ცონსეთეთურ ყუო იდ, ფერ ვივენდუმ ყუაერენდუმ ეა, ესთ ამეთ მოვეთ სუავითათე ცუ. ვითაე სენსიბუს ან ვიხ. ეხერცი დეთერრუისსეთ უთ ყუი. ვოცენთ დებითის ადიფისცი ეთ ფერ. ნეც ან ფეუგაით ფორენსიბუს ინთერესსეთ. იდ დიცო რიდენს იუს. დისსენთიეთ ცონსეყუუნთურ სედ ნე, ნოვუმ მუნერე ეუმ ათ, ნე ეუმ ნიჰილ ირაცუნდია ურბანითას.', + 'अधिकांश अमितकुमार प्रोत्साहित मुख्य जाने प्रसारन विश्लेषण विश्व दारी अनुवादक अधिकांश नवंबर विषय गटकउसि गोपनीयता विकास जनित परस्पर गटकउसि अन्तरराष्ट्रीयकरन होसके मानव पुर्णता कम्प्युटर यन्त्रालय प्रति साधन', + '覧六子当聞社計文護行情投身斗来。増落世的況上席備界先関権能万。本物挙歯乳全事携供板栃果以。頭月患端撤競見界記引去法条公泊候。決海備駆取品目芸方用朝示上用報。講申務紙約週堂出応理田流団幸稿。起保帯吉対阜庭支肯豪彰属本躍。量抑熊事府募動極都掲仮読岸。自続工就断庫指北速配鳴約事新住米信中験。婚浜袋著金市生交保他取情距。', + '八メル務問へふらく博辞説いわょ読全タヨムケ東校どっ知壁テケ禁去フミ人過を装5階がねぜ法逆はじ端40落ミ予竹マヘナセ任1悪た。省ぜりせ製暇ょへそけ風井イ劣手はぼまず郵富法く作断タオイ取座ゅょが出作ホシ月給26島ツチ皇面ユトクイ暮犯リワナヤ断連こうでつ蔭柔薄とレにの。演めけふぱ損田転10得観びトげぎ王物鉄夜がまけ理惜くち牡提づ車惑参ヘカユモ長臓超漫ぼドかわ。', + '모든 국민은 행위시의 법률에 의하여 범죄를 구성하지 아니하는 행위로 소추되지 아니하며. 전직대통령의 신분과 예우에 관하여는 법률로 정한다, 국회는 헌법 또는 법률에 특별한 규정이 없는 한 재적의원 과반수의 출석과 출석의원 과반수의 찬성으로 의결한다. 군인·군무원·경찰공무원 기타 법률이 정하는 자가 전투·훈련등 직무집행과 관련하여 받은 손해에 대하여는 법률이 정하는 보상외에 국가 또는 공공단체에 공무원의 직무상 불법행위로 인한 배상은 청구할 수 없다.', + 'كان فشكّل الشرقي مع, واحدة للمجهود تزامناً بعض بل. وتم جنوب للصين غينيا لم, ان وبدون وكسبت الأمور ذلك, أسر الخاسر الانجليزية هو. نفس لغزو مواقعها هو. الجو علاقة الصعداء انه أي, كما مع بمباركة للإتحاد الوزراء. ترتيب الأولى أن حدى, الشتوية باستحداث مدن بل, كان قد أوسع عملية. الأوضاع بالمطالبة كل قام, دون إذ شمال الربيع،. هُزم الخاصّة ٣٠ أما, مايو الصينية مع قبل.', + 'או סדר החול מיזמי קרימינולוגיה. קהילה בגרסה לויקיפדים אל היא, של צעד ציור ואלקטרוניקה. מדע מה ברית המזנון ארכיאולוגיה, אל טבלאות מבוקשים כלל. מאמרשיחהצפה העריכהגירסאות שכל אל, כתב עיצוב מושגי של. קבלו קלאסיים ב מתן. נבחרים אווירונאוטיקה אם מלא, לוח למנוע ארכיאולוגיה מה. ארץ לערוך בקרבת מונחונים או, עזרה רקטות לויקיפדים אחר גם.', + 'Лорем ლორემ अधिकांश 覧六子 八メル 모든 בקרבת 💮 😂 äggg 123€ 𝄞.' +]; + + +describe('StringToUtf32 decoder', () => { describe('full codepoint test', () => { it('0..65535', () => { const decoder = new StringToUtf32(); @@ -51,6 +75,15 @@ describe('StringToUtf32 Decoder', () => { } }); }); + it('test strings', () => { + const decoder = new StringToUtf32(); + const target = new Uint32Array(500); + for (let i = 0; i < TEST_STRINGS.length; ++i) { + const length = decoder.decode(TEST_STRINGS[i], target); + assert.equal(toString(target, length), TEST_STRINGS[i]); + decoder.clear(); + } + }); describe('stream handling', () => { it('surrogates mixed advance by 1', () => { const decoder = new StringToUtf32(); @@ -65,3 +98,102 @@ describe('StringToUtf32 Decoder', () => { }); }); }); + +describe('Utf8ToUtf32 decoder', () => { + describe('full codepoint test', () => { + it('0..65535 (1/2/3 byte sequences)', () => { + const decoder = new Utf8ToUtf32(); + const target = new Uint32Array(5); + for (let i = 0; i < 65536; ++i) { + // skip surrogate pairs + if (i >= 0xD800 && i <= 0xDFFF) { + continue; + } + const utf8Data = fromByteString(encode(String.fromCharCode(i))); + const length = decoder.decode(utf8Data, target); + assert.equal(length, 1); + assert.equal(toString(target, length), String.fromCharCode(i)); + decoder.clear(); + } + }); + it('65536..0x10FFFF (4 byte sequences)', function(): void { + this.timeout(20000); + const decoder = new Utf8ToUtf32(); + const target = new Uint32Array(5); + for (let i = 65536; i < 0x10FFFF; ++i) { + const utf8Data = fromByteString(encode(stringFromCodePoint(i))); + const length = decoder.decode(utf8Data, target); + assert.equal(length, 1); + assert.equal(target[0], i); + decoder.clear(); + } + }); + }); + it('test strings', () => { + const decoder = new Utf8ToUtf32(); + const target = new Uint32Array(500); + for (let i = 0; i < TEST_STRINGS.length; ++i) { + const utf8Data = fromByteString(encode(TEST_STRINGS[i])); + const length = decoder.decode(utf8Data, target); + assert.equal(toString(target, length), TEST_STRINGS[i]); + decoder.clear(); + } + }); + describe('stream handling', () => { + it('2 byte sequences - advance by 1', () => { + const decoder = new Utf8ToUtf32(); + const target = new Uint32Array(5); + const utf8Data = fromByteString('\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f\xc3\xb6\xc3\xa4\xc3\xbc'); + let decoded = ''; + for (let i = 0; i < utf8Data.length; ++i) { + const written = decoder.decode(utf8Data.slice(i, i + 1), target); + decoded += toString(target, written); + } + assert(decoded, 'ÄÖÜßöäü'); + }); + it('2/3 byte sequences - advance by 1', () => { + const decoder = new Utf8ToUtf32(); + const target = new Uint32Array(5); + const utf8Data = fromByteString('\xc3\x84\xe2\x82\xac\xc3\x96\xe2\x82\xac\xc3\x9c\xe2\x82\xac\xc3\x9f\xe2\x82\xac\xc3\xb6\xe2\x82\xac\xc3\xa4\xe2\x82\xac\xc3\xbc'); + let decoded = ''; + for (let i = 0; i < utf8Data.length; ++i) { + const written = decoder.decode(utf8Data.slice(i, i + 1), target); + decoded += toString(target, written); + } + assert(decoded, 'Āր܀߀ö€ä€ü'); + }); + it('2/3/4 byte sequences - advance by 1', () => { + const decoder = new Utf8ToUtf32(); + const target = new Uint32Array(5); + const utf8Data = fromByteString('\xc3\x84\xe2\x82\xac\xf0\x9d\x84\x9e\xc3\x96\xf0\x9d\x84\x9e\xe2\x82\xac\xc3\x9c\xf0\x9d\x84\x9e\xe2\x82\xac'); + let decoded = ''; + for (let i = 0; i < utf8Data.length; ++i) { + const written = decoder.decode(utf8Data.slice(i, i + 1), target); + decoded += toString(target, written); + } + assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€'); + }); + it('2/3/4 byte sequences - advance by 2', () => { + const decoder = new Utf8ToUtf32(); + const target = new Uint32Array(5); + const utf8Data = fromByteString('\xc3\x84\xe2\x82\xac\xf0\x9d\x84\x9e\xc3\x96\xf0\x9d\x84\x9e\xe2\x82\xac\xc3\x9c\xf0\x9d\x84\x9e\xe2\x82\xac'); + let decoded = ''; + for (let i = 0; i < utf8Data.length; i += 2) { + const written = decoder.decode(utf8Data.slice(i, i + 2), target); + decoded += toString(target, written); + } + assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€'); + }); + it('2/3/4 byte sequences - advance by 3', () => { + const decoder = new Utf8ToUtf32(); + const target = new Uint32Array(5); + const utf8Data = fromByteString('\xc3\x84\xe2\x82\xac\xf0\x9d\x84\x9e\xc3\x96\xf0\x9d\x84\x9e\xe2\x82\xac\xc3\x9c\xf0\x9d\x84\x9e\xe2\x82\xac'); + let decoded = ''; + for (let i = 0; i < utf8Data.length; i += 3) { + const written = decoder.decode(utf8Data.slice(i, i + 3), target); + decoded += toString(target, written); + } + assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€'); + }); + }); +}); diff --git a/src/core/input/TextDecoder.ts b/src/core/input/TextDecoder.ts index 04407a09..9080c09d 100644 --- a/src/core/input/TextDecoder.ts +++ b/src/core/input/TextDecoder.ts @@ -3,6 +3,19 @@ * @license MIT */ + +/** + * Polyfill - Convert UTF32 codepoint into JS string. + */ +export function stringFromCodePoint(codePoint: number): string { + if (codePoint > 0xFFFF) { + codePoint -= 0x10000; + return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); + } + return String.fromCharCode(codePoint); +} + + /** * StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints. * To keep the decoder in line with JS strings it handles single surrogates as UCS2. @@ -73,12 +86,232 @@ export class StringToUtf32 { } /** - * Polyfill - Convert UTF32 codepoint into JS string. + * Utf8Decoder - decodes UTF8 byte sequences into UTF32 codepoints. */ -export function stringFromCodePoint(codePoint: number): string { - if (codePoint > 0xFFFF) { - codePoint -= 0x10000; - return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00); +export class Utf8ToUtf32 { + public interim: Uint8Array = new Uint8Array(3); + + /** + * Clears interim bytes and resets decoder to clean state. + */ + public clear(): void { + this.interim.fill(0); + } + + /** + * Decodes UTF8 byte sequences in `input` to UTF32 codepoints in `target`. + * The methods assumes stream input and will store partly transmitted bytes + * and decode them with the next data chunk. + * Note: The method does no bound checks for target, therefore make sure + * the provided data chunk does not exceed the size of `target`. + * Returns the number of written codepoints in `target`. + */ + decode(input: Uint8Array, target: Uint32Array): number { + const length = input.length; + + if (!length) { + return 0; + } + + let size = 0; + let byte1; + let byte2; + let byte3; + let byte4; + let codepoint = 0; + let startPos = 0; + + // handle leftover bytes + if (this.interim[0]) { + let discardInterim = false; + let cp = this.interim[0]; + cp &= ((((cp & 0xE0) === 0xC0)) ? 0x1F : (((cp & 0xF0) === 0xE0)) ? 0x0F : 0x07); + let pos = 0; + let tmp; + while ((tmp = this.interim[++pos] & 0x3F) && pos < 4) { + cp <<= 6; + cp |= tmp; + } + // missing bytes - read ahead from input + const type = (((this.interim[0] & 0xE0) === 0xC0)) ? 2 : (((this.interim[0] & 0xF0) === 0xE0)) ? 3 : 4; + const missing = type - pos; + while (startPos < missing) { + if (startPos >= length) { + return 0; + } + tmp = input[startPos++]; + if ((tmp & 0xC0) !== 0x80) { + // wrong continuation, discard interim bytes completely + startPos--; + discardInterim = true; + break; + } else { + // need to save so we can continue short inputs in next call + this.interim[pos++] = tmp; + cp <<= 6; + cp |= tmp & 0x3F; + } + } + if (!discardInterim) { + // final test is type dependent + if (type === 2) { + if (cp < 0x80) { + // wrong starter byte + startPos--; + } else { + target[size++] = cp; + } + } else if (type === 3) { + if (cp < 0x0800 || (cp >= 0xD800 && cp <= 0xDFFF)) { + // illegal codepoint + } else { + target[size++] = cp; + } + } else { + if (codepoint < 0x010000 || codepoint > 0x10FFFF) { + // illegal codepoint + } else { + target[size++] = cp; + } + } + } + this.interim.fill(0); + } + + // loop through input + const fourStop = length - 4; + let i = startPos; + while (i < length) { + + /** + * ASCII shortcut with loop unrolled to 4 consecutive ASCII chars. + * This is a compromise between speed gain for ASCII + * and penalty for non ASCII: + * For best ASCII performance the char should be stored directly into target, + * but even a single attempt to write to target and compare afterwards + * penalizes non ASCII really bad (-50%), thus we load the char into byteX first, + * which reduces ASCII performance by ~15%. + * This trial for ASCII reduces non ASCII performance by ~10% which seems acceptible + * compared to the gains. + * Note that this optimization only takes place for 4 consecutive ASCII chars, + * for any shorter it bails out. Worst case - all 4 bytes being read but + * thrown away due to the last being a non ASCII char (-10% performance). + */ + while (i < fourStop + && !((byte1 = input[i]) & 0x80) + && !((byte2 = input[i + 1]) & 0x80) + && !((byte3 = input[i + 2]) & 0x80) + && !((byte4 = input[i + 3]) & 0x80)) + { + target[size++] = byte1; + target[size++] = byte2; + target[size++] = byte3; + target[size++] = byte4; + i += 4; + } + + // reread byte1 + byte1 = input[i++]; + + // 1 byte + if (byte1 < 0x80) { + target[size++] = byte1; + + // 2 bytes + } else if ((byte1 & 0xE0) === 0xC0) { + if (i >= length) { + this.interim[0] = byte1; + return size; + } + byte2 = input[i++]; + if ((byte2 & 0xC0) !== 0x80) { + // wrong continuation + i--; + continue; + } + codepoint = (byte1 & 0x1F) << 6 | (byte2 & 0x3F); + if (codepoint < 0x80) { + // wrong starter byte + i--; + continue; + } + target[size++] = codepoint; + + // 3 bytes + } else if ((byte1 & 0xF0) === 0xE0) { + if (i >= length) { + this.interim[0] = byte1; + return size; + } + byte2 = input[i++]; + if ((byte2 & 0xC0) !== 0x80) { + // wrong continuation + i--; + continue; + } + if (i >= length) { + this.interim[0] = byte1; + this.interim[1] = byte2; + return size; + } + byte3 = input[i++]; + if ((byte3 & 0xC0) !== 0x80) { + // wrong continuation + i--; + continue; + } + codepoint = (byte1 & 0x0F) << 12 | (byte2 & 0x3F) << 6 | (byte3 & 0x3F); + if (codepoint < 0x0800 || (codepoint >= 0xD800 && codepoint <= 0xDFFF)) { + // illegal codepoint, no i-- here + continue; + } + target[size++] = codepoint; + + // 4 bytes + } else if ((byte1 & 0xF8) === 0xF0) { + if (i >= length) { + this.interim[0] = byte1; + return size; + } + byte2 = input[i++]; + if ((byte2 & 0xC0) !== 0x80) { + // wrong continuation + i--; + continue; + } + if (i >= length) { + this.interim[0] = byte1; + this.interim[1] = byte2; + return size; + } + byte3 = input[i++]; + if ((byte3 & 0xC0) !== 0x80) { + // wrong continuation + i--; + continue; + } + if (i >= length) { + this.interim[0] = byte1; + this.interim[1] = byte2; + this.interim[2] = byte3; + return size; + } + byte4 = input[i++]; + if ((byte4 & 0xC0) !== 0x80) { + // wrong continuation + i--; + continue; + } + codepoint = (byte1 & 0x07) << 18 | (byte2 & 0x3F) << 12 | (byte3 & 0x3F) << 6 | (byte4 & 0x3F); + if (codepoint < 0x010000 || codepoint > 0x10FFFF) { + // illegal codepoint, no i-- here + continue; + } + target[size++] = codepoint; + } else { + // illegal byte, just skip + } + } + return size; } - return String.fromCharCode(codePoint); } From d7ea0edfcf27fdd3e37963b041fc647d035b0b8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 20 Jan 2019 22:19:50 +0100 Subject: [PATCH 04/42] add utf8 input to terminal --- src/InputHandler.ts | 33 ++++++++++++++++++++++++++++++--- src/Terminal.ts | 17 +++++++++++++++++ src/Types.ts | 1 + src/public/Terminal.ts | 3 +++ src/ui/TestUtils.test.ts | 3 +++ typings/xterm.d.ts | 6 ++++++ 6 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/InputHandler.ts b/src/InputHandler.ts index c41b4165..70491027 100644 --- a/src/InputHandler.ts +++ b/src/InputHandler.ts @@ -15,7 +15,7 @@ import { ICharset } from './core/Types'; import { IDisposable } from 'xterm'; import { Disposable } from './common/Lifecycle'; import { concat, utf32ToString } from './common/TypedArrayUtils'; -import { StringToUtf32, stringFromCodePoint } from './core/input/TextDecoder'; +import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32 } from './core/input/TextDecoder'; import { CellData } from './BufferLine'; /** @@ -104,8 +104,9 @@ class DECRQSS implements IDcsHandler { * each function's header comment. */ export class InputHandler extends Disposable implements IInputHandler { - private _parseBuffer: Uint32Array = new Uint32Array(4096); - private _stringDecoder: StringToUtf32 = new StringToUtf32(); + private _parseBuffer = new Uint32Array(4096); + private _stringDecoder = new StringToUtf32(); + private _utf8Decoder = new Utf8ToUtf32(); private _cell: CellData = new CellData(); constructor( @@ -311,6 +312,32 @@ export class InputHandler extends Disposable implements IInputHandler { } } + public parseUtf8(data: Uint8Array): void { + // Ensure the terminal is not disposed + if (!this._terminal) { + return; + } + + let buffer = this._terminal.buffer; + const cursorStartX = buffer.x; + const cursorStartY = buffer.y; + + // TODO: Consolidate debug/logging #1560 + if ((this._terminal).debug) { + this._terminal.log('data: ' + data); + } + + if (this._parseBuffer.length < data.length) { + this._parseBuffer = new Uint32Array(data.length); + } + this._parser.parse(this._parseBuffer, this._utf8Decoder.decode(data, this._parseBuffer)); + + buffer = this._terminal.buffer; + if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) { + this._terminal.emit('cursormove'); + } + } + public print(data: Uint32Array, start: number, end: number): void { let code: number; let chWidth: number; diff --git a/src/Terminal.ts b/src/Terminal.ts index 33d8e60f..e92ef08b 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1301,6 +1301,23 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } } + /** + * Writes utf8 data to the terminal. + * TODO: This currently does no flow control. + */ + public writeUtf8(data: Uint8Array): void { + if (this._isDisposed) { + return; + } + this._refreshStart = this.buffer.y; + this._refreshEnd = this.buffer.y; + + this._inputHandler.parseUtf8(data); + + this.updateRange(this.buffer.y); + this.refresh(this._refreshStart, this._refreshEnd); + } + /** * Writes text to the terminal. * @param data The text to write to the terminal. diff --git a/src/Types.ts b/src/Types.ts index d176799f..186e2149 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -111,6 +111,7 @@ export interface ICompositionHelper { */ export interface IInputHandler { parse(data: string): void; + parseUtf8(data: Uint8Array): void; print(data: Uint32Array, start: number, end: number): void; /** C0 BEL */ bell(): void; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 87fcfaef..7e219e08 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -122,6 +122,9 @@ export class Terminal implements ITerminalApi { public write(data: string): void { this._core.write(data); } + public writeUtf8(data: Uint8Array): void { + this._core.writeUtf8(data); + } public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold' | 'rendererType' | 'termName'): string; public getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean; public getOption(key: 'colors'): string[]; diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index 9d525fbf..13abee76 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -99,6 +99,9 @@ export class MockTerminal implements ITerminal { write(data: string): void { throw new Error('Method not implemented.'); } + writeUtf8(data: Uint8Array): void { + throw new Error('Method not implemented.'); + } bracketedPasteMode: boolean; mouseHelper: IMouseHelper; renderer: IRenderer; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 11fab909..ffaba90e 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -636,6 +636,12 @@ declare module 'xterm' { */ write(data: string): void; + /** + * Writes UTF8 data to the terminal. + * @param data The data to write to the terminal. + */ + writeUtf8(data: Uint8Array): void; + /** * Retrieves an option's value from the terminal. * @param key The option key. From e6e5ecc0f2c4742e781b0f6c7b95caaaae6a024f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 20 Jan 2019 23:06:51 +0100 Subject: [PATCH 05/42] change demo to utf8 input --- demo/client.ts | 1 + demo/server.js | 13 +++++++------ src/addons/attach/attach.ts | 5 +++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index a3a912f6..a38b8a5c 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -134,6 +134,7 @@ function createTerminal(): void { pid = processId; socketURL += processId; socket = new WebSocket(socketURL); + socket.binaryType = 'arraybuffer'; socket.onopen = runRealTerminal; socket.onclose = runFakeTerminal; socket.onerror = runFakeTerminal; diff --git a/demo/server.js b/demo/server.js index 5ff9ca61..86bc9069 100644 --- a/demo/server.js +++ b/demo/server.js @@ -32,7 +32,8 @@ function startServer() { cols: cols || 80, rows: rows || 24, cwd: process.env.PWD, - env: process.env + env: process.env, + encoding: null }); console.log('Created terminal with PID: ' + term.pid); @@ -62,20 +63,20 @@ function startServer() { ws.send(logs[term.pid]); function buffer(socket, timeout) { - let s = ''; + let buffer = []; let sender = null; return (data) => { - s += data; + buffer.push(data); if (!sender) { sender = setTimeout(() => { - socket.send(s); - s = ''; + socket.send(Buffer.concat(buffer)); + buffer = []; sender = null; }, timeout); } }; } - const send = buffer(ws, 5); + const send = buffer(ws, 5); term.on('data', function(data) { try { diff --git a/src/addons/attach/attach.ts b/src/addons/attach/attach.ts index f121e2e2..7333f92b 100644 --- a/src/addons/attach/attach.ts +++ b/src/addons/attach/attach.ts @@ -42,6 +42,11 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean addonTerminal.__getMessage = function(ev: MessageEvent): void { let str: string; + if (ev.data instanceof ArrayBuffer) { + addonTerminal.writeUtf8(new Uint8Array(ev.data)); + return; + } + if (typeof ev.data === 'object') { if (!myTextDecoder) { myTextDecoder = new TextDecoder(); From 3f993d675f304a3587677dfe8536a763d579a70e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 2 Apr 2019 20:17:33 -0400 Subject: [PATCH 06/42] Fix conflicts with new stuff --- src/public/Terminal.test.ts | 17 ----------------- src/tsconfig.all.json | 3 --- 2 files changed, 20 deletions(-) delete mode 100644 src/public/Terminal.test.ts diff --git a/src/public/Terminal.test.ts b/src/public/Terminal.test.ts deleted file mode 100644 index 06c8f1d5..00000000 --- a/src/public/Terminal.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright (c) 2016 The xterm.js authors. All rights reserved. - * @license MIT - */ - -import { assert } from 'chai'; -import { Terminal } from './Terminal'; -import * as attach from '../addons/attach/attach'; - -describe('Terminal', () => { - it('should apply addons with Terminal.applyAddon', () => { - Terminal.applyAddon(attach); - // Test that addon was applied successfully, adding attach to Terminal's - // prototype. - assert.equal(typeof (Terminal).prototype.attach, 'function'); - }); -}); diff --git a/src/tsconfig.all.json b/src/tsconfig.all.json index bee5df32..d0bf01a0 100644 --- a/src/tsconfig.all.json +++ b/src/tsconfig.all.json @@ -3,14 +3,11 @@ "include": [], "references": [ { "path": "." }, - { "path": "./addons/attach" }, { "path": "./addons/fit" }, { "path": "./addons/fullscreen" }, { "path": "./addons/search" }, { "path": "./addons/terminado" }, - { "path": "./addons/webLinks" }, { "path": "./addons/winptyCompat" }, { "path": "./addons/zmodem" } ] } - \ No newline at end of file From 26a80d0d3509dd7e997df7cf2ebdbc8e522ba39b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 7 Apr 2019 14:53:09 -0400 Subject: [PATCH 07/42] Convert to much simpler model --- demo/client.ts | 5 ++- demo/start.js | 1 + package.json | 4 +- src/Terminal.ts | 14 ++----- src/public/Terminal.ts | 12 ++---- src/ui/AddonManager.test.ts | 73 ++++--------------------------------- src/ui/AddonManager.ts | 36 ++++++------------ src/ui/TestUtils.test.ts | 10 +---- typings/xterm.d.ts | 18 ++++----- yarn.lock | 16 ++++---- 10 files changed, 49 insertions(+), 140 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 3d55bc00..f4a23f52 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -88,8 +88,9 @@ function createTerminal(): void { // Load addons const typedTerm = term as TerminalType; - typedTerm.loadAddon(WebLinksAddon).init(); - attachAddon = typedTerm.loadAddon(AttachAddon); + typedTerm.loadAddon(new WebLinksAddon()); + attachAddon = new AttachAddon(); + typedTerm.loadAddon(attachAddon); window.term = term; // Expose `term` to window for debugging purposes term.on('resize', (size: { cols: number, rows: number }) => { diff --git a/demo/start.js b/demo/start.js index 2e2186dc..7e13e790 100644 --- a/demo/start.js +++ b/demo/start.js @@ -31,6 +31,7 @@ const clientConfig = { ] }, resolve: { + modules: [path.resolve(__dirname, '..'), 'node_modules'], extensions: [ '.tsx', '.ts', '.js' ] }, output: { diff --git a/package.json b/package.json index 438e3e1b..b1f8e8f4 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,8 @@ "vinyl-source-stream": "^1.1.0", "webpack": "^4.17.1", "webpack-cli": "^3.1.0", - "xterm-addon-attach": "0.1.0-beta4", - "xterm-addon-web-links": "0.1.0-beta3", + "xterm-addon-attach": "0.1.0-beta7", + "xterm-addon-web-links": "0.1.0-beta6", "zmodem.js": "^0.1.5" }, "scripts": { diff --git a/src/Terminal.ts b/src/Terminal.ts index df8f5d90..7a29a282 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -44,7 +44,7 @@ import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager'; import { MouseZoneManager } from './ui/MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './ui/ScreenDprMonitor'; -import { ITheme, IMarker, IDisposable, ITerminalAddon, ITerminalAddonConstructor } from 'xterm'; +import { ITheme, IMarker, IDisposable, ITerminalAddon } from 'xterm'; import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache'; import { DomRenderer } from './renderer/dom/DomRenderer'; import { IKeyboardEvent } from './common/Types'; @@ -1925,16 +1925,8 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // this.options.bellStyle === 'both'; } - public loadAddon(addonConstructor: ITerminalAddonConstructor): T { - return this._addonManager.loadAddon(this, addonConstructor); - } - - public disposeAddon(addonConstructor: ITerminalAddonConstructor): void { - this._addonManager.disposeAddon(addonConstructor); - } - - public getAddon(addonConstructor: ITerminalAddonConstructor): T { - return this._addonManager.getAddon(addonConstructor); + public loadAddon(addon: ITerminalAddon): void { + return this._addonManager.loadAddon(this, addon); } } diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 6f2de10f..77889271 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ITerminalAddonConstructor } from 'xterm'; +import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon } from 'xterm'; import { ITerminal } from '../Types'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../Strings'; @@ -154,14 +154,8 @@ export class Terminal implements ITerminalApi { public static applyAddon(addon: any): void { addon.apply(Terminal); } - public loadAddon(addonConstructor: ITerminalAddonConstructor): T { - return this._core.loadAddon(addonConstructor); - } - public getAddon(addonConstructor: ITerminalAddonConstructor): T { - return this._core.getAddon(addonConstructor); - } - public disposeAddon(addonConstructor: ITerminalAddonConstructor): void { - this._core.disposeAddon(addonConstructor); + public loadAddon(addon: ITerminalAddon): void { + return this._core.loadAddon(addon); } public static get strings(): ILocalizableStrings { return Strings; diff --git a/src/ui/AddonManager.test.ts b/src/ui/AddonManager.test.ts index ac83917c..8198ce31 100644 --- a/src/ui/AddonManager.test.ts +++ b/src/ui/AddonManager.test.ts @@ -24,84 +24,27 @@ describe('AddonManager', () => { it('should call addon constructor', () => { let called = false; class Addon implements ITerminalAddon { - constructor(terminal: any) { + activate(terminal: any): void { assert.equal(terminal, 'foo', 'The first constructor arg should be Terminal'); called = true; } dispose(): void { } } - manager.loadAddon('foo' as any, Addon); + manager.loadAddon('foo' as any, new Addon()); assert.equal(called, true); }); }); - describe('getAddon', () => { - it('should fetch registered addons', () => { - class BaseAddon implements ITerminalAddon { - constructor() { } - dispose(): void { } - } - class Addon1 extends BaseAddon { } - class Addon2 extends BaseAddon { } - class Addon3 extends BaseAddon { } - const addon1 = manager.loadAddon(null, Addon1); - assert.equal(manager.getAddon(Addon1), addon1); - assert.equal(manager.addons.length, 1); - const addon2 = manager.loadAddon(null, Addon2); - assert.equal(manager.getAddon(Addon1), addon1); - assert.equal(manager.getAddon(Addon2), addon2); - assert.equal(manager.addons.length, 2); - const addon3 = manager.loadAddon(null, Addon3); - assert.equal(manager.getAddon(Addon1), addon1); - assert.equal(manager.getAddon(Addon2), addon2); - assert.equal(manager.getAddon(Addon3), addon3); - assert.equal(manager.addons.length, 3); - }); - }); - - describe('disposeAddon', () => { - it('should dispose the loaded addon and remove it from the loaded list', () => { - let called = 0; - class BaseAddon implements ITerminalAddon { - constructor() { } - dispose(): void { - called++; - } - } - class Addon1 extends BaseAddon { } - class Addon2 extends BaseAddon { } - class Addon3 extends BaseAddon { } - manager.loadAddon(null, Addon1); - manager.loadAddon(null, Addon2); - manager.loadAddon(null, Addon3); - assert.equal(manager.addons.length, 3); - manager.disposeAddon(Addon1); - assert.equal(called, 1); - assert.equal(manager.addons.length, 2); - manager.disposeAddon(Addon2); - assert.equal(called, 2); - assert.equal(manager.addons.length, 1); - manager.disposeAddon(Addon3); - assert.equal(called, 3); - assert.equal(manager.addons.length, 0); - }); - }); - describe('dispose', () => { it('should dispose all loaded addons', () => { let called = 0; - class BaseAddon implements ITerminalAddon { - constructor() { } - dispose(): void { - called++; - } + class Addon implements ITerminalAddon { + activate(): void {} + dispose(): void { called++; } } - class Addon1 extends BaseAddon { } - class Addon2 extends BaseAddon { } - class Addon3 extends BaseAddon { } - manager.loadAddon(null, Addon1); - manager.loadAddon(null, Addon2); - manager.loadAddon(null, Addon3); + manager.loadAddon(null, new Addon()); + manager.loadAddon(null, new Addon()); + manager.loadAddon(null, new Addon()); assert.equal(manager.addons.length, 3); manager.dispose(); assert.equal(called, 3); diff --git a/src/ui/AddonManager.ts b/src/ui/AddonManager.ts index b55bd6a8..34e7e5dd 100644 --- a/src/ui/AddonManager.ts +++ b/src/ui/AddonManager.ts @@ -3,12 +3,12 @@ * @license MIT */ -import { ITerminalAddon, ITerminalAddonConstructor, IDisposable, Terminal } from 'xterm'; +import { ITerminalAddon, IDisposable, Terminal } from 'xterm'; export interface ILoadedAddon { - ctor: ITerminalAddonConstructor; instance: ITerminalAddon; dispose: () => void; + isDisposed: boolean; } export class AddonManager implements IDisposable { @@ -23,38 +23,25 @@ export class AddonManager implements IDisposable { } } - public loadAddon(terminal: Terminal, addonConstructor: ITerminalAddonConstructor): T { - const instance = new addonConstructor(terminal); + public loadAddon(terminal: Terminal, instance: ITerminalAddon): void { const loadedAddon: ILoadedAddon = { - ctor: addonConstructor, instance, - dispose: instance.dispose + dispose: instance.dispose, + isDisposed: false }; this._addons.push(loadedAddon); instance.dispose = () => this._wrappedAddonDispose(loadedAddon); - return instance; - } - - public disposeAddon(addonConstructor: ITerminalAddonConstructor): void { - const match = this._addons.find(value => value.ctor === addonConstructor); - if (!match) { - throw new Error('Could not dispose an addon that has not been loaded'); - } - match.instance.dispose(); - } - - public getAddon(addonConstructor: ITerminalAddonConstructor): T { - const match = this._addons.find(value => value.ctor === addonConstructor); - if (!match) { - return undefined; - } - return match.instance as T; + instance.activate(terminal); } private _wrappedAddonDispose(loadedAddon: ILoadedAddon): void { + if (loadedAddon.isDisposed) { + // Do nothing if already disposed + return; + } let index = -1; for (let i = 0; i < this._addons.length; i++) { - if (this._addons[i].ctor === loadedAddon.ctor) { + if (this._addons[i] === loadedAddon) { index = i; break; } @@ -63,6 +50,7 @@ export class AddonManager implements IDisposable { throw new Error('Could not dispose an addon that has not been loaded'); } loadedAddon.dispose(); + loadedAddon.isDisposed = true; this._addons.splice(index, 1); } } diff --git a/src/ui/TestUtils.test.ts b/src/ui/TestUtils.test.ts index ec3aa60e..4a3a24e3 100644 --- a/src/ui/TestUtils.test.ts +++ b/src/ui/TestUtils.test.ts @@ -8,7 +8,7 @@ import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuff import { ICircularList, XtermListener } from '../common/Types'; import { Buffer } from '../Buffer'; import * as Browser from '../common/Platform'; -import { ITheme, IDisposable, IMarker, ITerminalAddon, ITerminalAddonConstructor } from 'xterm'; +import { ITheme, IDisposable, IMarker, ITerminalAddon } from 'xterm'; import { Terminal } from '../Terminal'; import { AttributeData } from '../BufferLine'; @@ -20,13 +20,7 @@ export class TestTerminal extends Terminal { } export class MockTerminal implements ITerminal { - loadAddon(addonConstructor: ITerminalAddonConstructor): T { - throw new Error('Method not implemented.'); - } - disposeAddon(addonConstructor: ITerminalAddonConstructor): void { - throw new Error('Method not implemented.'); - } - getAddon(addonConstructor: ITerminalAddonConstructor): T { + loadAddon(addon: ITerminalAddon): void { throw new Error('Method not implemented.'); } markers: IMarker[]; diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index fddd8982..830ed298 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -772,22 +772,18 @@ declare module 'xterm' { */ static applyAddon(addon: any): void; - loadAddon(addonConstructor: ITerminalAddonConstructor): T; - disposeAddon(addonConstructor: ITerminalAddonConstructor): void; - getAddon(addonConstructor: ITerminalAddonConstructor): T; - } - - export interface ITerminalAddonConstructor { - new(terminal: Terminal): T; + /** + * Loads an addon into this instance of xterm.js. + * @param addon The addon to load. + */ + loadAddon(addon: ITerminalAddon): void; } export interface ITerminalAddon { /** - * This property declares all addon dependencies that must be intialized - * before this addon can be constructed. For addons with no dependencies - * just don't include this property. + * This is called when the addon is activated within xterm.js. */ - // readonly DEPENDENCIES?: ITerminalAddonConstructor[]; + activate(terminal: Terminal): void; /** * This function includes anything that needs to happen to clean up when diff --git a/yarn.lock b/yarn.lock index 1fa1739c..4ca95193 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7122,15 +7122,15 @@ xregexp@4.0.0: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= -xterm-addon-attach@0.1.0-beta4: - version "0.1.0-beta4" - resolved "https://registry.yarnpkg.com/xterm-addon-attach/-/xterm-addon-attach-0.1.0-beta4.tgz#546010f66533f22bfad7605345e44ed95d90a1de" - integrity sha512-HwxNoNS1Fxoo6+MPZJ+5+sMTQHrZEcptL4qstHlaERqxL7ei/lvKMpSRfIo1eRNqxL4vHzYkNWJO7QLATmdalA== +xterm-addon-attach@0.1.0-beta7: + version "0.1.0-beta7" + resolved "https://registry.yarnpkg.com/xterm-addon-attach/-/xterm-addon-attach-0.1.0-beta7.tgz#787f6cce709611ee08ab731b95a62fa1c0bce6a9" + integrity sha512-nQr6LcYtpZcyDoHyL/BDIPJcTgL7qlHR/rvm8lSizQysGVT0pSzr5M7SjY3kQHw33U3hTer3c6oZzwjfj4ohOw== -xterm-addon-web-links@0.1.0-beta3: - version "0.1.0-beta3" - resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.1.0-beta3.tgz#bd2d45d399340bd1b5bbf44850a0be9c91a1451e" - integrity sha512-nkgwAYZXS97zL650MTl6RnA/iXYAD0yOVf/28+ZTlLQZJVYH2DP22rhOK0aqO+tWX91WUrvkcqxFCY965fomaQ== +xterm-addon-web-links@0.1.0-beta6: + version "0.1.0-beta6" + resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.1.0-beta6.tgz#9b4e862be8928ef455a667745bea479665db6c6b" + integrity sha512-tkVU5wCfBFjXwfOvcbMHoLoMDANztkwSREiKyu2R059kEF+sP67Z33HzxVCXUWFuCmutcx40xR2O0BK68gXZlg== y18n@^3.2.0, y18n@^3.2.1: version "3.2.1" From 267071ead65b1858403bce3baa8db28ab535f0aa Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 7 Apr 2019 15:00:27 -0400 Subject: [PATCH 08/42] Add deprecation message to applyAddon --- typings/xterm.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 830ed298..1d1ebad9 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -769,6 +769,7 @@ declare module 'xterm' { * Applies an addon to the Terminal prototype, making it available to all * newly created Terminals. * @param addon The addon to apply. + * @deprecated Use the new loadAddon API/addon format. */ static applyAddon(addon: any): void; From 518e2734bacf8fdb7f84e0e745e9ad7ed09151f4 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 10 May 2019 20:27:53 -0700 Subject: [PATCH 09/42] Re-introduce original attach and webLinks addons back We could release this in v3 if we don't break addons like this --- src/addons/attach/Interfaces.ts | 23 +++ src/addons/attach/attach.test.ts | 20 +++ src/addons/attach/attach.ts | 157 ++++++++++++++++++++ src/addons/attach/index.html | 93 ++++++++++++ src/addons/attach/package.json | 5 + src/addons/attach/tsconfig.json | 19 +++ src/addons/webLinks/package.json | 5 + src/addons/webLinks/tsconfig.json | 22 +++ src/addons/webLinks/webLinks.test.ts | 212 +++++++++++++++++++++++++++ src/addons/webLinks/webLinks.ts | 47 ++++++ src/public/Terminal.test.ts | 17 +++ src/tsconfig.all.json | 2 + 12 files changed, 622 insertions(+) create mode 100644 src/addons/attach/Interfaces.ts create mode 100644 src/addons/attach/attach.test.ts create mode 100644 src/addons/attach/attach.ts create mode 100644 src/addons/attach/index.html create mode 100644 src/addons/attach/package.json create mode 100644 src/addons/attach/tsconfig.json create mode 100644 src/addons/webLinks/package.json create mode 100644 src/addons/webLinks/tsconfig.json create mode 100644 src/addons/webLinks/webLinks.test.ts create mode 100644 src/addons/webLinks/webLinks.ts create mode 100644 src/public/Terminal.test.ts diff --git a/src/addons/attach/Interfaces.ts b/src/addons/attach/Interfaces.ts new file mode 100644 index 00000000..4b269099 --- /dev/null +++ b/src/addons/attach/Interfaces.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2018 The xterm.js authors. All rights reserved. + * @license MIT + * + * Implements the attach method, that attaches the terminal to a WebSocket stream. + */ + +import { Terminal, IDisposable } from 'xterm'; + +export interface IAttachAddonTerminal extends Terminal { + _core: { + register(d: T): void; + }; + + __socket?: WebSocket; + __attachSocketBuffer?: string; + __dataListener?: IDisposable; + + __getMessage?(ev: MessageEvent): void; + __flushBuffer?(): void; + __pushToBuffer?(data: string): void; + __sendData?(data: string): void; +} diff --git a/src/addons/attach/attach.test.ts b/src/addons/attach/attach.test.ts new file mode 100644 index 00000000..e280b656 --- /dev/null +++ b/src/addons/attach/attach.test.ts @@ -0,0 +1,20 @@ +/** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; + +import * as attach from './attach'; + +class MockTerminal {} + +describe('attach addon', () => { + describe('apply', () => { + it('should do register the `attach` and `detach` methods', () => { + attach.apply(MockTerminal); + assert.equal(typeof (MockTerminal).prototype.attach, 'function'); + assert.equal(typeof (MockTerminal).prototype.detach, 'function'); + }); + }); +}); diff --git a/src/addons/attach/attach.ts b/src/addons/attach/attach.ts new file mode 100644 index 00000000..2c8a5d4d --- /dev/null +++ b/src/addons/attach/attach.ts @@ -0,0 +1,157 @@ +/** + * Copyright (c) 2014 The xterm.js authors. All rights reserved. + * @license MIT + * + * Implements the attach method, that attaches the terminal to a WebSocket stream. + */ + +import { Terminal, IDisposable } from 'xterm'; +import { IAttachAddonTerminal } from './Interfaces'; + +/** + * Attaches the given terminal to the given socket. + * + * @param term The terminal to be attached to the given socket. + * @param socket The socket to attach the current terminal. + * @param bidirectional Whether the terminal should send data to the socket as well. + * @param buffered Whether the rendering of incoming data should happen instantly or at a maximum + * frequency of 1 rendering per 10ms. + */ +export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean, buffered: boolean): void { + const addonTerminal = term; + bidirectional = (typeof bidirectional === 'undefined') ? true : bidirectional; + addonTerminal.__socket = socket; + + addonTerminal.__flushBuffer = () => { + addonTerminal.write(addonTerminal.__attachSocketBuffer); + addonTerminal.__attachSocketBuffer = null; + }; + + addonTerminal.__pushToBuffer = (data: string) => { + if (addonTerminal.__attachSocketBuffer) { + addonTerminal.__attachSocketBuffer += data; + } else { + addonTerminal.__attachSocketBuffer = data; + setTimeout(addonTerminal.__flushBuffer, 10); + } + }; + + // TODO: This should be typed but there seem to be issues importing the type + let myTextDecoder: any; + + addonTerminal.__getMessage = function(ev: MessageEvent): void { + let str: string; + + if (typeof ev.data === 'object') { + if (!myTextDecoder) { + myTextDecoder = new TextDecoder(); + } + if (ev.data instanceof ArrayBuffer) { + str = myTextDecoder.decode(ev.data); + displayData(str); + } else { + const fileReader = new FileReader(); + + fileReader.addEventListener('load', () => { + str = myTextDecoder.decode(fileReader.result); + displayData(str); + }); + fileReader.readAsArrayBuffer(ev.data); + } + } else if (typeof ev.data === 'string') { + displayData(ev.data); + } else { + throw Error(`Cannot handle "${typeof ev.data}" websocket message.`); + } + }; + + /** + * Push data to buffer or write it in the terminal. + * This is used as a callback for FileReader.onload. + * + * @param str String decoded by FileReader. + * @param data The data of the EventMessage. + */ + function displayData(str?: string, data?: string): void { + if (buffered) { + addonTerminal.__pushToBuffer(str || data); + } else { + addonTerminal.write(str || data); + } + } + + addonTerminal.__sendData = (data: string) => { + if (socket.readyState !== 1) { + return; + } + socket.send(data); + }; + + addonTerminal._core.register(addSocketListener(socket, 'message', addonTerminal.__getMessage)); + + if (bidirectional) { + addonTerminal.__dataListener = addonTerminal.onData(addonTerminal.__sendData); + addonTerminal._core.register(addonTerminal.__dataListener); + } + + addonTerminal._core.register(addSocketListener(socket, 'close', () => detach(addonTerminal, socket))); + addonTerminal._core.register(addSocketListener(socket, 'error', () => detach(addonTerminal, socket))); +} + +function addSocketListener(socket: WebSocket, type: string, handler: (this: WebSocket, ev: Event) => any): IDisposable { + socket.addEventListener(type, handler); + return { + dispose: () => { + if (!handler) { + // Already disposed + return; + } + socket.removeEventListener(type, handler); + handler = null; + } + }; +} + +/** + * Detaches the given terminal from the given socket + * + * @param term The terminal to be detached from the given socket. + * @param socket The socket from which to detach the current terminal. + */ +export function detach(term: Terminal, socket: WebSocket): void { + const addonTerminal = term; + addonTerminal.__dataListener.dispose(); + addonTerminal.__dataListener = undefined; + + socket = (typeof socket === 'undefined') ? addonTerminal.__socket : socket; + + if (socket) { + socket.removeEventListener('message', addonTerminal.__getMessage); + } + + delete addonTerminal.__socket; +} + + +export function apply(terminalConstructor: typeof Terminal): void { + /** + * Attaches the current terminal to the given socket + * + * @param socket The socket to attach the current terminal. + * @param bidirectional Whether the terminal should send data to the socket as well. + * @param buffered Whether the rendering of incoming data should happen instantly or at a maximum + * frequency of 1 rendering per 10ms. + */ + (terminalConstructor.prototype).attach = function (socket: WebSocket, bidirectional: boolean, buffered: boolean): void { + attach(this, socket, bidirectional, buffered); + }; + + /** + * Detaches the current terminal from the given socket. + * + * @param socket The socket from which to detach the current terminal. + */ + (terminalConstructor.prototype).detach = function (socket: WebSocket): void { + detach(this, socket); + }; +} diff --git a/src/addons/attach/index.html b/src/addons/attach/index.html new file mode 100644 index 00000000..b6f853be --- /dev/null +++ b/src/addons/attach/index.html @@ -0,0 +1,93 @@ + + + + + + + + + + +
+ +

+ xterm.js: socket attach +

+

+ Attach the terminal to a WebSocket terminal stream with ease. Perfect for attaching to your + Docker containers. +

+

+ Socket information +

+
+ + +
+
+ +
+ + + \ No newline at end of file diff --git a/src/addons/attach/package.json b/src/addons/attach/package.json new file mode 100644 index 00000000..9e45068b --- /dev/null +++ b/src/addons/attach/package.json @@ -0,0 +1,5 @@ +{ + "name": "xterm.attach", + "main": "attach.js", + "private": true +} diff --git a/src/addons/attach/tsconfig.json b/src/addons/attach/tsconfig.json new file mode 100644 index 00000000..2f39102c --- /dev/null +++ b/src/addons/attach/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "lib": [ + "dom", + "es6", + ], + "rootDir": ".", + "outDir": "../../../lib/addons/attach/", + "sourceMap": true, + "removeComments": true, + "declaration": true + }, + "include": [ + "**/*.ts", + "../../../typings/xterm.d.ts" + ] +} diff --git a/src/addons/webLinks/package.json b/src/addons/webLinks/package.json new file mode 100644 index 00000000..f200cab4 --- /dev/null +++ b/src/addons/webLinks/package.json @@ -0,0 +1,5 @@ +{ + "name": "xterm.weblinks", + "main": "weblinks.js", + "private": true +} diff --git a/src/addons/webLinks/tsconfig.json b/src/addons/webLinks/tsconfig.json new file mode 100644 index 00000000..9c4f1176 --- /dev/null +++ b/src/addons/webLinks/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es5", + "lib": [ + "dom", + "es5", + ], + "rootDir": ".", + "outDir": "../../../lib/addons/webLinks/", + "sourceMap": true, + "removeComments": true, + "declaration": true, + "types": [ + "../../node_modules/@types/mocha" + ] + }, + "include": [ + "**/*.ts", + "../../../typings/xterm.d.ts" + ] +} diff --git a/src/addons/webLinks/webLinks.test.ts b/src/addons/webLinks/webLinks.test.ts new file mode 100644 index 00000000..da5569ab --- /dev/null +++ b/src/addons/webLinks/webLinks.test.ts @@ -0,0 +1,212 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; + +import * as webLinks from './webLinks'; + +class MockTerminal { + public regex: RegExp; + public handler: (event: MouseEvent, uri: string) => void; + public options?: any; + + public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: any): number { + this.regex = regex; + this.handler = handler; + this.options = options; + return 0; + } +} + +describe('webLinks addon', () => { + describe('apply', () => { + it('should do register the `webLinksInit` method', () => { + webLinks.apply(MockTerminal); + assert.equal(typeof (MockTerminal).prototype.webLinksInit, 'function'); + }); + }); + + describe('should allow simple URI path', () => { + it('foo.com', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://foo.com '; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com'); + }); + + it('bar.io', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://bar.io '; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://bar.io'); + }); + }); + + describe('should allow ~ character in URI path', () => { + it('foo.com', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://foo.com/a~b#c~d?e~f '; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/a~b#c~d?e~f'); + }); + + it('bar.io', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://bar.io/a~b#c~d?e~f '; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://bar.io/a~b#c~d?e~f'); + }); + }); + + describe('should allow : character in URI path', () => { + it('foo.com', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://foo.com/colon:test '; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/colon:test'); + }); + + it('bar.io', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://bar.io/colon:test '; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://bar.io/colon:test'); + }); + }); + + describe('should not allow : character at the end of a URI path', () => { + it('foo.com', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://foo.com/colon:test: '; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/colon:test'); + }); + + it('bar.io', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = ' http://bar.io/colon:test: '; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://bar.io/colon:test'); + }); + }); + + describe('should not allow " character at the end of a URI enclosed with ""', () => { + it('foo.com', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = '"http://foo.com/"'; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/'); + }); + + it('bar.io', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = '"http://bar.io/"'; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://bar.io/'); + }); + }); + + describe('should not allow \' character at the end of a URI enclosed with \'\'', () => { + it('foo.com', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = '\'http://foo.com/\''; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/'); + }); + + it('bar.io', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = '\'http://bar.io/\''; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://bar.io/'); + }); + }); + + describe('should allow + character in URI path', () => { + it('foo.com', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = 'http://foo.com/subpath/+/id'; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/subpath/+/id'); + }); + + it('bar.io', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = 'http://bar.io/subpath/+/id'; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://bar.io/subpath/+/id'); + }); + }); +}); diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts new file mode 100644 index 00000000..8a0fec09 --- /dev/null +++ b/src/addons/webLinks/webLinks.ts @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2017 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { Terminal, ILinkMatcherOptions } from 'xterm'; + +const protocolClause = '(https?:\\/\\/)'; +const domainCharacterSet = '[\\da-z\\.-]+'; +const negatedDomainCharacterSet = '[^\\da-z\\.-]+'; +const domainBodyClause = '(' + domainCharacterSet + ')'; +const tldClause = '([a-z\\.]{2,6})'; +const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})'; +const localHostClause = '(localhost)'; +const portClause = '(:\\d{1,5})'; +const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?'; +const pathCharacterSet = '(\\/[\\/\\w\\.\\-%~:+]*)*([^:"\'\\s])'; +const pathClause = '(' + pathCharacterSet + ')?'; +const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*'; +const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; +const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; +const negatedPathCharacterSet = '[^\\/\\w\\.\\-%]+'; +const bodyClause = hostClause + pathClause + queryStringClause + hashFragmentClause; +const start = '(?:^|' + negatedDomainCharacterSet + ')('; +const end = ')($|' + negatedPathCharacterSet + ')'; +const strictUrlRegex = new RegExp(start + protocolClause + bodyClause + end); + +function handleLink(event: MouseEvent, uri: string): void { + window.open(uri, '_blank'); +} + +/** + * Initialize the web links addon, registering the link matcher. + * @param term The terminal to use web links within. + * @param handler A custom handler to use. + * @param options Custom options to use, matchIndex will always be ignored. + */ +export function webLinksInit(term: Terminal, handler: (event: MouseEvent, uri: string) => void = handleLink, options: ILinkMatcherOptions = {}): void { + options.matchIndex = 1; + term.registerLinkMatcher(strictUrlRegex, handler, options); +} + +export function apply(terminalConstructor: typeof Terminal): void { + (terminalConstructor.prototype).webLinksInit = function (handler?: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): void { + webLinksInit(this, handler, options); + }; +} diff --git a/src/public/Terminal.test.ts b/src/public/Terminal.test.ts new file mode 100644 index 00000000..6bad5b04 --- /dev/null +++ b/src/public/Terminal.test.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2016 The xterm.js authors. All rights reserved. + * @license MIT + */ + +import { assert } from 'chai'; +import { Terminal } from './Terminal'; +import * as attach from '../addons/attach/attach'; + + describe('Terminal', () => { + it('should apply addons with Terminal.applyAddon', () => { + Terminal.applyAddon(attach); + // Test that addon was applied successfully, adding attach to Terminal's + // prototype. + assert.equal(typeof (Terminal).prototype.attach, 'function'); + }); +}); diff --git a/src/tsconfig.all.json b/src/tsconfig.all.json index 55689ad8..2a53ab89 100644 --- a/src/tsconfig.all.json +++ b/src/tsconfig.all.json @@ -3,10 +3,12 @@ "include": [], "references": [ { "path": "." }, + { "path": "./addons/attach" }, { "path": "./addons/fit" }, { "path": "./addons/fullscreen" }, { "path": "./addons/search" }, { "path": "./addons/terminado" }, + { "path": "./addons/webLinks" }, { "path": "./addons/zmodem" } ] } From d6960469216d827c94f72c3e81ab3f389a8214c8 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Fri, 10 May 2019 20:38:08 -0700 Subject: [PATCH 10/42] Simplify and mark new addon API as experimental --- typings/xterm.d.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index cd7bcf0d..817da6ca 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -858,22 +858,16 @@ declare module 'xterm' { static applyAddon(addon: any): void; /** - * Loads an addon into this instance of xterm.js. + * (EXPERIMENTAL) Loads an addon into this instance of xterm.js. * @param addon The addon to load. */ loadAddon(addon: ITerminalAddon): void; } - export interface ITerminalAddon { + export interface ITerminalAddon extends IDisposable { /** - * This is called when the addon is activated within xterm.js. + * (EXPERIMENTAL) This is called when the addon is activated within xterm.js. */ activate(terminal: Terminal): void; - - /** - * This function includes anything that needs to happen to clean up when - * the addon is being disposed. - */ - dispose(): void; } } From 8a929ed1422f77a8b666bfdafb8a53efd89a1c97 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 11 May 2019 11:02:15 -0700 Subject: [PATCH 11/42] Add API tests for loadAddon --- src/Terminal.ts | 1 + src/public/Terminal.api.ts | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/src/Terminal.ts b/src/Terminal.ts index 6b74dfbb..55c5c062 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -275,6 +275,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } public dispose(): void { + this._addonManager.dispose(); super.dispose(); if (this._windowsMode) { this._windowsMode.dispose(); diff --git a/src/public/Terminal.api.ts b/src/public/Terminal.api.ts index 380cc89c..e56e3535 100644 --- a/src/public/Terminal.api.ts +++ b/src/public/Terminal.api.ts @@ -111,6 +111,52 @@ describe('API Integration Tests', () => { assert.equal(await page.evaluate(`document.activeElement.className`), ''); }); + describe('loadAddon', () => { + it('constructor', async function(): Promise { + this.timeout(10000); + await openTerminal({ cols: 5 }); + await page.evaluate(` + window.cols = 0; + window.term.loadAddon({ + activate: (t) => window.cols = t.cols, + dispose: () => {} + }); + `); + assert.equal(await page.evaluate(`window.cols`), 5); + }); + + it('dispose (addon)', async function(): Promise { + this.timeout(10000); + await openTerminal(); + await page.evaluate(` + window.disposeCalled = false + window.addon = { + activate: () => {}, + dispose: () => window.disposeCalled = true + }; + window.term.loadAddon(window.addon); + `); + assert.equal(await page.evaluate(`window.disposeCalled`), false); + await page.evaluate(`window.addon.dispose()`); + assert.equal(await page.evaluate(`window.disposeCalled`), true); + }); + + it('dispose (terminal)', async function(): Promise { + this.timeout(10000); + await openTerminal(); + await page.evaluate(` + window.disposeCalled = false + window.term.loadAddon({ + activate: () => {}, + dispose: () => window.disposeCalled = true + }); + `); + assert.equal(await page.evaluate(`window.disposeCalled`), false); + await page.evaluate(`window.term.dispose()`); + assert.equal(await page.evaluate(`window.disposeCalled`), true); + }); + }); + describe('Events', () => { it('onCursorMove', async function(): Promise { this.timeout(10000); From 7a567d27177615f7abfad5ae47f6a88f1c1c9e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 11 May 2019 23:10:18 +0200 Subject: [PATCH 12/42] apply time-based limit --- src/Terminal.ts | 81 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 74 insertions(+), 7 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 2cb1dcf0..391cd991 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -183,6 +183,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // user input states public writeBuffer: string[]; + public writeBufferUtf8: Uint8Array[]; private _writeInProgress: boolean; /** @@ -340,6 +341,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // user input states this.writeBuffer = []; + this.writeBufferUtf8 = []; this._writeInProgress = false; this._xoffSentToCatchUp = false; @@ -1366,20 +1368,85 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } /** - * Writes utf8 data to the terminal. - * TODO: This currently does no flow control. + * Writes raw utf8 bytes to the terminal. + * @param data The text to write to the terminal. */ public writeUtf8(data: Uint8Array): void { + // Ensure the terminal isn't disposed if (this._isDisposed) { return; } - this._refreshStart = this.buffer.y; - this._refreshEnd = this.buffer.y; - this._inputHandler.parseUtf8(data); + // Ignore falsy data values (including the empty string) + if (!data) { + return; + } - this.updateRange(this.buffer.y); - this.refresh(this._refreshStart, this._refreshEnd); + this.writeBufferUtf8.push(data); + + // Send XOFF to pause the pty process if the write buffer becomes too large so + // xterm.js can catch up before more data is sent. This is necessary in order + // to keep signals such as ^C responsive. + if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBufferUtf8.length >= WRITE_BUFFER_PAUSE_THRESHOLD) { + // XOFF - stop pty pipe + // XON will be triggered by emulator before processing data chunk + this.handler(C0.DC3); + this._xoffSentToCatchUp = true; + } + + if (!this._writeInProgress && this.writeBufferUtf8.length > 0) { + // Kick off a write which will write all data in sequence recursively + this._writeInProgress = true; + // Kick off an async innerWrite so more writes can come in while processing data + setTimeout(() => { + this._innerWriteUtf8(); + }); + } + } + + protected _innerWriteUtf8(bufferOffset: number = 0): void { + // Ensure the terminal isn't disposed + if (this._isDisposed) { + this.writeBufferUtf8 = []; + } + + const startTime = Date.now(); + while (this.writeBufferUtf8.length > bufferOffset) { + const data = this.writeBufferUtf8[bufferOffset]; + bufferOffset++; + + // If XOFF was sent in order to catch up with the pty process, resume it if + // we reached the end of the writeBuffer to allow more data to come in. + if (this._xoffSentToCatchUp && this.writeBufferUtf8.length === bufferOffset) { + this.handler(C0.DC1); + this._xoffSentToCatchUp = false; + } + + this._refreshStart = this.buffer.y; + this._refreshEnd = this.buffer.y; + + // HACK: Set the parser state based on it's state at the time of return. + // This works around the bug #662 which saw the parser state reset in the + // middle of parsing escape sequence in two chunks. For some reason the + // state of the parser resets to 0 after exiting parser.parse. This change + // just sets the state back based on the correct return statement. + + this._inputHandler.parseUtf8(data); + + this.updateRange(this.buffer.y); + this.refresh(this._refreshStart, this._refreshEnd); + + if (Date.now() - startTime >= WRITE_TIMEOUT_MS) { + break; + } + } + if (this.writeBufferUtf8.length > bufferOffset) { + // Allow renderer to catch up before processing the next batch + setTimeout(() => this._innerWriteUtf8(bufferOffset), 0); + } else { + this._writeInProgress = false; + this.writeBufferUtf8 = []; + } } /** From ce079f399c89ba059797b582a8045686a0a0573f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 12 May 2019 00:07:07 +0200 Subject: [PATCH 13/42] fix docstring --- src/Terminal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 391cd991..383b73f8 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1369,7 +1369,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II /** * Writes raw utf8 bytes to the terminal. - * @param data The text to write to the terminal. + * @param data UintArray with UTF8 bytes to write to the terminal. */ public writeUtf8(data: Uint8Array): void { // Ensure the terminal isn't disposed From 2340fcd071975ced2ed7451b89773ff483a07b44 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 11 May 2019 21:21:36 -0700 Subject: [PATCH 14/42] Fix compile and update yarn.lock with utf8 --- src/Types.ts | 1 + yarn.lock | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/src/Types.ts b/src/Types.ts index b6b8ac86..84927a65 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -263,6 +263,7 @@ export interface IPublicTerminal extends IDisposable, IEventEmitter { scrollToLine(line: number): void; clear(): void; write(data: string): void; + writeUtf8(data: Uint8Array): void; getOption(key: string): any; setOption(key: string, value: any): void; refresh(start: number, end: number): void; diff --git a/yarn.lock b/yarn.lock index e14ad3a1..87ca4a18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -93,6 +93,11 @@ dependencies: source-map "^0.6.1" +"@types/utf8@^2.1.6": + version "2.1.6" + resolved "https://registry.yarnpkg.com/@types/utf8/-/utf8-2.1.6.tgz#430cabb71a42d0a3613cce5621324fe4f5a25753" + integrity sha512-pRs2gYF5yoKYrgSaira0DJqVg2tFuF+Qjp838xS7K+mJyY2jJzjsrl6y17GbIa4uMRogMbxs+ghNCvKg6XyNrA== + "@types/webpack@^4.4.11": version "4.4.11" resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.4.11.tgz#0ca832870d55c4e92498c01d22d00d02b0f62ae9" @@ -6823,6 +6828,11 @@ user-home@^1.1.1: resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190" integrity sha1-K1viOjK2Onyd640PKNSFcko98ZA= +utf8@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1" + integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ== + util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" From 00423407c189a8e7e9e6e2a4736d6b579442ddff Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 11 May 2019 22:08:25 -0700 Subject: [PATCH 15/42] Clean up --- src/core/input/TextDecoder.test.ts | 3 --- src/core/input/TextDecoder.ts | 4 ---- typings/xterm.d.ts | 14 +++++++------- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/src/core/input/TextDecoder.test.ts b/src/core/input/TextDecoder.test.ts index 68eb1d9b..dc358bc5 100644 --- a/src/core/input/TextDecoder.test.ts +++ b/src/core/input/TextDecoder.test.ts @@ -28,7 +28,6 @@ function fromByteString(s: string): Uint8Array { return result; } - const TEST_STRINGS = [ 'Лорем ипсум долор сит амет, ех сеа аццусам диссентиет. Ан еос стет еирмод витуперата. Иус дицерет урбанитас ет. Ан при алтера долорес сплендиде, цу яуо интегре денияуе, игнота волуптариа инструцтиор цу вим.', 'ლორემ იფსუმ დოლორ სით ამეთ, ფაცერ მუციუს ცონსეთეთურ ყუო იდ, ფერ ვივენდუმ ყუაერენდუმ ეა, ესთ ამეთ მოვეთ სუავითათე ცუ. ვითაე სენსიბუს ან ვიხ. ეხერცი დეთერრუისსეთ უთ ყუი. ვოცენთ დებითის ადიფისცი ეთ ფერ. ნეც ან ფეუგაით ფორენსიბუს ინთერესსეთ. იდ დიცო რიდენს იუს. დისსენთიეთ ცონსეყუუნთურ სედ ნე, ნოვუმ მუნერე ეუმ ათ, ნე ეუმ ნიჰილ ირაცუნდია ურბანითას.', @@ -41,7 +40,6 @@ const TEST_STRINGS = [ 'Лорем ლორემ अधिकांश 覧六子 八メル 모든 בקרבת 💮 😂 äggg 123€ 𝄞.' ]; - describe('text encodings', () => { it('stringFromCodePoint/utf32ToString', () => { const s = 'abcdefg'; @@ -110,7 +108,6 @@ describe('text encodings', () => { assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€'); }); }); - }); describe('Utf8ToUtf32 decoder', () => { diff --git a/src/core/input/TextDecoder.ts b/src/core/input/TextDecoder.ts index b9ac19ce..75029822 100644 --- a/src/core/input/TextDecoder.ts +++ b/src/core/input/TextDecoder.ts @@ -3,7 +3,6 @@ * @license MIT */ - /** * Polyfill - Convert UTF32 codepoint into JS string. * Note: The built-in String.fromCodePoint happens to be much slower @@ -19,7 +18,6 @@ export function stringFromCodePoint(codePoint: number): string { return String.fromCharCode(codePoint); } - /** * Convert UTF32 char codes into JS string. * Basically the same as `stringFromCodePoint` but for multiple codepoints @@ -44,7 +42,6 @@ export function utf32ToString(data: Uint32Array, start: number = 0, end: number return result; } - /** * StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints. * To keep the decoder in line with JS strings it handles single surrogates as UCS2. @@ -211,7 +208,6 @@ export class Utf8ToUtf32 { const fourStop = length - 4; let i = startPos; while (i < length) { - /** * ASCII shortcut with loop unrolled to 4 consecutive ASCII chars. * This is a compromise between speed gain for ASCII diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 2edad79a..b3bb582e 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -553,12 +553,6 @@ declare module 'xterm' { */ resize(columns: number, rows: number): void; - /** - * Writes text to the terminal, followed by a break line character (\n). - * @param data The text to write to the terminal. - */ - writeln(data: string): void; - /** * Opens the terminal within an element. * @param parent The element to create the terminal within. This element @@ -746,7 +740,13 @@ declare module 'xterm' { write(data: string): void; /** - * Writes UTF8 data to the terminal. + * Writes text to the terminal, followed by a break line character (\n). + * @param data The text to write to the terminal. + */ + writeln(data: string): void; + + /** + * Writes text to the terminal encoded as UTF-8 to the terminal. * @param data The data to write to the terminal. */ writeUtf8(data: Uint8Array): void; From e5dfc5603040feb469d8ca801551b37b01b1cf7a Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 11 May 2019 22:24:33 -0700 Subject: [PATCH 16/42] Add api test for writeUtf8 --- src/public/Terminal.api.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/public/Terminal.api.ts b/src/public/Terminal.api.ts index 031e03d5..57645cf1 100644 --- a/src/public/Terminal.api.ts +++ b/src/public/Terminal.api.ts @@ -62,6 +62,20 @@ describe('API Integration Tests', () => { assert.equal(await page.evaluate(`window.term.buffer.getLine(1).translateToString(true)`), 'bar'); }); + it.only('writeUtf8', async function(): Promise { + this.timeout(10000); + await openTerminal(); + await page.evaluate(` + // foo + window.term.writeUtf8(new Uint8Array([102, 111, 111])); + // bar + window.term.writeUtf8(new Uint8Array([98, 97, 114])); + // 文 + window.term.writeUtf8(new Uint8Array([230, 150, 135])); + `); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文'); + }); + it('clear', async function(): Promise { this.timeout(10000); await openTerminal({ rows: 5 }); From c13d6e5eeb6287f78962dce20a24dbb966eaec02 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 11 May 2019 22:29:01 -0700 Subject: [PATCH 17/42] Make write and writeln consistent with writeUtf8 test --- src/public/Terminal.api.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/public/Terminal.api.ts b/src/public/Terminal.api.ts index 57645cf1..5d124532 100644 --- a/src/public/Terminal.api.ts +++ b/src/public/Terminal.api.ts @@ -47,8 +47,9 @@ describe('API Integration Tests', () => { await page.evaluate(` window.term.write('foo'); window.term.write('bar'); + window.term.write('文'); `); - assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar'); + assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文'); }); it('writeln', async function(): Promise { @@ -57,12 +58,14 @@ describe('API Integration Tests', () => { await page.evaluate(` window.term.writeln('foo'); window.term.writeln('bar'); + window.term.writeln('文'); `); assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo'); assert.equal(await page.evaluate(`window.term.buffer.getLine(1).translateToString(true)`), 'bar'); + assert.equal(await page.evaluate(`window.term.buffer.getLine(2).translateToString(true)`), '文'); }); - it.only('writeUtf8', async function(): Promise { + it('writeUtf8', async function(): Promise { this.timeout(10000); await openTerminal(); await page.evaluate(` From 780e924b213373e23f03720225e99c8017695d03 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 11 May 2019 23:15:04 -0700 Subject: [PATCH 18/42] Use stable API for buffer in search addon --- src/addons/search/Interfaces.ts | 1 - src/addons/search/SearchHelper.ts | 34 +++++++++++++++---------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts index a1f05895..d99bd271 100644 --- a/src/addons/search/Interfaces.ts +++ b/src/addons/search/Interfaces.ts @@ -7,7 +7,6 @@ import { Terminal } from 'xterm'; // TODO: Don't rely on this private API export interface ITerminalCore { - buffer: any; selectionManager: any; } diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 7db1ed43..ce3d0004 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -45,7 +45,7 @@ export class SearchHelper implements ISearchHelper { } let startCol: number = 0; - let startRow = this._terminal._core.buffer.ydisp; + let startRow = this._terminal.buffer.viewportY; if (selectionManager.selectionEnd) { // Start from the selection end if there is a selection @@ -64,7 +64,7 @@ export class SearchHelper implements ISearchHelper { let cumulativeCols = startCol; // If startRow is wrapped row, scan for unwrapped row above. // So we can start matching on wrapped line from long unwrapped line. - while (this._terminal._core.buffer.lines.get(findingRow).isWrapped) { + while (this._terminal.buffer.getLine(findingRow).isWrapped) { findingRow--; cumulativeCols += this._terminal.cols; } @@ -75,7 +75,7 @@ export class SearchHelper implements ISearchHelper { // Search from startRow + 1 to end if (!result) { - for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { + for (let y = startRow + 1; y < this._terminal.buffer.baseY + this._terminal.rows; y++) { // If the current line is wrapped line, increase index of column to ignore the previous scan // Otherwise, reset beginning column index to zero with set new unwrapped line index @@ -118,7 +118,7 @@ export class SearchHelper implements ISearchHelper { } const isReverseSearch = true; - let startRow = this._terminal._core.buffer.ydisp + this._terminal.rows - 1; + let startRow = this._terminal.buffer.viewportY + this._terminal.rows - 1; let startCol = this._terminal.cols; if (selectionManager.selectionStart) { @@ -139,7 +139,7 @@ export class SearchHelper implements ISearchHelper { // If the line is wrapped line, increase number of columns that is needed to be scanned // Se we can scan on wrapped line from unwrapped line let cumulativeCols = this._terminal.cols; - if (this._terminal._core.buffer.lines.get(startRow).isWrapped) { + if (this._terminal.buffer.getLine(startRow).isWrapped) { cumulativeCols += startCol; } for (let y = startRow - 1; y >= 0; y--) { @@ -149,7 +149,7 @@ export class SearchHelper implements ISearchHelper { } // If the current line is wrapped line, increase scanning range, // preparing for scanning on unwrapped line - if (this._terminal._core.buffer.lines.get(y).isWrapped) { + if (this._terminal.buffer.getLine(y).isWrapped) { cumulativeCols += this._terminal.cols; } else { cumulativeCols = this._terminal.cols; @@ -160,14 +160,14 @@ export class SearchHelper implements ISearchHelper { // Search from the bottom to startRow (search the whole startRow again in // case startCol > 0) if (!result) { - const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1; + const searchFrom = this._terminal.buffer.baseY + this._terminal.rows - 1; let cumulativeCols = this._terminal.cols; for (let y = searchFrom; y >= startRow; y--) { result = this._findInLine(term, y, cumulativeCols, searchOptions, isReverseSearch); if (result) { break; } - if (this._terminal._core.buffer.lines.get(y).isWrapped) { + if (this._terminal.buffer.getLine(y).isWrapped) { cumulativeCols += this._terminal.cols; } else { cumulativeCols = this._terminal.cols; @@ -184,7 +184,7 @@ export class SearchHelper implements ISearchHelper { */ private _initLinesCache(): void { if (!this._linesCache) { - this._linesCache = new Array(this._terminal._core.buffer.length); + this._linesCache = new Array(this._terminal.buffer.length); this._cursorMoveListener = this._terminal.onCursorMove(() => this._destroyLinesCache()); this._resizeListener = this._terminal.onResize(() => this._destroyLinesCache()); } @@ -234,7 +234,7 @@ export class SearchHelper implements ISearchHelper { protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult { // Ignore wrapped lines, only consider on unwrapped line (first row of command string). - if (this._terminal._core.buffer.lines.get(row).isWrapped) { + if (this._terminal.buffer.getLine(row).isWrapped) { return; } let stringLine = this._linesCache ? this._linesCache[row] : void 0; @@ -286,18 +286,18 @@ export class SearchHelper implements ISearchHelper { return; } - const line = this._terminal._core.buffer.lines.get(row); + const line = this._terminal.buffer.getLine(row); for (let i = 0; i < resultIndex; i++) { - const charData = line.get(i); + const cell = line.getCell(i); // Adjust the searchIndex to normalize emoji into single chars - const char = charData[1/*CHAR_DATA_CHAR_INDEX*/]; + const char = cell.char; if (char.length > 1) { resultIndex -= char.length - 1; } // Adjust the searchIndex for empty characters following wide unicode // chars (eg. CJK) - const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/]; + const charWidth = cell.width; if (charWidth === 0) { resultIndex++; } @@ -322,9 +322,9 @@ export class SearchHelper implements ISearchHelper { let lineWrapsToNext: boolean; do { - const nextLine = this._terminal._core.buffer.lines.get(lineIndex + 1); + const nextLine = this._terminal.buffer.getLine(lineIndex + 1); lineWrapsToNext = nextLine ? nextLine.isWrapped : false; - lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight).substring(0, this._terminal.cols); + lineString += this._terminal.buffer.getLine(lineIndex).translateToString(!lineWrapsToNext && trimRight).substring(0, this._terminal.cols); lineIndex++; } while (lineWrapsToNext); @@ -342,7 +342,7 @@ export class SearchHelper implements ISearchHelper { return false; } this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length); - this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp); + this._terminal.scrollLines(result.row - this._terminal.buffer.viewportY); return true; } } From 161c7f9339dd714bb7b855b5c84f49d7a9e67eed Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 11 May 2019 23:22:46 -0700 Subject: [PATCH 19/42] Use API clearSelection in search addon --- src/addons/search/SearchHelper.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index ce3d0004..7f24a618 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -40,7 +40,7 @@ export class SearchHelper implements ISearchHelper { let result: ISearchResult; if (!term || term.length === 0) { - selectionManager.clearSelection(); + this._terminal.clearSelection(); return false; } @@ -113,7 +113,7 @@ export class SearchHelper implements ISearchHelper { let result: ISearchResult; if (!term || term.length === 0) { - selectionManager.clearSelection(); + this._terminal.clearSelection(); return false; } From 6674f05cc8eac07bde01c9a3185a475de835407c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 11 May 2019 23:35:31 -0700 Subject: [PATCH 20/42] Implement setSelection API Fixes #1443 --- src/Terminal.ts | 10 ++++++++++ src/TestUtils.test.ts | 3 +++ src/Types.ts | 1 + src/public/Terminal.ts | 3 +++ typings/xterm.d.ts | 8 ++++++++ 5 files changed, 25 insertions(+) diff --git a/src/Terminal.ts b/src/Terminal.ts index 6da60458..56205d1e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1535,6 +1535,16 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II return this.selectionManager ? this.selectionManager.hasSelection : false; } + /** + * Selects text within the terminal. + * @param column The column the selection starts at.. + * @param row The row the selection starts at. + * @param length The length of the selection. + */ + public setSelection(column: number, row: number, length: number): void { + this.selectionManager.setSelection(column, row, length); + } + /** * Gets the terminal's current selection, this is useful for implementing copy * behavior outside of xterm.js. diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 1d5008c8..f5008a43 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -80,6 +80,9 @@ export class MockTerminal implements ITerminal { hasSelection(): boolean { throw new Error('Method not implemented.'); } + setSelection(column: number, row: number, length: number): void { + throw new Error('Method not implemented.'); + } getSelection(): string { throw new Error('Method not implemented.'); } diff --git a/src/Types.ts b/src/Types.ts index 5841626b..238dbd41 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -249,6 +249,7 @@ export interface IPublicTerminal extends IDisposable, IEventEmitter { deregisterCharacterJoiner(joinerId: number): void; addMarker(cursorYOffset: number): IMarker; hasSelection(): boolean; + setSelection(column: number, row: number, length: number): void; getSelection(): string; clearSelection(): void; selectAll(): void; diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 0c6a7745..3f1cfd0c 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -96,6 +96,9 @@ export class Terminal implements ITerminalApi { public hasSelection(): boolean { return this._core.hasSelection(); } + public setSelection(column: number, row: number, length: number): void { + this._core.setSelection(column, row, length); + } public getSelection(): string { return this._core.getSelection(); } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 0714f55f..59eebc14 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -670,6 +670,14 @@ declare module 'xterm' { */ hasSelection(): boolean; + /** + * Selects text within the terminal. + * @param column The column the selection starts at.. + * @param row The row the selection starts at. + * @param length The length of the selection. + */ + setSelection(column: number, row: number, length: number): void; + /** * Gets the terminal's current selection, this is useful for implementing * copy behavior outside of xterm.js. From da7c39e5267cf4f198638b68713576ad57492d2c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 11 May 2019 23:37:42 -0700 Subject: [PATCH 21/42] Add API test --- src/public/Terminal.api.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/public/Terminal.api.ts b/src/public/Terminal.api.ts index 031e03d5..772ffdc3 100644 --- a/src/public/Terminal.api.ts +++ b/src/public/Terminal.api.ts @@ -99,6 +99,9 @@ describe('API Integration Tests', () => { await page.evaluate(`window.term.clearSelection()`); assert.equal(await page.evaluate(`window.term.hasSelection()`), false); assert.equal(await page.evaluate(`window.term.getSelection()`), ''); + await page.evaluate(`window.term.setSelection(1, 2, 2)`) + assert.equal(await page.evaluate(`window.term.hasSelection()`), true); + assert.equal(await page.evaluate(`window.term.getSelection()`), 'oo'); }); it('focus, blur', async function(): Promise { From 5bbbbf7348e4f0e65afb271912a7def198769404 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 11 May 2019 23:52:38 -0700 Subject: [PATCH 22/42] Expose getSelectionPosition and select APIs instead --- src/Terminal.ts | 17 ++++++++++-- src/TestUtils.test.ts | 11 +++++--- src/Types.ts | 5 ++-- src/addons/search/SearchHelper.ts | 2 +- src/public/Terminal.api.ts | 2 +- src/public/Terminal.ts | 9 ++++-- typings/xterm.d.ts | 46 +++++++++++++++++++++++++------ 7 files changed, 71 insertions(+), 21 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 56205d1e..58e58b6e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -43,7 +43,7 @@ import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager'; import { MouseZoneManager } from './MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './ui/ScreenDprMonitor'; -import { ITheme, IMarker, IDisposable } from 'xterm'; +import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm'; import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache'; import { DomRenderer } from './renderer/dom/DomRenderer'; import { IKeyboardEvent } from './common/Types'; @@ -1541,7 +1541,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II * @param row The row the selection starts at. * @param length The length of the selection. */ - public setSelection(column: number, row: number, length: number): void { + public select(column: number, row: number, length: number): void { this.selectionManager.setSelection(column, row, length); } @@ -1553,6 +1553,19 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II return this.selectionManager ? this.selectionManager.selectionText : ''; } + public getSelectionPosition(): ISelectionPosition | undefined { + if (!this.selectionManager.hasSelection) { + return undefined; + } + + return { + startColumn: this.selectionManager.selectionStart[0], + startRow: this.selectionManager.selectionStart[1], + endColumn: this.selectionManager.selectionEnd[0], + endRow: this.selectionManager.selectionEnd[1] + }; + } + /** * Clears the current terminal selection. */ diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index f5008a43..3f53c7d3 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -9,7 +9,7 @@ import { IBufferLine, ICellData, IAttributeData } from './core/Types'; import { ICircularList, XtermListener } from './common/Types'; import { Buffer } from './Buffer'; import * as Browser from './common/Platform'; -import { ITheme, IDisposable, IMarker, IEvent } from 'xterm'; +import { ITheme, IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm'; import { Terminal } from './Terminal'; import { AttributeData } from './core/buffer/BufferLine'; @@ -80,15 +80,18 @@ export class MockTerminal implements ITerminal { hasSelection(): boolean { throw new Error('Method not implemented.'); } - setSelection(column: number, row: number, length: number): void { - throw new Error('Method not implemented.'); - } getSelection(): string { throw new Error('Method not implemented.'); } + getSelectionPosition(): ISelectionPosition | undefined { + throw new Error('Method not implemented.'); + } clearSelection(): void { throw new Error('Method not implemented.'); } + select(column: number, row: number, length: number): void { + throw new Error('Method not implemented.'); + } selectAll(): void { throw new Error('Method not implemented.'); } diff --git a/src/Types.ts b/src/Types.ts index 238dbd41..26a35814 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable, IMarker } from 'xterm'; +import { ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable, IMarker, ISelectionPosition } from 'xterm'; import { IColorSet, IRenderer } from './renderer/Types'; import { ICharset, IAttributeData, ICellData, IBufferLine, CharData } from './core/Types'; import { ICircularList } from './common/Types'; @@ -249,9 +249,10 @@ export interface IPublicTerminal extends IDisposable, IEventEmitter { deregisterCharacterJoiner(joinerId: number): void; addMarker(cursorYOffset: number): IMarker; hasSelection(): boolean; - setSelection(column: number, row: number, length: number): void; getSelection(): string; + getSelectionPosition(): ISelectionPosition | undefined; clearSelection(): void; + select(column: number, row: number, length: number): void; selectAll(): void; selectLines(start: number, end: number): void; dispose(): void; diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 7db1ed43..3dc99603 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -341,7 +341,7 @@ export class SearchHelper implements ISearchHelper { this._terminal.clearSelection(); return false; } - this._terminal._core.selectionManager.setSelection(result.col, result.row, result.term.length); + this._terminal.select(result.col, result.row, result.term.length); this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp); return true; } diff --git a/src/public/Terminal.api.ts b/src/public/Terminal.api.ts index 772ffdc3..3f289f84 100644 --- a/src/public/Terminal.api.ts +++ b/src/public/Terminal.api.ts @@ -99,7 +99,7 @@ describe('API Integration Tests', () => { await page.evaluate(`window.term.clearSelection()`); assert.equal(await page.evaluate(`window.term.hasSelection()`), false); assert.equal(await page.evaluate(`window.term.getSelection()`), ''); - await page.evaluate(`window.term.setSelection(1, 2, 2)`) + 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'); }); diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 3f1cfd0c..96406919 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm'; +import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, ISelectionPosition } from 'xterm'; import { ITerminal, IBuffer } from '../Types'; import { IBufferLine } from '../core/Types'; import { Terminal as TerminalCore } from '../Terminal'; @@ -96,12 +96,15 @@ export class Terminal implements ITerminalApi { public hasSelection(): boolean { return this._core.hasSelection(); } - public setSelection(column: number, row: number, length: number): void { - this._core.setSelection(column, row, length); + public select(column: number, row: number, length: number): void { + this._core.select(column, row, length); } public getSelection(): string { return this._core.getSelection(); } + public getSelectionPosition(): ISelectionPosition | undefined { + return this._core.getSelectionPosition(); + } public clearSelection(): void { this._core.clearSelection(); } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 59eebc14..466ddb6d 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -670,25 +670,30 @@ declare module 'xterm' { */ hasSelection(): boolean; - /** - * Selects text within the terminal. - * @param column The column the selection starts at.. - * @param row The row the selection starts at. - * @param length The length of the selection. - */ - setSelection(column: number, row: number, length: number): void; - /** * Gets the terminal's current selection, this is useful for implementing * copy behavior outside of xterm.js. */ getSelection(): string; + /** + * Gets the selection position or undefined if there is no selection. + */ + getSelectionPosition(): ISelectionPosition | undefined; + /** * Clears the current terminal selection. */ clearSelection(): void; + /** + * Selects text within the terminal. + * @param column The column the selection starts at.. + * @param row The row the selection starts at. + * @param length The length of the selection. + */ + select(column: number, row: number, length: number): void; + /** * Selects all text within the terminal. */ @@ -872,6 +877,31 @@ declare module 'xterm' { static applyAddon(addon: any): void; } + /** + * An object representing a selecrtion 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; + } + interface IBuffer { /** * The y position of the cursor. This ranges between `0` (when the From 9ca9f7296efdedfc0db20a57007749a69609c29d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 11 May 2019 23:56:34 -0700 Subject: [PATCH 23/42] Use new APIs in selection addon --- src/addons/search/Interfaces.ts | 1 - src/addons/search/SearchHelper.ts | 24 ++++++++++-------------- 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts index a1f05895..27e3c4b3 100644 --- a/src/addons/search/Interfaces.ts +++ b/src/addons/search/Interfaces.ts @@ -8,7 +8,6 @@ import { Terminal } from 'xterm'; // TODO: Don't rely on this private API export interface ITerminalCore { buffer: any; - selectionManager: any; } export interface ISearchAddonTerminal extends Terminal { diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 3dc99603..c42167fa 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -35,25 +35,23 @@ export class SearchHelper implements ISearchHelper { * @return Whether a result was found. */ public findNext(term: string, searchOptions?: ISearchOptions): boolean { - const selectionManager = this._terminal._core.selectionManager; const {incremental} = searchOptions; let result: ISearchResult; if (!term || term.length === 0) { - selectionManager.clearSelection(); + this._terminal.clearSelection(); return false; } let startCol: number = 0; let startRow = this._terminal._core.buffer.ydisp; - if (selectionManager.selectionEnd) { + if (this._terminal.hasSelection()) { // Start from the selection end if there is a selection // For incremental search, use existing row - if (this._terminal.getSelection().length !== 0) { - startRow = incremental ? selectionManager.selectionStart[1] : selectionManager.selectionEnd[1]; - startCol = incremental ? selectionManager.selectionStart[0] : selectionManager.selectionEnd[0]; - } + const currentSelection = this._terminal.getSelectionPosition(); + startRow = incremental ? currentSelection.startRow : currentSelection.endRow; + startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn; } this._initLinesCache(); @@ -109,11 +107,10 @@ export class SearchHelper implements ISearchHelper { * @return Whether a result was found. */ public findPrevious(term: string, searchOptions?: ISearchOptions): boolean { - const selectionManager = this._terminal._core.selectionManager; let result: ISearchResult; if (!term || term.length === 0) { - selectionManager.clearSelection(); + this._terminal.clearSelection(); return false; } @@ -121,12 +118,11 @@ export class SearchHelper implements ISearchHelper { let startRow = this._terminal._core.buffer.ydisp + this._terminal.rows - 1; let startCol = this._terminal.cols; - if (selectionManager.selectionStart) { + if (this._terminal.hasSelection()) { // Start from the selection start if there is a selection - if (this._terminal.getSelection().length !== 0) { - startRow = selectionManager.selectionStart[1]; - startCol = selectionManager.selectionStart[0]; - } + const currentSelection = this._terminal.getSelectionPosition(); + startRow = currentSelection.startRow; + startCol = currentSelection.startColumn; } this._initLinesCache(); From a7fa58cc60073ef2ab23e3274d5e32eba5fea43b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 11 May 2019 23:59:51 -0700 Subject: [PATCH 24/42] Add API test for getSelectionPosition --- src/public/Terminal.api.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/public/Terminal.api.ts b/src/public/Terminal.api.ts index 3f289f84..a16f2c62 100644 --- a/src/public/Terminal.api.ts +++ b/src/public/Terminal.api.ts @@ -89,19 +89,23 @@ describe('API Integration Tests', () => { it('selection', async function(): Promise { this.timeout(10000); - await openTerminal({ rows: 5 }); + await openTerminal({ rows: 5, cols: 5 }); await page.evaluate(`window.term.write('\\n\\nfoo\\n\\n\\rbar\\n\\n\\rbaz')`); assert.equal(await page.evaluate(`window.term.hasSelection()`), false); assert.equal(await page.evaluate(`window.term.getSelection()`), ''); + assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), undefined); await page.evaluate(`window.term.selectAll()`); assert.equal(await page.evaluate(`window.term.hasSelection()`), true); 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 }); await page.evaluate(`window.term.clearSelection()`); assert.equal(await page.evaluate(`window.term.hasSelection()`), false); assert.equal(await page.evaluate(`window.term.getSelection()`), ''); - await page.evaluate(`window.term.select(1, 2, 2)`) + assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), undefined); + 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 }); }); it('focus, blur', async function(): Promise { From f654044e5eb0a6dafe7452b99065c613b3b45662 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 12 May 2019 00:17:59 -0700 Subject: [PATCH 25/42] Fix search addon unit tests --- src/addons/search/search.test.ts | 45 ++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index 6551fa8b..aee88f50 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -23,11 +23,56 @@ class MockTerminal { get core(): any { return this._core; } + get buffer(): IBuffer { + // TODO: This is a hacky workaround until we use puppeteer for addon tests + const buffer = this._core.buffer; + return { + cursorY: buffer.y, + cursorX: buffer.x, + viewportY: buffer.ydisp, + baseY: buffer.ybase, + length: buffer.length, + getLine(y: number): IBufferLine { + return { + isWrapped: buffer.lines.get(y) ? buffer.lines.get(y).isWrapped : false, + getCell(x: number): IBufferCell { + return { + char: buffer.lines.get(y).get(x)[1/*CHAR_DATA_CHAR_INDEX*/], + width: buffer.lines.get(y).get(x)[2/*CHAR_DATA_WIDTH_INDEX*/] + }; + }, + translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string { + return buffer.translateBufferLineToString(y, trimRight); + } + }; + } + }; + } pushWriteData(): void { this._core._innerWrite(); } } +interface IBuffer { + readonly cursorY: number; + readonly cursorX: number; + readonly viewportY: number; + readonly baseY: number; + readonly length: number; + getLine(y: number): IBufferLine | undefined; +} + +interface IBufferLine { + readonly isWrapped: boolean; + getCell(x: number): IBufferCell; + translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string; +} + +interface IBufferCell { + readonly char: string; + readonly width: number; +} + class TestSearchHelper extends SearchHelper { public findInLine(term: string, rowNumber: number, searchOptions?: ISearchOptions): ISearchResult { return this._findInLine(term, rowNumber, 0, searchOptions); From 0c588a7333e4f6d952f04ce15c86ed385b069c3d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 12 May 2019 09:45:09 -0700 Subject: [PATCH 26/42] Fix missed merge conflict --- src/Terminal.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 5dd765e2..def45f3a 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -43,11 +43,7 @@ import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager'; import { MouseZoneManager } from './MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './ui/ScreenDprMonitor'; -<<<<<<< HEAD -import { ITheme, IMarker, IDisposable, ITerminalAddon } from 'xterm'; -======= -import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm'; ->>>>>>> ups/master +import { ITheme, IMarker, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm'; import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache'; import { DomRenderer } from './renderer/dom/DomRenderer'; import { IKeyboardEvent } from './common/Types'; From 523d562f73d84639e07d04665006f18498755f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 12 May 2019 18:49:23 +0200 Subject: [PATCH 27/42] fix missing types --- src/Terminal.ts | 2 +- src/core/input/TextDecoder.ts | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Terminal.ts b/src/Terminal.ts index 383b73f8..74688cf5 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -1377,7 +1377,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II return; } - // Ignore falsy data values (including the empty string) + // Ignore falsy data values if (!data) { return; } diff --git a/src/core/input/TextDecoder.ts b/src/core/input/TextDecoder.ts index 75029822..7e141e02 100644 --- a/src/core/input/TextDecoder.ts +++ b/src/core/input/TextDecoder.ts @@ -28,7 +28,7 @@ export function utf32ToString(data: Uint32Array, start: number = 0, end: number for (let i = start; i < end; ++i) { let codepoint = data[i]; if (codepoint > 0xFFFF) { - // JS string are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate pair + // JS strings are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate pair // conversion rules: // - subtract 0x10000 from code point, leaving a 20 bit number // - add high 10 bits to 0xD800 --> first surrogate @@ -140,10 +140,10 @@ export class Utf8ToUtf32 { } let size = 0; - let byte1; - let byte2; - let byte3; - let byte4; + let byte1: number; + let byte2: number; + let byte3: number; + let byte4: number; let codepoint = 0; let startPos = 0; @@ -153,7 +153,7 @@ export class Utf8ToUtf32 { let cp = this.interim[0]; cp &= ((((cp & 0xE0) === 0xC0)) ? 0x1F : (((cp & 0xF0) === 0xE0)) ? 0x0F : 0x07); let pos = 0; - let tmp; + let tmp: number; while ((tmp = this.interim[++pos] & 0x3F) && pos < 4) { cp <<= 6; cp |= tmp; From eda04bcb2393ce9d9fcdd45552e47503b945b05f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 12 May 2019 10:20:38 -0700 Subject: [PATCH 28/42] Add new search addon, move AddonManager to public Because addons build on top of the API it needs to live in public, the main reason for this is because the implementation of buffer differs on public/Terminal and src/Terminal. --- demo/client.ts | 9 ++++++--- package.json | 1 + src/Terminal.ts | 10 +--------- src/Types.ts | 2 +- src/{ui => public}/AddonManager.test.ts | 0 src/{ui => public}/AddonManager.ts | 5 ++--- src/public/Terminal.ts | 6 +++++- yarn.lock | 5 +++++ 8 files changed, 21 insertions(+), 17 deletions(-) rename src/{ui => public}/AddonManager.test.ts (100%) rename src/{ui => public}/AddonManager.ts (87%) diff --git a/demo/client.ts b/demo/client.ts index a48b5582..b0ca23e8 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -9,6 +9,7 @@ import { Terminal } from '../lib/public/Terminal'; import { AttachAddon } from 'xterm-addon-attach'; +import { SearchAddon } from 'xterm-addon-search'; import { WebLinksAddon } from 'xterm-addon-web-links'; import * as fit from '../lib/addons/fit/fit'; @@ -28,10 +29,10 @@ declare let window: IWindowWithTerminal; Terminal.applyAddon(fit); Terminal.applyAddon(fullscreen); -Terminal.applyAddon(search); let term; let attachAddon: AttachAddon; +let searchAddon: SearchAddon; let protocol; let socketURL; let socket; @@ -95,6 +96,8 @@ function createTerminal(): void { typedTerm.loadAddon(new WebLinksAddon()); attachAddon = new AttachAddon(); typedTerm.loadAddon(attachAddon); + searchAddon = new SearchAddon(); + typedTerm.loadAddon(searchAddon); window.term = term; // Expose `term` to window for debugging purposes term.onResize((size: { cols: number, rows: number }) => { @@ -119,12 +122,12 @@ function createTerminal(): void { addDomListener(actionElements.findNext, 'keyup', (e) => { const searchOptions = getSearchOptions(); searchOptions.incremental = e.key !== `Enter`; - term.findNext(actionElements.findNext.value, searchOptions); + searchAddon.findNext(actionElements.findNext.value, searchOptions); }); addDomListener(actionElements.findPrevious, 'keyup', (e) => { if (e.key === `Enter`) { - term.findPrevious(actionElements.findPrevious.value, getSearchOptions()); + searchAddon.findPrevious(actionElements.findPrevious.value, getSearchOptions()); } }); diff --git a/package.json b/package.json index 7f3cfc66..55fbef6f 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "webpack": "^4.17.1", "webpack-cli": "^3.1.0", "xterm-addon-attach": "0.1.0-beta7", + "xterm-addon-search": "0.1.0-beta3", "xterm-addon-web-links": "0.1.0-beta6", "zmodem.js": "^0.1.5" }, diff --git a/src/Terminal.ts b/src/Terminal.ts index def45f3a..58e58b6e 100644 --- a/src/Terminal.ts +++ b/src/Terminal.ts @@ -43,14 +43,13 @@ import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager'; import { MouseZoneManager } from './MouseZoneManager'; import { AccessibilityManager } from './AccessibilityManager'; import { ScreenDprMonitor } from './ui/ScreenDprMonitor'; -import { ITheme, IMarker, IDisposable, ITerminalAddon, ISelectionPosition } from 'xterm'; +import { ITheme, IMarker, IDisposable, ISelectionPosition } from 'xterm'; import { removeTerminalFromCache } from './renderer/atlas/CharAtlasCache'; import { DomRenderer } from './renderer/dom/DomRenderer'; import { IKeyboardEvent } from './common/Types'; import { evaluateKeyboardEvent } from './core/input/Keyboard'; import { KeyboardResultType, ICharset, IBufferLine, IAttributeData } from './core/Types'; import { clone } from './common/Clone'; -import { AddonManager } from './ui/AddonManager'; import { EventEmitter2, IEvent } from './common/EventEmitter2'; import { Attributes, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine'; import { applyWindowsMode } from './WindowsMode'; @@ -212,7 +211,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II private _mouseZoneManager: IMouseZoneManager; public mouseHelper: MouseHelper; private _accessibilityManager: AccessibilityManager; - private _addonManager: AddonManager; private _screenDprMonitor: ScreenDprMonitor; private _theme: ITheme; private _windowsMode: IDisposable | undefined; @@ -275,7 +273,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II } public dispose(): void { - this._addonManager.dispose(); super.dispose(); if (this._windowsMode) { this._windowsMode.dispose(); @@ -361,7 +358,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II this.linkifier = this.linkifier || new Linkifier(this); this._mouseZoneManager = this._mouseZoneManager || null; this.soundManager = this.soundManager || new SoundManager(this); - this._addonManager = this._addonManager || new AddonManager(); // Create the terminal's buffers and set the current buffer this.buffers = new BufferSet(this); @@ -1973,10 +1969,6 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II // return this.options.bellStyle === 'sound' || // this.options.bellStyle === 'both'; } - - public loadAddon(addon: ITerminalAddon): void { - return this._addonManager.loadAddon(this, addon); - } } /** diff --git a/src/Types.ts b/src/Types.ts index 939e8531..6136dafb 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -220,6 +220,7 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc showCursor(): void; } +// Portions of the public API that are required by the internal Terminal export interface IPublicTerminal extends IDisposable, IEventEmitter { textarea: HTMLTextAreaElement; rows: number; @@ -268,7 +269,6 @@ export interface IPublicTerminal extends IDisposable, IEventEmitter { setOption(key: string, value: any): void; refresh(start: number, end: number): void; reset(): void; - loadAddon(addon: ITerminalAddon): void; } export interface ITerminalAddon extends IDisposable { diff --git a/src/ui/AddonManager.test.ts b/src/public/AddonManager.test.ts similarity index 100% rename from src/ui/AddonManager.test.ts rename to src/public/AddonManager.test.ts diff --git a/src/ui/AddonManager.ts b/src/public/AddonManager.ts similarity index 87% rename from src/ui/AddonManager.ts rename to src/public/AddonManager.ts index 6821ac60..b66bd4b1 100644 --- a/src/ui/AddonManager.ts +++ b/src/public/AddonManager.ts @@ -3,8 +3,7 @@ * @license MIT */ -import { ITerminalAddon, IDisposable } from 'xterm'; -import { IPublicTerminal } from '../Types'; +import { ITerminalAddon, IDisposable, Terminal } from 'xterm'; export interface ILoadedAddon { instance: ITerminalAddon; @@ -24,7 +23,7 @@ export class AddonManager implements IDisposable { } } - public loadAddon(terminal: IPublicTerminal, instance: ITerminalAddon): void { + public loadAddon(terminal: Terminal, instance: ITerminalAddon): void { const loadedAddon: ILoadedAddon = { instance, dispose: instance.dispose, diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts index 33f346ba..ce13c9b0 100644 --- a/src/public/Terminal.ts +++ b/src/public/Terminal.ts @@ -9,12 +9,15 @@ import { IBufferLine } from '../core/Types'; import { Terminal as TerminalCore } from '../Terminal'; import * as Strings from '../Strings'; import { IEvent } from '../common/EventEmitter2'; +import { AddonManager } from './AddonManager'; export class Terminal implements ITerminalApi { private _core: ITerminal; + private _addonManager: AddonManager; constructor(options?: ITerminalOptions) { this._core = new TerminalCore(options); + this._addonManager = new AddonManager(); } public get onCursorMove(): IEvent { return this._core.onCursorMove; } @@ -115,6 +118,7 @@ export class Terminal implements ITerminalApi { this._core.selectLines(start, end); } public dispose(): void { + this._addonManager.dispose(); this._core.dispose(); } public destroy(): void { @@ -174,7 +178,7 @@ export class Terminal implements ITerminalApi { addon.apply(Terminal); } public loadAddon(addon: ITerminalAddon): void { - return this._core.loadAddon(addon); + return this._addonManager.loadAddon(this, addon); } public static get strings(): ILocalizableStrings { return Strings; diff --git a/yarn.lock b/yarn.lock index 7312d587..eaab4512 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7231,6 +7231,11 @@ xterm-addon-attach@0.1.0-beta7: resolved "https://registry.yarnpkg.com/xterm-addon-attach/-/xterm-addon-attach-0.1.0-beta7.tgz#787f6cce709611ee08ab731b95a62fa1c0bce6a9" integrity sha512-nQr6LcYtpZcyDoHyL/BDIPJcTgL7qlHR/rvm8lSizQysGVT0pSzr5M7SjY3kQHw33U3hTer3c6oZzwjfj4ohOw== +xterm-addon-search@0.1.0-beta3: + version "0.1.0-beta3" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.1.0-beta3.tgz#0754fa329cd505d6591abf24aac560c72f865636" + integrity sha512-09w/h3wsFtCveH1C0Fu8dwVvjiNvWRgp2lDABSK/yQEGETq4nznLzRSiMnFMz1y1rilFUi3Xn+l4+tQK+8iORg== + xterm-addon-web-links@0.1.0-beta6: version "0.1.0-beta6" resolved "https://registry.yarnpkg.com/xterm-addon-web-links/-/xterm-addon-web-links-0.1.0-beta6.tgz#9b4e862be8928ef455a667745bea479665db6c6b" From d230f93bdecf5b04bc4a96056d060ae0467adaad Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 12 May 2019 10:25:41 -0700 Subject: [PATCH 29/42] Remove old search addon from demo --- demo/client.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index b0ca23e8..fff57379 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -9,13 +9,11 @@ import { Terminal } from '../lib/public/Terminal'; import { AttachAddon } from 'xterm-addon-attach'; -import { SearchAddon } from 'xterm-addon-search'; +import { SearchAddon, ISearchOptions } from 'xterm-addon-search'; import { WebLinksAddon } from 'xterm-addon-web-links'; import * as fit from '../lib/addons/fit/fit'; import * as fullscreen from '../lib/addons/fullscreen/fullscreen'; -import * as search from '../lib/addons/search/search'; -import { ISearchOptions } from '../lib/addons/search/Interfaces'; // Pulling in the module's types relies on the above, it's looks a // little weird here as we're importing "this" module From caf8f9d35ea52ffbe2c7273f0fd2725fea280319 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 12 May 2019 10:39:39 -0700 Subject: [PATCH 30/42] Update search addon --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 55fbef6f..832b479f 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "webpack": "^4.17.1", "webpack-cli": "^3.1.0", "xterm-addon-attach": "0.1.0-beta7", - "xterm-addon-search": "0.1.0-beta3", + "xterm-addon-search": "0.1.0-beta4", "xterm-addon-web-links": "0.1.0-beta6", "zmodem.js": "^0.1.5" }, diff --git a/yarn.lock b/yarn.lock index eaab4512..666dae16 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7231,10 +7231,10 @@ xterm-addon-attach@0.1.0-beta7: resolved "https://registry.yarnpkg.com/xterm-addon-attach/-/xterm-addon-attach-0.1.0-beta7.tgz#787f6cce709611ee08ab731b95a62fa1c0bce6a9" integrity sha512-nQr6LcYtpZcyDoHyL/BDIPJcTgL7qlHR/rvm8lSizQysGVT0pSzr5M7SjY3kQHw33U3hTer3c6oZzwjfj4ohOw== -xterm-addon-search@0.1.0-beta3: - version "0.1.0-beta3" - resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.1.0-beta3.tgz#0754fa329cd505d6591abf24aac560c72f865636" - integrity sha512-09w/h3wsFtCveH1C0Fu8dwVvjiNvWRgp2lDABSK/yQEGETq4nznLzRSiMnFMz1y1rilFUi3Xn+l4+tQK+8iORg== +xterm-addon-search@0.1.0-beta4: + version "0.1.0-beta4" + resolved "https://registry.yarnpkg.com/xterm-addon-search/-/xterm-addon-search-0.1.0-beta4.tgz#c73fe058c87f07eaae31baaa92976e927438a396" + integrity sha512-tJgZ1VTRd/DOFUhSFZzybRF8SR1LCEXRYkw/mHzGV5Ba3zhqVdSkN/0J9sjOpX6u21buee2OmTiCMZxq80zfJg== xterm-addon-web-links@0.1.0-beta6: version "0.1.0-beta6" From 1502fea43159f8342eda3c82676d6ef095fde62d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 12 May 2019 10:41:01 -0700 Subject: [PATCH 31/42] Remove unused fullscreen addon from demo --- demo/client.ts | 2 -- demo/index.html | 3 +-- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index fff57379..2d7c800d 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -13,7 +13,6 @@ import { SearchAddon, ISearchOptions } from 'xterm-addon-search'; import { WebLinksAddon } from 'xterm-addon-web-links'; import * as fit from '../lib/addons/fit/fit'; -import * as fullscreen from '../lib/addons/fullscreen/fullscreen'; // Pulling in the module's types relies on the above, it's looks a // little weird here as we're importing "this" module @@ -26,7 +25,6 @@ export interface IWindowWithTerminal extends Window { declare let window: IWindowWithTerminal; Terminal.applyAddon(fit); -Terminal.applyAddon(fullscreen); let term; let attachAddon: AttachAddon; diff --git a/demo/index.html b/demo/index.html index 370a51ed..a7ab0f0c 100644 --- a/demo/index.html +++ b/demo/index.html @@ -3,7 +3,6 @@ xterm.js demo - @@ -16,7 +15,7 @@

- +

From f90239377bd7bd457af00427d9703481e8892015 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 12 May 2019 10:47:35 -0700 Subject: [PATCH 32/42] Fix possible infinite loop if addon disposes terminal --- src/public/AddonManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/public/AddonManager.ts b/src/public/AddonManager.ts index b66bd4b1..b5506514 100644 --- a/src/public/AddonManager.ts +++ b/src/public/AddonManager.ts @@ -49,8 +49,8 @@ export class AddonManager implements IDisposable { if (index === -1) { throw new Error('Could not dispose an addon that has not been loaded'); } - loadedAddon.dispose(); loadedAddon.isDisposed = true; + loadedAddon.dispose(); this._addons.splice(index, 1); } } From 347576494658ba49f63964a100544fc8ddfd464e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 12 May 2019 10:59:26 -0700 Subject: [PATCH 33/42] Types/doc polish --- src/TestUtils.test.ts | 5 +---- src/Types.ts | 6 +----- typings/xterm.d.ts | 3 +++ 3 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts index 109bebd2..3f53c7d3 100644 --- a/src/TestUtils.test.ts +++ b/src/TestUtils.test.ts @@ -9,7 +9,7 @@ import { IBufferLine, ICellData, IAttributeData } from './core/Types'; import { ICircularList, XtermListener } from './common/Types'; import { Buffer } from './Buffer'; import * as Browser from './common/Platform'; -import { ITheme, IDisposable, IMarker, IEvent, ITerminalAddon, ISelectionPosition } from 'xterm'; +import { ITheme, IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm'; import { Terminal } from './Terminal'; import { AttributeData } from './core/buffer/BufferLine'; @@ -30,9 +30,6 @@ export class MockTerminal implements ITerminal { onKey: IEvent<{ key: string; domEvent: KeyboardEvent; }>; onRender: IEvent<{ start: number; end: number; }>; onResize: IEvent<{ cols: number; rows: number; }>; - loadAddon(addon: ITerminalAddon): void { - throw new Error('Method not implemented.'); - } markers: IMarker[]; addMarker(cursorYOffset: number): IMarker { throw new Error('Method not implemented.'); diff --git a/src/Types.ts b/src/Types.ts index 6136dafb..4ffcf7e8 100644 --- a/src/Types.ts +++ b/src/Types.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable, IMarker, Terminal, ISelectionPosition } from 'xterm'; +import { ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable, IMarker, ISelectionPosition } from 'xterm'; import { IColorSet, IRenderer } from './renderer/Types'; import { ICharset, IAttributeData, ICellData, IBufferLine, CharData } from './core/Types'; import { ICircularList } from './common/Types'; @@ -271,10 +271,6 @@ export interface IPublicTerminal extends IDisposable, IEventEmitter { reset(): void; } -export interface ITerminalAddon extends IDisposable { - activate(terminal: Terminal): void; -} - export interface IBufferAccessor { buffer: IBuffer; } diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 89d2b62b..c059b480 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -884,6 +884,9 @@ declare module 'xterm' { loadAddon(addon: ITerminalAddon): void; } + /** + * An addon that can provide additional functionality to the terminal. + */ export interface ITerminalAddon extends IDisposable { /** * (EXPERIMENTAL) This is called when the addon is activated within xterm.js. From bda068c5f04f276bd2742defcf7fef83386fc03a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 12 May 2019 22:07:14 +0200 Subject: [PATCH 34/42] docs --- typings/xterm.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts index 73c7aeed..9afac941 100644 --- a/typings/xterm.d.ts +++ b/typings/xterm.d.ts @@ -759,7 +759,9 @@ declare module 'xterm' { writeln(data: string): void; /** - * Writes text to the terminal encoded as UTF-8 to the terminal. + * Writes UTF8 data to the terminal. + * This has a slight performance advantage over the string based write method + * due to lesser data conversions needed on the way from the pty to xterm.js. * @param data The data to write to the terminal. */ writeUtf8(data: Uint8Array): void; From c0e9bbe87f6a606aee25341547cb5514ee50aea5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sun, 12 May 2019 22:17:06 +0200 Subject: [PATCH 35/42] Revert "change demo to utf8 input" This reverts commit e6e5ecc0f2c4742e781b0f6c7b95caaaae6a024f. --- demo/client.ts | 1 - demo/server.js | 13 ++++++------- src/addons/attach/attach.ts | 5 ----- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index f91c535f..2d7c800d 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -143,7 +143,6 @@ function createTerminal(): void { pid = processId; socketURL += processId; socket = new WebSocket(socketURL); - socket.binaryType = 'arraybuffer'; socket.onopen = runRealTerminal; socket.onclose = runFakeTerminal; socket.onerror = runFakeTerminal; diff --git a/demo/server.js b/demo/server.js index 08413941..8270a398 100644 --- a/demo/server.js +++ b/demo/server.js @@ -36,8 +36,7 @@ function startServer() { cols: cols || 80, rows: rows || 24, cwd: process.env.PWD, - env: process.env, - encoding: null + env: process.env }); console.log('Created terminal with PID: ' + term.pid); @@ -67,20 +66,20 @@ function startServer() { ws.send(logs[term.pid]); function buffer(socket, timeout) { - let buffer = []; + let s = ''; let sender = null; return (data) => { - buffer.push(data); + s += data; if (!sender) { sender = setTimeout(() => { - socket.send(Buffer.concat(buffer)); - buffer = []; + socket.send(s); + s = ''; sender = null; }, timeout); } }; } - const send = buffer(ws, 5); + const send = buffer(ws, 5); term.on('data', function(data) { try { diff --git a/src/addons/attach/attach.ts b/src/addons/attach/attach.ts index 0f5d2ea1..2c8a5d4d 100644 --- a/src/addons/attach/attach.ts +++ b/src/addons/attach/attach.ts @@ -42,11 +42,6 @@ export function attach(term: Terminal, socket: WebSocket, bidirectional: boolean addonTerminal.__getMessage = function(ev: MessageEvent): void { let str: string; - if (ev.data instanceof ArrayBuffer) { - addonTerminal.writeUtf8(new Uint8Array(ev.data)); - return; - } - if (typeof ev.data === 'object') { if (!myTextDecoder) { myTextDecoder = new TextDecoder(); From 6fd5cdd8f1b28e5e643d4b1d598a8832f9d3a4fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 13 May 2019 00:58:14 +0200 Subject: [PATCH 36/42] apply attach changes to demo --- demo/client.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 2d7c800d..affcf64c 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -27,7 +27,6 @@ declare let window: IWindowWithTerminal; Terminal.applyAddon(fit); let term; -let attachAddon: AttachAddon; let searchAddon: SearchAddon; let protocol; let socketURL; @@ -90,8 +89,6 @@ function createTerminal(): void { // Load addons const typedTerm = term as TerminalType; typedTerm.loadAddon(new WebLinksAddon()); - attachAddon = new AttachAddon(); - typedTerm.loadAddon(attachAddon); searchAddon = new SearchAddon(); typedTerm.loadAddon(searchAddon); @@ -152,7 +149,7 @@ function createTerminal(): void { } function runRealTerminal(): void { - attachAddon.attach(socket); + term.loadAddon(new AttachAddon(socket)); term._initialized = true; } From 53167df1a800000f6121bf95bc9adf44c30e181e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 13 May 2019 01:24:41 +0200 Subject: [PATCH 37/42] document utf8 switch in client.ts --- demo/client.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/demo/client.ts b/demo/client.ts index affcf64c..f9a6f6ff 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -149,7 +149,14 @@ function createTerminal(): void { } function runRealTerminal(): void { + /** + * The demo defaults to string transport by default. + * To run it with UTF8 binary transport, swap comment on + * the lines below. (Must also be switched in server.js) + */ term.loadAddon(new AttachAddon(socket)); + //term.loadAddon(new AttachAddon(socket, {inputUtf8: true})); + term._initialized = true; } From 67be8c36e75e735f8ed9bb590ccbeb9158922b97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 13 May 2019 01:40:54 +0200 Subject: [PATCH 38/42] utf8 switch in server.js --- demo/client.ts | 4 ++-- demo/server.js | 28 ++++++++++++++++++++++++++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index f9a6f6ff..685f633b 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -154,8 +154,8 @@ function runRealTerminal(): void { * To run it with UTF8 binary transport, swap comment on * the lines below. (Must also be switched in server.js) */ - term.loadAddon(new AttachAddon(socket)); - //term.loadAddon(new AttachAddon(socket, {inputUtf8: true})); + //term.loadAddon(new AttachAddon(socket)); + term.loadAddon(new AttachAddon(socket, {inputUtf8: true})); term._initialized = true; } diff --git a/demo/server.js b/demo/server.js index 8270a398..718327e4 100644 --- a/demo/server.js +++ b/demo/server.js @@ -3,6 +3,13 @@ var expressWs = require('express-ws'); var os = require('os'); var pty = require('node-pty'); +/** + * Whether to use UTF8 binary transport. + * (Must also be switched in client.ts) + */ +const USE_BINARY_UTF8 = true; + + function startServer() { var app = express(); expressWs(app); @@ -36,7 +43,8 @@ function startServer() { cols: cols || 80, rows: rows || 24, cwd: process.env.PWD, - env: process.env + env: process.env, + encoding: USE_BINARY_UTF8 ? null : 'utf8' }); console.log('Created terminal with PID: ' + term.pid); @@ -65,6 +73,7 @@ function startServer() { console.log('Connected to terminal ' + term.pid); ws.send(logs[term.pid]); + // string message buffering function buffer(socket, timeout) { let s = ''; let sender = null; @@ -79,7 +88,22 @@ function startServer() { } }; } - const send = buffer(ws, 5); + // binary message buffering + function bufferUtf8(socket, timeout) { + let buffer = []; + let sender = null; + return (data) => { + buffer.push(data); + if (!sender) { + sender = setTimeout(() => { + socket.send(Buffer.concat(buffer)); + buffer = []; + sender = null; + }, timeout); + } + }; + } + const send = USE_BINARY_UTF8 ? bufferUtf8(ws, 5) : buffer(ws, 5); term.on('data', function(data) { try { From 1a596f9608b0ca337be5d81b1d8b0a851006eb8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 13 May 2019 01:41:51 +0200 Subject: [PATCH 39/42] default to string transport --- demo/client.ts | 4 ++-- demo/server.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 685f633b..f9a6f6ff 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -154,8 +154,8 @@ function runRealTerminal(): void { * To run it with UTF8 binary transport, swap comment on * the lines below. (Must also be switched in server.js) */ - //term.loadAddon(new AttachAddon(socket)); - term.loadAddon(new AttachAddon(socket, {inputUtf8: true})); + term.loadAddon(new AttachAddon(socket)); + //term.loadAddon(new AttachAddon(socket, {inputUtf8: true})); term._initialized = true; } diff --git a/demo/server.js b/demo/server.js index 718327e4..823dc6c6 100644 --- a/demo/server.js +++ b/demo/server.js @@ -7,7 +7,7 @@ var pty = require('node-pty'); * Whether to use UTF8 binary transport. * (Must also be switched in client.ts) */ -const USE_BINARY_UTF8 = true; +const USE_BINARY_UTF8 = false; function startServer() { From 69411916cb84c4572837d7f0cfee4e3d8913f0cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 13 May 2019 01:45:21 +0200 Subject: [PATCH 40/42] make linter happy --- demo/client.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/client.ts b/demo/client.ts index f9a6f6ff..01789402 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -155,7 +155,7 @@ function runRealTerminal(): void { * the lines below. (Must also be switched in server.js) */ term.loadAddon(new AttachAddon(socket)); - //term.loadAddon(new AttachAddon(socket, {inputUtf8: true})); + // term.loadAddon(new AttachAddon(socket, {inputUtf8: true})); term._initialized = true; } From 58d461bb9047c9282e627a5ea323fef863112ee2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Mon, 13 May 2019 15:33:54 +0200 Subject: [PATCH 41/42] optimize Buffer.concat --- demo/server.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/demo/server.js b/demo/server.js index 823dc6c6..e3473400 100644 --- a/demo/server.js +++ b/demo/server.js @@ -92,13 +92,16 @@ function startServer() { function bufferUtf8(socket, timeout) { let buffer = []; let sender = null; + let length = 0; return (data) => { buffer.push(data); + length += data.length; if (!sender) { sender = setTimeout(() => { - socket.send(Buffer.concat(buffer)); + socket.send(Buffer.concat(buffer, length)); buffer = []; sender = null; + length = 0; }, timeout); } }; From b9cfbbea8fb96fd6d9201d016c5edc07c728d749 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 13 May 2019 20:34:14 -0700 Subject: [PATCH 42/42] xterm-addon-attach@0.1.0-beta8 --- package.json | 4 ++-- yarn.lock | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index cafa43b1..043b76cb 100644 --- a/package.json +++ b/package.json @@ -40,13 +40,13 @@ "ts-loader": "^4.5.0", "tslint": "^5.9.1", "tslint-consistent-codestyle": "^1.13.0", - "utf8": "^3.0.0", "typescript": "3.4", + "utf8": "^3.0.0", "vinyl-buffer": "^1.0.0", "vinyl-source-stream": "^1.1.0", "webpack": "^4.17.1", "webpack-cli": "^3.1.0", - "xterm-addon-attach": "0.1.0-beta7", + "xterm-addon-attach": "0.1.0-beta8", "xterm-addon-search": "0.1.0-beta4", "xterm-addon-web-links": "0.1.0-beta6", "zmodem.js": "^0.1.5" diff --git a/yarn.lock b/yarn.lock index 3db8e9a4..321fe6f3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7236,10 +7236,10 @@ xregexp@4.0.0: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68= -xterm-addon-attach@0.1.0-beta7: - version "0.1.0-beta7" - resolved "https://registry.yarnpkg.com/xterm-addon-attach/-/xterm-addon-attach-0.1.0-beta7.tgz#787f6cce709611ee08ab731b95a62fa1c0bce6a9" - integrity sha512-nQr6LcYtpZcyDoHyL/BDIPJcTgL7qlHR/rvm8lSizQysGVT0pSzr5M7SjY3kQHw33U3hTer3c6oZzwjfj4ohOw== +xterm-addon-attach@0.1.0-beta8: + version "0.1.0-beta8" + resolved "https://registry.yarnpkg.com/xterm-addon-attach/-/xterm-addon-attach-0.1.0-beta8.tgz#e469ed9d6ab7e535d0a9ffae23ef4f2efe58163b" + integrity sha512-HtQuwqnvcR+SwI9/JbBMd//Il+oEeo3rWrIucLLKHT8sB+OAOkdhmo5KIM/hhnovjI040WJ+tTHkDgPFwIJtmw== xterm-addon-search@0.1.0-beta4: version "0.1.0-beta4"