New addon prototype

Part of #1128
This commit is contained in:
Daniel Imms
2019-01-03 20:22:53 -08:00
parent f98572cf71
commit 290394b401
9 changed files with 261 additions and 7 deletions
+3 -2
View File
@@ -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();
+1 -1
View File
@@ -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,
+16 -1
View File
@@ -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<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): T {
return this._addonManager.loadAddon(this, addonConstructor);
}
public disposeAddon<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): void {
this._addonManager.disposeAddon(addonConstructor);
}
public getAddon<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): T {
return this._addonManager.getAddon(addonConstructor);
}
}
/**
+19 -1
View File
@@ -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
(<any>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);
}
}
+10 -1
View File
@@ -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<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): T {
return this._core.loadAddon(addonConstructor);
}
public getAddon<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): T {
return this._core.getAddon(addonConstructor);
}
public disposeAddon<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): void {
this._core.disposeAddon(addonConstructor);
}
public static get strings(): ILocalizableStrings {
return Strings;
}
+111
View File
@@ -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);
});
});
});
+68
View File
@@ -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<ITerminalAddon>;
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<T extends ITerminalAddon>(terminal: Terminal, addonConstructor: ITerminalAddonConstructor<T>): 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<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): 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<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): 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);
}
}
+10 -1
View File
@@ -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<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): T {
throw new Error('Method not implemented.');
}
disposeAddon<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): void {
throw new Error('Method not implemented.');
}
getAddon<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): T {
throw new Error('Method not implemented.');
}
markers: IMarker[];
addMarker(cursorYOffset: number): IMarker {
throw new Error('Method not implemented.');
+23
View File
@@ -753,5 +753,28 @@ declare module 'xterm' {
* @param addon The addon to apply.
*/
static applyAddon(addon: any): void;
loadAddon<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): T;
disposeAddon<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): void;
getAddon<T extends ITerminalAddon>(addonConstructor: ITerminalAddonConstructor<T>): T;
}
export interface ITerminalAddonConstructor<T extends ITerminalAddon> {
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<ITerminalAddon>[];
/**
* This function includes anything that needs to happen to clean up when
* the addon is being disposed.
*/
dispose(): void;
}
}