From 290394b401c53e096f4659daea4eb1c3417e9e9d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 3 Jan 2019 20:22:53 -0800 Subject: [PATCH 01/16] 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 a3a912f630..ae628baa11 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 fdc9678be0..4807ddde6c 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 bc97de29ff..9e0ef6b9ea 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 f0d69cc58d..6fc6b25d4f 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 87fcfaef99..6f2de10f58 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 0000000000..ac83917c21 --- /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 0000000000..b55bd6a891 --- /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 e6e4aaa32f..fdcdd4e58d 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 c5d2b0204e..d73a9156d3 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/16] 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 ae628baa11..406794770b 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 78f1ff1d31..1796dc174c 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 5ed3a4b800..ab101fca79 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 ab5846f5ea..0000000000 --- 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 e280b65686..0000000000 --- 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 f121e2e2d4..0000000000 --- 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 b6f853be8f..0000000000 --- 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 9e45068b58..0000000000 --- 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 359fbd2408..0000000000 --- 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 f200cab4a1..0000000000 --- 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 18105aa246..0000000000 --- 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 1e8a4ae7e6..0000000000 --- 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 6fc6b25d4f..0000000000 --- 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 5555321d1f..e345f6d2ff 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/16] 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 06c8f1d591..0000000000 --- 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 bee5df3237..d0bf01a016 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/16] 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 | 18 ++++----- 10 files changed, 50 insertions(+), 141 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 3d55bc0020..f4a23f52bd 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 2e2186dc2e..7e13e79055 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 438e3e1b1b..b1f8e8f462 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 df8f5d9011..7a29a282c7 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 6f2de10f58..77889271a4 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 ac83917c21..8198ce316c 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 b55bd6a891..34e7e5dd4d 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 ec3aa60e0e..4a3a24e37f 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 fddd898270..830ed2986e 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 1fa1739c78..4ca95193af 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-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-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-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/16] 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 830ed2986e..1d1ebad9ad 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/16] 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 0000000000..4b2690993d --- /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 0000000000..e280b65686 --- /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 0000000000..2c8a5d4d21 --- /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 0000000000..b6f853be8f --- /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 0000000000..9e45068b58 --- /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 0000000000..2f39102cab --- /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 0000000000..f200cab4a1 --- /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 0000000000..9c4f117606 --- /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 0000000000..da5569ab81 --- /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 0000000000..8a0fec097f --- /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 0000000000..6bad5b0440 --- /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 55689ad86d..2a53ab8994 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/16] 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 cd7bcf0d97..817da6ca66 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/16] 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 6b74dfbbcf..55c5c06275 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 380cc89c3a..e56e35357f 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/16] 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 a1f05895e7..d99bd271c0 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 7db1ed4337..ce3d000481 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/16] 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 ce3d000481..7f24a618bb 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/16] 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 6551fa8b52..aee88f5096 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/16] 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 5dd765e292..def45f3ab6 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/16] 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 a48b55824f..b0ca23e8f3 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 7f3cfc6656..55fbef6f2c 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 def45f3ab6..58e58b6ebd 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 939e853191..6136dafbb4 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 6821ac6023..b66bd4b1a5 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 33f346ba6b..ce13c9b0bb 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 7312d58760..eaab45128b 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/16] 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 b0ca23e8f3..fff573798c 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/16] 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 55fbef6f2c..832b479fe1 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 eaab45128b..666dae16d8 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/16] 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 fff573798c..2d7c800d42 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 370a51ed53..a7ab0f0cf9 100644 --- a/demo/index.html +++ b/demo/index.html @@ -3,7 +3,6 @@ xterm.js demo - @@ -16,7 +15,7 @@

Actions

- +