From 290394b401c53e096f4659daea4eb1c3417e9e9d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 3 Jan 2019 20:22:53 -0800 Subject: [PATCH 01/18] 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/18] 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 3f993d675f304a3587677dfe8536a763d579a70e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 2 Apr 2019 20:17:33 -0400 Subject: [PATCH 03/18] 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 04/18] 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 05/18] 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 06/18] 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 07/18] 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 08/18] 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 780e924b213373e23f03720225e99c8017695d03 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 11 May 2019 23:15:04 -0700 Subject: [PATCH 09/18] 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 10/18] 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 f654044e5eb0a6dafe7452b99065c613b3b45662 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 12 May 2019 00:17:59 -0700 Subject: [PATCH 11/18] 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 12/18] 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 eda04bcb2393ce9d9fcdd45552e47503b945b05f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 12 May 2019 10:20:38 -0700 Subject: [PATCH 13/18] 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 14/18] 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 15/18] 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 16/18] 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 17/18] 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 18/18] 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.