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 a3a912f63..ae628baa1 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 fdc9678be..4807ddde6 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 bc97de29f..9e0ef6b9e 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 f0d69cc58..6fc6b25d4 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 87fcfaef9..6f2de10f5 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 000000000..ac83917c2 --- /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 000000000..b55bd6a89 --- /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 e6e4aaa32..fdcdd4e58 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 c5d2b0204..d73a9156d 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 ae628baa1..406794770 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 78f1ff1d3..1796dc174 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 5ed3a4b80..ab101fca7 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 ab5846f5e..000000000 --- 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 e280b6568..000000000 --- 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 f121e2e2d..000000000 --- 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 b6f853be8..000000000 --- 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 9e45068b5..000000000 --- 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 359fbd240..000000000 --- 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 f200cab4a..000000000 --- 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 18105aa24..000000000 --- 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 1e8a4ae7e..000000000 --- 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 6fc6b25d4..000000000 --- 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 5555321d1..e345f6d2f 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 06c8f1d59..000000000 --- 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 bee5df323..d0bf01a01 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 3d55bc002..f4a23f52b 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 2e2186dc2..7e13e7905 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 438e3e1b1..b1f8e8f46 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 df8f5d901..7a29a282c 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 6f2de10f5..77889271a 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 ac83917c2..8198ce316 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 b55bd6a89..34e7e5dd4 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 ec3aa60e0..4a3a24e37 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 fddd89827..830ed2986 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 1fa1739c7..4ca95193a 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 830ed2986..1d1ebad9a 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 000000000..4b2690993 --- /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 000000000..e280b6568 --- /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 000000000..2c8a5d4d2 --- /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 000000000..b6f853be8 --- /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 000000000..9e45068b5 --- /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 000000000..2f39102ca --- /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 000000000..f200cab4a --- /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 000000000..9c4f11760 --- /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 000000000..da5569ab8 --- /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 000000000..8a0fec097 --- /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 000000000..6bad5b044 --- /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 55689ad86..2a53ab899 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 cd7bcf0d9..817da6ca6 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 6b74dfbbc..55c5c0627 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 380cc89c3..e56e35357 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 a1f05895e..d99bd271c 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 7db1ed433..ce3d00048 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 ce3d00048..7f24a618b 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 6551fa8b5..aee88f509 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 5dd765e29..def45f3ab 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 a48b55824..b0ca23e8f 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 7f3cfc665..55fbef6f2 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 def45f3ab..58e58b6eb 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 939e85319..6136dafbb 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 6821ac602..b66bd4b1a 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 33f346ba6..ce13c9b0b 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 7312d5876..eaab45128 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 b0ca23e8f..fff573798 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 55fbef6f2..832b479fe 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 eaab45128..666dae16d 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 fff573798..2d7c800d4 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 370a51ed5..a7ab0f0cf 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 b66bd4b1a..b55065142 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 109bebd2f..3f53c7d3a 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 6136dafbb..4ffcf7e84 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 89d2b62b4..c059b480a 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.