mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge branch 'master' into utf8_input
This commit is contained in:
+19
-15
@@ -8,12 +8,11 @@
|
||||
/// <reference path="../typings/xterm.d.ts"/>
|
||||
|
||||
import { Terminal } from '../lib/public/Terminal';
|
||||
import * as attach from '../lib/addons/attach/attach';
|
||||
import { AttachAddon } from 'xterm-addon-attach';
|
||||
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 * as webLinks from '../lib/addons/webLinks/webLinks';
|
||||
import { ISearchOptions } from '../lib/addons/search/Interfaces';
|
||||
|
||||
// Pulling in the module's types relies on the <reference> above, it's looks a
|
||||
// little weird here as we're importing "this" module
|
||||
@@ -25,14 +24,11 @@ export interface IWindowWithTerminal extends Window {
|
||||
}
|
||||
declare let window: IWindowWithTerminal;
|
||||
|
||||
Terminal.applyAddon(attach);
|
||||
Terminal.applyAddon(fit);
|
||||
Terminal.applyAddon(fullscreen);
|
||||
Terminal.applyAddon(search);
|
||||
Terminal.applyAddon(webLinks);
|
||||
|
||||
|
||||
let term;
|
||||
let attachAddon: AttachAddon;
|
||||
let searchAddon: SearchAddon;
|
||||
let protocol;
|
||||
let socketURL;
|
||||
let socket;
|
||||
@@ -85,10 +81,20 @@ function createTerminal(): void {
|
||||
while (terminalContainer.children.length) {
|
||||
terminalContainer.removeChild(terminalContainer.children[0]);
|
||||
}
|
||||
|
||||
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
|
||||
term = new Terminal({
|
||||
windowsMode: isWindows
|
||||
} as ITerminalOptions);
|
||||
|
||||
// Load addons
|
||||
const typedTerm = term as TerminalType;
|
||||
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 }) => {
|
||||
if (!pid) {
|
||||
@@ -104,8 +110,6 @@ function createTerminal(): void {
|
||||
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
|
||||
|
||||
term.open(terminalContainer);
|
||||
|
||||
term.webLinksInit();
|
||||
term.fit();
|
||||
term.focus();
|
||||
|
||||
@@ -114,12 +118,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());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -149,7 +153,7 @@ function createTerminal(): void {
|
||||
}
|
||||
|
||||
function runRealTerminal(): void {
|
||||
term.attach(socket);
|
||||
attachAddon.attach(socket);
|
||||
term._initialized = true;
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<title>xterm.js demo</title>
|
||||
<link rel="stylesheet" href="/src/xterm.css" />
|
||||
<link rel="stylesheet" href="/src/addons/fullscreen/fullscreen.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
|
||||
@@ -16,7 +15,7 @@
|
||||
<p>
|
||||
<label>Find next <input id="find-next"/></label>
|
||||
<label>Find previous <input id="find-previous"/></label>
|
||||
<label>Use regex<input type="checkbox" id="regex"/></label>
|
||||
<label>Use regex<input type="checkbox" id="regex"/></label>
|
||||
<label>Case sensitive<input type="checkbox" id="case-sensitive"/></label>
|
||||
<label>Whole word<input type="checkbox" id="whole-word"/></label>
|
||||
</p>
|
||||
|
||||
+3
-1
@@ -25,11 +25,13 @@ const clientConfig = {
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre"
|
||||
enforce: "pre",
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
modules: [path.resolve(__dirname, '..'), 'node_modules'],
|
||||
extensions: [ '.tsx', '.ts', '.js' ]
|
||||
},
|
||||
output: {
|
||||
|
||||
@@ -46,6 +46,9 @@
|
||||
"vinyl-source-stream": "^1.1.0",
|
||||
"webpack": "^4.17.1",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"xterm-addon-attach": "0.1.0-beta7",
|
||||
"xterm-addon-search": "0.1.0-beta4",
|
||||
"xterm-addon-web-links": "0.1.0-beta6",
|
||||
"zmodem.js": "^0.1.5"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -16,7 +16,7 @@ class TestTerminal extends Terminal {
|
||||
public keyPress(ev: any): boolean { return this._keyPress(ev); }
|
||||
}
|
||||
|
||||
describe('xterm.js', () => {
|
||||
describe('Terminal', () => {
|
||||
let term: TestTerminal;
|
||||
const termOptions = {
|
||||
cols: INIT_COLS,
|
||||
|
||||
+49
-48
@@ -221,55 +221,56 @@ 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;
|
||||
cols: number;
|
||||
buffer: IBuffer;
|
||||
markers: IMarker[];
|
||||
onCursorMove: IEvent<void>;
|
||||
onData: IEvent<string>;
|
||||
onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>;
|
||||
onLineFeed: IEvent<void>;
|
||||
onScroll: IEvent<number>;
|
||||
onSelectionChange: IEvent<void>;
|
||||
onRender: IEvent<{ start: number, end: number }>;
|
||||
onResize: IEvent<{ cols: number, rows: number }>;
|
||||
onTitleChange: IEvent<string>;
|
||||
blur(): void;
|
||||
focus(): void;
|
||||
resize(columns: number, rows: number): void;
|
||||
writeln(data: string): void;
|
||||
open(parent: HTMLElement): void;
|
||||
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
|
||||
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
|
||||
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
|
||||
deregisterLinkMatcher(matcherId: number): void;
|
||||
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
|
||||
deregisterCharacterJoiner(joinerId: number): void;
|
||||
addMarker(cursorYOffset: number): IMarker;
|
||||
hasSelection(): boolean;
|
||||
getSelection(): string;
|
||||
getSelectionPosition(): ISelectionPosition | undefined;
|
||||
clearSelection(): void;
|
||||
select(column: number, row: number, length: number): void;
|
||||
selectAll(): void;
|
||||
selectLines(start: number, end: number): void;
|
||||
dispose(): void;
|
||||
destroy(): void;
|
||||
scrollLines(amount: number): void;
|
||||
scrollPages(pageCount: number): void;
|
||||
scrollToTop(): void;
|
||||
scrollToBottom(): void;
|
||||
scrollToLine(line: number): void;
|
||||
clear(): void;
|
||||
write(data: string): void;
|
||||
writeUtf8(data: Uint8Array): void;
|
||||
getOption(key: string): any;
|
||||
setOption(key: string, value: any): void;
|
||||
refresh(start: number, end: number): void;
|
||||
reset(): void;
|
||||
textarea: HTMLTextAreaElement;
|
||||
rows: number;
|
||||
cols: number;
|
||||
buffer: IBuffer;
|
||||
markers: IMarker[];
|
||||
onCursorMove: IEvent<void>;
|
||||
onData: IEvent<string>;
|
||||
onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>;
|
||||
onLineFeed: IEvent<void>;
|
||||
onScroll: IEvent<number>;
|
||||
onSelectionChange: IEvent<void>;
|
||||
onRender: IEvent<{ start: number, end: number }>;
|
||||
onResize: IEvent<{ cols: number, rows: number }>;
|
||||
onTitleChange: IEvent<string>;
|
||||
blur(): void;
|
||||
focus(): void;
|
||||
resize(columns: number, rows: number): void;
|
||||
writeln(data: string): void;
|
||||
open(parent: HTMLElement): void;
|
||||
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
|
||||
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
|
||||
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
|
||||
deregisterLinkMatcher(matcherId: number): void;
|
||||
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
|
||||
deregisterCharacterJoiner(joinerId: number): void;
|
||||
addMarker(cursorYOffset: number): IMarker;
|
||||
hasSelection(): boolean;
|
||||
getSelection(): string;
|
||||
getSelectionPosition(): ISelectionPosition | undefined;
|
||||
clearSelection(): void;
|
||||
select(column: number, row: number, length: number): void;
|
||||
selectAll(): void;
|
||||
selectLines(start: number, end: number): void;
|
||||
dispose(): void;
|
||||
destroy(): void;
|
||||
scrollLines(amount: number): void;
|
||||
scrollPages(pageCount: number): void;
|
||||
scrollToTop(): void;
|
||||
scrollToBottom(): void;
|
||||
scrollToLine(line: number): void;
|
||||
clear(): void;
|
||||
write(data: string): void;
|
||||
writeUtf8(data: Uint8Array): void;
|
||||
getOption(key: string): any;
|
||||
setOption(key: string, value: any): void;
|
||||
refresh(start: number, end: number): void;
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
export interface IBufferAccessor {
|
||||
|
||||
@@ -5,14 +5,8 @@
|
||||
|
||||
import { Terminal } from 'xterm';
|
||||
|
||||
// TODO: Don't rely on this private API
|
||||
export interface ITerminalCore {
|
||||
buffer: any;
|
||||
}
|
||||
|
||||
export interface ISearchAddonTerminal extends Terminal {
|
||||
__searchHelper?: ISearchHelper;
|
||||
_core: ITerminalCore;
|
||||
}
|
||||
|
||||
export interface ISearchHelper {
|
||||
|
||||
@@ -44,7 +44,7 @@ export class SearchHelper implements ISearchHelper {
|
||||
}
|
||||
|
||||
let startCol: number = 0;
|
||||
let startRow = this._terminal._core.buffer.ydisp;
|
||||
let startRow = this._terminal.buffer.viewportY;
|
||||
|
||||
if (this._terminal.hasSelection()) {
|
||||
// Start from the selection end if there is a selection
|
||||
@@ -62,7 +62,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;
|
||||
}
|
||||
@@ -73,7 +73,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
|
||||
@@ -115,7 +115,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 (this._terminal.hasSelection()) {
|
||||
@@ -135,7 +135,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--) {
|
||||
@@ -145,7 +145,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;
|
||||
@@ -156,14 +156,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;
|
||||
@@ -180,7 +180,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());
|
||||
}
|
||||
@@ -230,7 +230,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;
|
||||
@@ -282,18 +282,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++;
|
||||
}
|
||||
@@ -318,9 +318,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);
|
||||
|
||||
@@ -338,7 +338,7 @@ export class SearchHelper implements ISearchHelper {
|
||||
return false;
|
||||
}
|
||||
this._terminal.select(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* 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 {
|
||||
activate(terminal: any): void {
|
||||
assert.equal(terminal, 'foo', 'The first constructor arg should be Terminal');
|
||||
called = true;
|
||||
}
|
||||
dispose(): void { }
|
||||
}
|
||||
manager.loadAddon('foo' as any, new Addon());
|
||||
assert.equal(called, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dispose', () => {
|
||||
it('should dispose all loaded addons', () => {
|
||||
let called = 0;
|
||||
class Addon implements ITerminalAddon {
|
||||
activate(): void {}
|
||||
dispose(): void { called++; }
|
||||
}
|
||||
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);
|
||||
assert.equal(manager.addons.length, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminalAddon, IDisposable, Terminal } from 'xterm';
|
||||
|
||||
export interface ILoadedAddon {
|
||||
instance: ITerminalAddon;
|
||||
dispose: () => void;
|
||||
isDisposed: boolean;
|
||||
}
|
||||
|
||||
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, instance: ITerminalAddon): void {
|
||||
const loadedAddon: ILoadedAddon = {
|
||||
instance,
|
||||
dispose: instance.dispose,
|
||||
isDisposed: false
|
||||
};
|
||||
this._addons.push(loadedAddon);
|
||||
instance.dispose = () => this._wrappedAddonDispose(loadedAddon);
|
||||
instance.activate(<any>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] === loadedAddon) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index === -1) {
|
||||
throw new Error('Could not dispose an addon that has not been loaded');
|
||||
}
|
||||
loadedAddon.isDisposed = true;
|
||||
loadedAddon.dispose();
|
||||
this._addons.splice(index, 1);
|
||||
}
|
||||
}
|
||||
@@ -135,6 +135,52 @@ describe('API Integration Tests', () => {
|
||||
assert.equal(await page.evaluate(`document.activeElement.className`), '');
|
||||
});
|
||||
|
||||
describe('loadAddon', () => {
|
||||
it('constructor', async function(): Promise<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
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<any> {
|
||||
this.timeout(10000);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { assert } from 'chai';
|
||||
import { Terminal } from './Terminal';
|
||||
import * as attach from '../addons/attach/attach';
|
||||
|
||||
describe('Terminal', () => {
|
||||
describe('Terminal', () => {
|
||||
it('should apply addons with Terminal.applyAddon', () => {
|
||||
Terminal.applyAddon(attach);
|
||||
// Test that addon was applied successfully, adding attach to Terminal's
|
||||
|
||||
@@ -3,18 +3,21 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi, ISelectionPosition } from 'xterm';
|
||||
import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm';
|
||||
import { ITerminal, IBuffer } from '../Types';
|
||||
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<void> { 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 {
|
||||
@@ -176,6 +180,9 @@ export class Terminal implements ITerminalApi {
|
||||
public static applyAddon(addon: any): void {
|
||||
addon.apply(Terminal);
|
||||
}
|
||||
public loadAddon(addon: ITerminalAddon): void {
|
||||
return this._addonManager.loadAddon(this, addon);
|
||||
}
|
||||
public static get strings(): ILocalizableStrings {
|
||||
return Strings;
|
||||
}
|
||||
|
||||
Vendored
+17
@@ -881,8 +881,25 @@ 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;
|
||||
|
||||
/**
|
||||
* (EXPERIMENTAL) Loads an addon into this instance of xterm.js.
|
||||
* @param addon The addon to load.
|
||||
*/
|
||||
loadAddon(addon: ITerminalAddon): void;
|
||||
}
|
||||
|
||||
/**
|
||||
* An addon that can provide additional functionality to the terminal.
|
||||
*/
|
||||
export interface ITerminalAddon extends IDisposable {
|
||||
/**
|
||||
* (EXPERIMENTAL) This is called when the addon is activated within xterm.js.
|
||||
*/
|
||||
activate(terminal: Terminal): void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -7236,6 +7236,21 @@ xregexp@4.0.0:
|
||||
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af"
|
||||
integrity sha1-pcbVMr5lbiPbgg77lDofBJmNY68=
|
||||
|
||||
xterm-addon-attach@0.1.0-beta7:
|
||||
version "0.1.0-beta7"
|
||||
resolved "https://registry.yarnpkg.com/xterm-addon-attach/-/xterm-addon-attach-0.1.0-beta7.tgz#787f6cce709611ee08ab731b95a62fa1c0bce6a9"
|
||||
integrity sha512-nQr6LcYtpZcyDoHyL/BDIPJcTgL7qlHR/rvm8lSizQysGVT0pSzr5M7SjY3kQHw33U3hTer3c6oZzwjfj4ohOw==
|
||||
|
||||
xterm-addon-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"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41"
|
||||
|
||||
Reference in New Issue
Block a user