above, it's looks a
// little weird here as we're importing "this" module
@@ -21,17 +20,14 @@ import { Terminal as TerminalType, ITerminalOptions } from 'xterm';
export interface IWindowWithTerminal extends Window {
term: TerminalType;
+ Terminal?: typeof TerminalType;
}
declare let window: IWindowWithTerminal;
-Terminal.applyAddon(attach);
Terminal.applyAddon(fit);
-Terminal.applyAddon(fullscreen);
-Terminal.applyAddon(search);
-Terminal.applyAddon(webLinks);
-
let term;
+let searchAddon: SearchAddon;
let protocol;
let socketURL;
let socket;
@@ -57,8 +53,6 @@ function getSearchOptions(): ISearchOptions {
};
}
-createTerminal();
-
const disposeRecreateButtonHandler = () => {
// If the terminal exists dispose of it, otherwise recreate it
if (term) {
@@ -74,17 +68,30 @@ const disposeRecreateButtonHandler = () => {
}
};
-document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
+if (document.location.pathname === '/test') {
+ window.Terminal = Terminal;
+} else {
+ createTerminal();
+ document.getElementById('dispose').addEventListener('click', disposeRecreateButtonHandler);
+}
function createTerminal(): void {
// Clean terminal
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());
+ 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) {
@@ -100,8 +107,6 @@ function createTerminal(): void {
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
term.open(terminalContainer);
-
- term.webLinksInit();
term.fit();
term.focus();
@@ -110,12 +115,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());
}
});
@@ -144,7 +149,14 @@ function createTerminal(): void {
}
function runRealTerminal(): void {
- term.attach(socket);
+ /**
+ * The demo defaults to string transport by default.
+ * To run it with UTF8 binary transport, swap comment on
+ * the lines below. (Must also be switched in server.js)
+ */
+ term.loadAddon(new AttachAddon(socket));
+ // term.loadAddon(new AttachAddon(socket, {inputUtf8: true}));
+
term._initialized = true;
}
@@ -259,8 +271,11 @@ function initOptions(term: TerminalType): void {
console.log('change', o, input.value);
if (o === 'cols' || o === 'rows') {
updateTerminalSize();
+ } else if (o === 'lineHeight') {
+ term.setOption(o, parseFloat(input.value));
+ updateTerminalSize();
} else {
- term.setOption(o, o === 'lineHeight' ? parseFloat(input.value) : parseInt(input.value, 10));
+ term.setOption(o, parseInt(input.value));
}
});
});
diff --git a/demo/index.html b/demo/index.html
index 370a51ed..a7ab0f0c 100644
--- a/demo/index.html
+++ b/demo/index.html
@@ -3,7 +3,6 @@
xterm.js demo
-
@@ -16,7 +15,7 @@
-
+
diff --git a/demo/server.js b/demo/server.js
index 758023c7..e3473400 100644
--- a/demo/server.js
+++ b/demo/server.js
@@ -3,6 +3,13 @@ var expressWs = require('express-ws');
var os = require('os');
var pty = require('node-pty');
+/**
+ * Whether to use UTF8 binary transport.
+ * (Must also be switched in client.ts)
+ */
+const USE_BINARY_UTF8 = false;
+
+
function startServer() {
var app = express();
expressWs(app);
@@ -16,6 +23,10 @@ function startServer() {
res.sendFile(__dirname + '/index.html');
});
+ app.get('/test', function(req, res){
+ res.sendFile(__dirname + '/test.html');
+ });
+
app.get('/style.css', function(req, res){
res.sendFile(__dirname + '/style.css');
});
@@ -32,7 +43,8 @@ function startServer() {
cols: cols || 80,
rows: rows || 24,
cwd: process.env.PWD,
- env: process.env
+ env: process.env,
+ encoding: USE_BINARY_UTF8 ? null : 'utf8'
});
console.log('Created terminal with PID: ' + term.pid);
@@ -61,6 +73,7 @@ function startServer() {
console.log('Connected to terminal ' + term.pid);
ws.send(logs[term.pid]);
+ // string message buffering
function buffer(socket, timeout) {
let s = '';
let sender = null;
@@ -75,7 +88,25 @@ function startServer() {
}
};
}
- const send = buffer(ws, 5);
+ // binary message buffering
+ function bufferUtf8(socket, timeout) {
+ let buffer = [];
+ let sender = null;
+ let length = 0;
+ return (data) => {
+ buffer.push(data);
+ length += data.length;
+ if (!sender) {
+ sender = setTimeout(() => {
+ socket.send(Buffer.concat(buffer, length));
+ buffer = [];
+ sender = null;
+ length = 0;
+ }, timeout);
+ }
+ };
+ }
+ const send = USE_BINARY_UTF8 ? bufferUtf8(ws, 5) : buffer(ws, 5);
term.on('data', function(data) {
try {
diff --git a/demo/start.js b/demo/start.js
index 278c572f..7e13e790 100644
--- a/demo/start.js
+++ b/demo/start.js
@@ -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: {
diff --git a/demo/test.html b/demo/test.html
new file mode 100644
index 00000000..275c542d
--- /dev/null
+++ b/demo/test.html
@@ -0,0 +1,12 @@
+
+
+
+ xterm.js integration test fixture
+
+
+
+
+
+
+
+
diff --git a/fixtures/typings-test/typings-test.ts b/fixtures/typings-test/typings-test.ts
index 87d911c2..65c478c6 100644
--- a/fixtures/typings-test/typings-test.ts
+++ b/fixtures/typings-test/typings-test.ts
@@ -21,7 +21,7 @@ namespace constructor {
'disableStdin': false,
'rows': 1,
'scrollback': 10,
- 'tabStopWidth': 2,
+ 'tabStopWidth': 2
});
}
}
@@ -119,8 +119,8 @@ namespace methods_core {
const t: Terminal = new Terminal();
t.attachCustomKeyEventHandler((e: KeyboardEvent) => true);
t.attachCustomKeyEventHandler((e: KeyboardEvent) => false);
- const d1: IDisposable = t.addCsiHandler("x",
- (params: number[], collect: string): boolean => params[0]===1);
+ const d1: IDisposable = t.addCsiHandler('x',
+ (params: number[], collect: string): boolean => params[0] === 1);
d1.dispose();
const d2: IDisposable = t.addOscHandler(199,
(data: string): boolean => true);
@@ -155,6 +155,7 @@ namespace methods_core {
const r25: string = t.getOption('fontWeightBold');
const r26: boolean = t.getOption('allowTransparency');
const r27: boolean = t.getOption('rightClickSelectsWord');
+ const r28: boolean = t.getOption('windowsMode');
}
{
const t: Terminal = new Terminal();
@@ -177,6 +178,7 @@ namespace methods_core {
t.setOption('useFlowControl', true);
t.setOption('allowTransparency', true);
t.setOption('visualBell', true);
+ t.setOption('windowsMode', true);
t.setOption('colors', ['a', 'b']);
t.setOption('letterSpacing', 1);
t.setOption('cols', 1);
diff --git a/package.json b/package.json
index d58a6a5d..043b76cb 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "xterm",
"description": "Full xterm terminal, in your browser",
- "version": "3.12.0",
+ "version": "3.13.0",
"main": "lib/public/Terminal.js",
"types": "typings/xterm.d.ts",
"repository": "https://github.com/xtermjs/xterm.js",
@@ -12,6 +12,8 @@
"@types/jsdom": "11.0.1",
"@types/mocha": "^2.2.33",
"@types/node": "6.0.108",
+ "@types/puppeteer": "^1.12.4",
+ "@types/utf8": "^2.1.6",
"@types/webpack": "^4.4.11",
"browserify": "^13.3.0",
"chai": "3.5.0",
@@ -32,16 +34,21 @@
"node-pty": "0.7.6",
"nodemon": "1.10.2",
"nyc": "^11.8.0",
+ "puppeteer": "^1.15.0",
"sorcery": "^0.10.0",
"source-map-loader": "^0.2.4",
"ts-loader": "^4.5.0",
"tslint": "^5.9.1",
"tslint-consistent-codestyle": "^1.13.0",
"typescript": "3.4",
+ "utf8": "^3.0.0",
"vinyl-buffer": "^1.0.0",
"vinyl-source-stream": "^1.1.0",
"webpack": "^4.17.1",
"webpack-cli": "^3.1.0",
+ "xterm-addon-attach": "0.1.0-beta8",
+ "xterm-addon-search": "0.1.0-beta4",
+ "xterm-addon-web-links": "0.1.0-beta6",
"zmodem.js": "^0.1.5"
},
"scripts": {
@@ -54,11 +61,13 @@
"test-debug": "node --inspect-brk node_modules/.bin/gulp test",
"test-suite": "gulp mocha-suite --test",
"test-coverage": "nyc -x gulpfile.js -x '**/*test*' npm run mocha",
+ "test-api": "mocha \"**/*.api.js\"",
"mocha": "gulp test",
"prebuild": "tsc -b ./src/tsconfig.all.json",
"build": "gulp build",
- "prepublish": "npm run build",
+ "prepare": "npm run prebuild",
+ "prepublishOnly": "npm run build",
"coveralls": "nyc report --reporter=text-lcov | coveralls",
"watch": "tsc -b -w ./src/tsconfig.all.json --preserveWatchOutput"
}
-}
\ No newline at end of file
+}
diff --git a/src/AccessibilityManager.ts b/src/AccessibilityManager.ts
index 1f44c132..4351a336 100644
--- a/src/AccessibilityManager.ts
+++ b/src/AccessibilityManager.ts
@@ -249,9 +249,11 @@ export class AccessibilityManager extends Disposable {
const lineData = buffer.translateBufferLineToString(buffer.ydisp + i, true);
const posInSet = (buffer.ydisp + i + 1).toString();
const element = this._rowElements[i];
- element.textContent = lineData.length === 0 ? Strings.blankLine : lineData;
- element.setAttribute('aria-posinset', posInSet);
- element.setAttribute('aria-setsize', setSize);
+ if (element) {
+ element.textContent = lineData.length === 0 ? Strings.blankLine : lineData;
+ element.setAttribute('aria-posinset', posInSet);
+ element.setAttribute('aria-setsize', setSize);
+ }
}
}
diff --git a/src/Buffer.test.ts b/src/Buffer.test.ts
index d866fd56..087d0ae5 100644
--- a/src/Buffer.test.ts
+++ b/src/Buffer.test.ts
@@ -5,10 +5,10 @@
import { assert, expect } from 'chai';
import { ITerminal } from './Types';
-import { Buffer, DEFAULT_ATTR_DATA } from './Buffer';
+import { Buffer } from './Buffer';
import { CircularList } from './common/CircularList';
import { MockTerminal, TestTerminal } from './TestUtils.test';
-import { BufferLine, CellData } from './BufferLine';
+import { BufferLine, CellData, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
const INIT_COLS = 80;
const INIT_ROWS = 24;
diff --git a/src/Buffer.ts b/src/Buffer.ts
index c1c08c85..37d61d88 100644
--- a/src/Buffer.ts
+++ b/src/Buffer.ts
@@ -4,42 +4,14 @@
*/
import { CircularList, IInsertEvent } from './common/CircularList';
-import { ITerminal, IBuffer, IBufferLine, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult, ICellData, IAttributeData } from './Types';
-import { IMarker } from 'xterm';
-import { BufferLine, CellData, AttributeData } from './BufferLine';
-import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from './BufferReflow';
-import { DEFAULT_COLOR } from './renderer/atlas/Types';
-import { EventEmitter2, IEvent } from './common/EventEmitter2';
-import { Disposable } from '../lib/common/Lifecycle';
+import { ITerminal, IBuffer, BufferIndex, IBufferStringIterator, IBufferStringIteratorResult } from './Types';
+import { IBufferLine, ICellData, IAttributeData } from './core/Types';
+import { BufferLine, CellData, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_WIDTH, WHITESPACE_CELL_CODE, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
+import { reflowLargerApplyNewLayout, reflowLargerCreateNewLayout, reflowLargerGetLinesToRemove, reflowSmallerGetNewLineLengths, getWrappedLineTrimmedLength } from './core/buffer/BufferReflow';
+import { Marker } from './core/buffer/Marker';
-export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0);
-
-export const DEFAULT_ATTR_DATA = new AttributeData();
-
-export const CHAR_DATA_ATTR_INDEX = 0;
-export const CHAR_DATA_CHAR_INDEX = 1;
-export const CHAR_DATA_WIDTH_INDEX = 2;
-export const CHAR_DATA_CODE_INDEX = 3;
export const MAX_BUFFER_SIZE = 4294967295; // 2^32 - 1
-/**
- * Null cell - a real empty cell (containing nothing).
- * Note that code should always be 0 for a null cell as
- * several test condition of the buffer line rely on this.
- */
-export const NULL_CELL_CHAR = '';
-export const NULL_CELL_WIDTH = 1;
-export const NULL_CELL_CODE = 0;
-
-/**
- * Whitespace cell.
- * This is meant as a replacement for empty cells when needed
- * during rendering lines to preserve correct aligment.
- */
-export const WHITESPACE_CELL_CHAR = ' ';
-export const WHITESPACE_CELL_WIDTH = 1;
-export const WHITESPACE_CELL_CODE = 32;
-
/**
* This class represents a terminal buffer (an internal state of the terminal), where the
* following information is stored (in high-level):
@@ -629,33 +601,6 @@ export class Buffer implements IBuffer {
}
}
-export class Marker extends Disposable implements IMarker {
- private static _nextId = 1;
-
- private _id: number = Marker._nextId++;
- public isDisposed: boolean = false;
-
- public get id(): number { return this._id; }
-
- private _onDispose = new EventEmitter2();
- public get onDispose(): IEvent { return this._onDispose.event; }
-
- constructor(
- public line: number
- ) {
- super();
- }
-
- public dispose(): void {
- if (this.isDisposed) {
- return;
- }
- this.isDisposed = true;
- // Emit before super.dispose such that dispose listeners get a change to react
- this._onDispose.fire();
- }
-}
-
/**
* Iterator to get unwrapped content strings from the buffer.
* The iterator returns at least the string data between the borders
diff --git a/src/BufferSet.ts b/src/BufferSet.ts
index ba885a0e..f22b92dc 100644
--- a/src/BufferSet.ts
+++ b/src/BufferSet.ts
@@ -3,7 +3,8 @@
* @license MIT
*/
-import { ITerminal, IBufferSet, IAttributeData, IBuffer } from './Types';
+import { ITerminal, IBufferSet, IBuffer } from './Types';
+import { IAttributeData } from './core/Types';
import { Buffer } from './Buffer';
import { EventEmitter2, IEvent } from './common/EventEmitter2';
diff --git a/src/CharWidth.test.ts b/src/CharWidth.test.ts
index ff6f17ed..17822fa2 100644
--- a/src/CharWidth.test.ts
+++ b/src/CharWidth.test.ts
@@ -7,8 +7,7 @@ import { TestTerminal } from './TestUtils.test';
import { assert } from 'chai';
import { getStringCellWidth, wcwidth } from './CharWidth';
import { IBuffer } from './Types';
-import { CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './Buffer';
-import { CellData } from './BufferLine';
+import { CellData, CHAR_DATA_WIDTH_INDEX, CHAR_DATA_CHAR_INDEX } from './core/buffer/BufferLine';
describe('getStringCellWidth', function(): void {
diff --git a/src/InputHandler.test.ts b/src/InputHandler.test.ts
index dcd4d1b4..f2e8d93e 100644
--- a/src/InputHandler.test.ts
+++ b/src/InputHandler.test.ts
@@ -6,10 +6,9 @@
import { assert, expect } from 'chai';
import { InputHandler } from './InputHandler';
import { MockInputHandlingTerminal, TestTerminal } from './TestUtils.test';
-import { DEFAULT_ATTR_DATA } from './Buffer';
import { Terminal } from './Terminal';
-import { IBufferLine } from './Types';
-import { CellData, Attributes, AttributeData } from './BufferLine';
+import { IBufferLine } from './core/Types';
+import { CellData, Attributes, AttributeData, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
describe('InputHandler', () => {
describe('save and restore cursor', () => {
diff --git a/src/InputHandler.ts b/src/InputHandler.ts
index 66d16c04..ac4bee93 100644
--- a/src/InputHandler.ts
+++ b/src/InputHandler.ts
@@ -7,14 +7,13 @@
import { IInputHandler, IDcsHandler, IEscapeSequenceParser, IInputHandlingTerminal } from './Types';
import { C0, C1 } from './common/data/EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from './core/data/Charsets';
-import { NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR_DATA } from './Buffer';
import { wcwidth } from './CharWidth';
import { EscapeSequenceParser } from './EscapeSequenceParser';
import { IDisposable } from 'xterm';
import { Disposable } from './common/Lifecycle';
import { concat } from './common/TypedArrayUtils';
-import { StringToUtf32, stringFromCodePoint, utf32ToString } from './core/input/TextDecoder';
-import { CellData, Attributes, FgFlags, BgFlags, AttributeData } from './BufferLine';
+import { StringToUtf32, stringFromCodePoint, utf32ToString, Utf8ToUtf32 } from './core/input/TextDecoder';
+import { CellData, Attributes, FgFlags, BgFlags, AttributeData, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
import { EventEmitter2, IEvent } from './common/EventEmitter2';
/**
@@ -105,6 +104,7 @@ class DECRQSS implements IDcsHandler {
export class InputHandler extends Disposable implements IInputHandler {
private _parseBuffer: Uint32Array = new Uint32Array(4096);
private _stringDecoder: StringToUtf32 = new StringToUtf32();
+ private _utf8Decoder: Utf8ToUtf32 = new Utf8ToUtf32();
private _workCell: CellData = new CellData();
private _onCursorMove = new EventEmitter2();
@@ -319,6 +319,32 @@ export class InputHandler extends Disposable implements IInputHandler {
}
}
+ public parseUtf8(data: Uint8Array): void {
+ // Ensure the terminal is not disposed
+ if (!this._terminal) {
+ return;
+ }
+
+ let buffer = this._terminal.buffer;
+ const cursorStartX = buffer.x;
+ const cursorStartY = buffer.y;
+
+ // TODO: Consolidate debug/logging #1560
+ if ((this._terminal).debug) {
+ this._terminal.log('data: ' + data);
+ }
+
+ if (this._parseBuffer.length < data.length) {
+ this._parseBuffer = new Uint32Array(data.length);
+ }
+ this._parser.parse(this._parseBuffer, this._utf8Decoder.decode(data, this._parseBuffer));
+
+ buffer = this._terminal.buffer;
+ if (buffer.x !== cursorStartX || buffer.y !== cursorStartY) {
+ this._terminal.emit('cursormove');
+ }
+ }
+
public print(data: Uint32Array, start: number, end: number): void {
let code: number;
let chWidth: number;
diff --git a/src/Linkifier.test.ts b/src/Linkifier.test.ts
index 8f734bac..6a2f0ee9 100644
--- a/src/Linkifier.test.ts
+++ b/src/Linkifier.test.ts
@@ -4,11 +4,12 @@
*/
import { assert } from 'chai';
-import { IMouseZoneManager, IMouseZone, ILinkMatcher, ITerminal, IBufferLine } from './Types';
+import { IMouseZoneManager, IMouseZone, ILinkMatcher, ITerminal } from './Types';
+import { IBufferLine } from './core/Types';
import { Linkifier } from './Linkifier';
import { MockBuffer, MockTerminal, TestTerminal } from './TestUtils.test';
import { CircularList } from './common/CircularList';
-import { BufferLine, CellData } from './BufferLine';
+import { BufferLine, CellData } from './core/buffer/BufferLine';
class TestLinkifier extends Linkifier {
constructor(terminal: ITerminal) {
diff --git a/src/SelectionManager.test.ts b/src/SelectionManager.test.ts
index 9a2ac405..24ba0100 100644
--- a/src/SelectionManager.test.ts
+++ b/src/SelectionManager.test.ts
@@ -8,9 +8,10 @@ import { CharMeasure } from './CharMeasure';
import { SelectionManager, SelectionMode } from './SelectionManager';
import { SelectionModel } from './SelectionModel';
import { BufferSet } from './BufferSet';
-import { ITerminal, IBuffer, IBufferLine } from './Types';
+import { ITerminal, IBuffer } from './Types';
+import { IBufferLine } from './core/Types';
import { MockTerminal } from './TestUtils.test';
-import { BufferLine, CellData } from './BufferLine';
+import { BufferLine, CellData } from './core/buffer/BufferLine';
class TestMockTerminal extends MockTerminal {
emit(event: string, data: any): void {}
diff --git a/src/SelectionManager.ts b/src/SelectionManager.ts
index dcd60068..39d6c3a4 100644
--- a/src/SelectionManager.ts
+++ b/src/SelectionManager.ts
@@ -3,13 +3,14 @@
* @license MIT
*/
-import { ITerminal, ISelectionManager, IBuffer, IBufferLine, ISelectionRedrawRequestEvent } from './Types';
+import { ITerminal, ISelectionManager, IBuffer, ISelectionRedrawRequestEvent } from './Types';
+import { IBufferLine } from './core/Types';
import { MouseHelper } from './MouseHelper';
import * as Browser from './common/Platform';
import { CharMeasure } from './CharMeasure';
import { SelectionModel } from './SelectionModel';
import { AltClickHandler } from './handlers/AltClickHandler';
-import { CellData } from './BufferLine';
+import { CellData } from './core/buffer/BufferLine';
import { IDisposable } from 'xterm';
import { EventEmitter2, IEvent } from './common/EventEmitter2';
@@ -245,6 +246,7 @@ export class SelectionManager implements ISelectionManager {
this._model.clearSelection();
this._removeMouseDownListeners();
this.refresh();
+ this._onSelectionChange.fire();
}
/**
diff --git a/src/Terminal.integration.ts b/src/Terminal.integration.ts
index 10043006..fa4403f4 100644
--- a/src/Terminal.integration.ts
+++ b/src/Terminal.integration.ts
@@ -13,9 +13,8 @@ import * as path from 'path';
import * as pty from 'node-pty';
import { assert } from 'chai';
import { Terminal } from './Terminal';
-import { WHITESPACE_CELL_CHAR } from './Buffer';
import { IViewport } from './Types';
-import { CellData } from './BufferLine';
+import { CellData, WHITESPACE_CELL_CHAR } from './core/buffer/BufferLine';
class TestTerminal extends Terminal {
innerWrite(): void { this._innerWrite(); }
@@ -114,8 +113,8 @@ if (os.platform() !== 'win32') {
51, 52, 54, 55, 56, 57, 58, 59, 60, 61,
63, 68
];
+ // These are failing on macOS only
if (os.platform() === 'darwin') {
- // These are failing on macOS only
skip.push(3, 7, 11, 67);
}
for (let i = 0; i < files.length; i++) {
@@ -123,9 +122,9 @@ if (os.platform() !== 'win32') {
continue;
}
((filename: string) => {
+ const inFile = fs.readFileSync(filename, 'utf8');
it(filename.split('/').slice(-1)[0], done => {
ptyReset(() => {
- const inFile = fs.readFileSync(filename, 'utf8');
ptyWriteRead(inFile, fromPty => {
// uncomment this to get log from terminal
// console.log = function(){};
diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts
index 69fbb8e2..5d54e6f2 100644
--- a/src/Terminal.test.ts
+++ b/src/Terminal.test.ts
@@ -6,8 +6,7 @@
import { assert, expect } from 'chai';
import { Terminal } from './Terminal';
import { MockViewport, MockCompositionHelper, MockRenderer } from './TestUtils.test';
-import { DEFAULT_ATTR_DATA } from './Buffer';
-import { CellData } from './BufferLine';
+import { CellData, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
const INIT_COLS = 80;
const INIT_ROWS = 24;
@@ -17,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,
diff --git a/src/Terminal.ts b/src/Terminal.ts
index 1038ea8b..7f7816c7 100644
--- a/src/Terminal.ts
+++ b/src/Terminal.ts
@@ -21,10 +21,10 @@
* http://linux.die.net/man/7/urxvt
*/
-import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharacterJoinerHandler, IBufferLine, IAttributeData, IMouseZoneManager } from './Types';
+import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminalOptions, ITerminal, IBrowser, ILinkifier, ILinkMatcherOptions, CustomKeyEventHandler, LinkMatcherHandler, CharacterJoinerHandler, IMouseZoneManager } from './Types';
import { IRenderer } from './renderer/Types';
import { BufferSet } from './BufferSet';
-import { Buffer, MAX_BUFFER_SIZE, DEFAULT_ATTR_DATA } from './Buffer';
+import { Buffer, MAX_BUFFER_SIZE } from './Buffer';
import { CompositionHelper } from './CompositionHelper';
import { EventEmitter } from './common/EventEmitter';
import { Viewport } from './Viewport';
@@ -43,16 +43,16 @@ import { DEFAULT_BELL_SOUND, SoundManager } from './SoundManager';
import { MouseZoneManager } from './MouseZoneManager';
import { AccessibilityManager } from './AccessibilityManager';
import { ScreenDprMonitor } from './ui/ScreenDprMonitor';
-import { ITheme, IMarker, IDisposable } 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 } from './core/Types';
import { WebglRenderer } from './renderer/webgl/WebglRenderer';
+import { KeyboardResultType, ICharset, IBufferLine, IAttributeData } from './core/Types';
import { clone } from './common/Clone';
import { EventEmitter2, IEvent } from './common/EventEmitter2';
-import { Attributes } from './BufferLine';
+import { Attributes, DEFAULT_ATTR_DATA } from './core/buffer/BufferLine';
import { applyWindowsMode } from './WindowsMode';
// Let it work inside Node.js for automated testing purposes.
@@ -184,6 +184,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// user input states
public writeBuffer: string[];
+ public writeBufferUtf8: Uint8Array[];
private _writeInProgress: boolean;
/**
@@ -341,6 +342,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
// user input states
this.writeBuffer = [];
+ this.writeBufferUtf8 = [];
this._writeInProgress = false;
this._xoffSentToCatchUp = false;
@@ -1367,6 +1369,88 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
}
}
+ /**
+ * Writes raw utf8 bytes to the terminal.
+ * @param data UintArray with UTF8 bytes to write to the terminal.
+ */
+ public writeUtf8(data: Uint8Array): void {
+ // Ensure the terminal isn't disposed
+ if (this._isDisposed) {
+ return;
+ }
+
+ // Ignore falsy data values
+ if (!data) {
+ return;
+ }
+
+ this.writeBufferUtf8.push(data);
+
+ // Send XOFF to pause the pty process if the write buffer becomes too large so
+ // xterm.js can catch up before more data is sent. This is necessary in order
+ // to keep signals such as ^C responsive.
+ if (this.options.useFlowControl && !this._xoffSentToCatchUp && this.writeBufferUtf8.length >= WRITE_BUFFER_PAUSE_THRESHOLD) {
+ // XOFF - stop pty pipe
+ // XON will be triggered by emulator before processing data chunk
+ this.handler(C0.DC3);
+ this._xoffSentToCatchUp = true;
+ }
+
+ if (!this._writeInProgress && this.writeBufferUtf8.length > 0) {
+ // Kick off a write which will write all data in sequence recursively
+ this._writeInProgress = true;
+ // Kick off an async innerWrite so more writes can come in while processing data
+ setTimeout(() => {
+ this._innerWriteUtf8();
+ });
+ }
+ }
+
+ protected _innerWriteUtf8(bufferOffset: number = 0): void {
+ // Ensure the terminal isn't disposed
+ if (this._isDisposed) {
+ this.writeBufferUtf8 = [];
+ }
+
+ const startTime = Date.now();
+ while (this.writeBufferUtf8.length > bufferOffset) {
+ const data = this.writeBufferUtf8[bufferOffset];
+ bufferOffset++;
+
+ // If XOFF was sent in order to catch up with the pty process, resume it if
+ // we reached the end of the writeBuffer to allow more data to come in.
+ if (this._xoffSentToCatchUp && this.writeBufferUtf8.length === bufferOffset) {
+ this.handler(C0.DC1);
+ this._xoffSentToCatchUp = false;
+ }
+
+ this._refreshStart = this.buffer.y;
+ this._refreshEnd = this.buffer.y;
+
+ // HACK: Set the parser state based on it's state at the time of return.
+ // This works around the bug #662 which saw the parser state reset in the
+ // middle of parsing escape sequence in two chunks. For some reason the
+ // state of the parser resets to 0 after exiting parser.parse. This change
+ // just sets the state back based on the correct return statement.
+
+ this._inputHandler.parseUtf8(data);
+
+ this.updateRange(this.buffer.y);
+ this.refresh(this._refreshStart, this._refreshEnd);
+
+ if (Date.now() - startTime >= WRITE_TIMEOUT_MS) {
+ break;
+ }
+ }
+ if (this.writeBufferUtf8.length > bufferOffset) {
+ // Allow renderer to catch up before processing the next batch
+ setTimeout(() => this._innerWriteUtf8(bufferOffset), 0);
+ } else {
+ this._writeInProgress = false;
+ this.writeBufferUtf8 = [];
+ }
+ }
+
/**
* Writes text to the terminal.
* @param data The text to write to the terminal.
@@ -1537,6 +1621,16 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
return this.selectionManager ? this.selectionManager.hasSelection : false;
}
+ /**
+ * Selects text within the terminal.
+ * @param column The column the selection starts at..
+ * @param row The row the selection starts at.
+ * @param length The length of the selection.
+ */
+ public select(column: number, row: number, length: number): void {
+ this.selectionManager.setSelection(column, row, length);
+ }
+
/**
* Gets the terminal's current selection, this is useful for implementing copy
* behavior outside of xterm.js.
@@ -1545,6 +1639,19 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
return this.selectionManager ? this.selectionManager.selectionText : '';
}
+ public getSelectionPosition(): ISelectionPosition | undefined {
+ if (!this.selectionManager.hasSelection) {
+ return undefined;
+ }
+
+ return {
+ startColumn: this.selectionManager.selectionStart[0],
+ startRow: this.selectionManager.selectionStart[1],
+ endColumn: this.selectionManager.selectionEnd[0],
+ endRow: this.selectionManager.selectionEnd[1]
+ };
+ }
+
/**
* Clears the current terminal selection.
*/
diff --git a/src/TestUtils.test.ts b/src/TestUtils.test.ts
index 92018d1f..b43f262b 100644
--- a/src/TestUtils.test.ts
+++ b/src/TestUtils.test.ts
@@ -4,13 +4,14 @@
*/
import { IColorSet, IRenderer, IRenderDimensions, IColorManager } from './renderer/Types';
-import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferLine, IBufferStringIterator, ICellData, IAttributeData } from './Types';
+import { IInputHandlingTerminal, IViewport, ICompositionHelper, ITerminal, IBuffer, IBufferSet, IBrowser, ICharMeasure, ISelectionManager, ITerminalOptions, ILinkifier, IMouseHelper, ILinkMatcherOptions, CharacterJoinerHandler, IBufferStringIterator } from './Types';
+import { IBufferLine, ICellData, IAttributeData } from './core/Types';
import { ICircularList, XtermListener } from './common/Types';
import { Buffer } from './Buffer';
import * as Browser from './common/Platform';
-import { ITheme, IDisposable, IMarker, IEvent } from 'xterm';
+import { ITheme, IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm';
import { Terminal } from './Terminal';
-import { AttributeData } from './BufferLine';
+import { AttributeData } from './core/buffer/BufferLine';
export class TestTerminal extends Terminal {
writeSync(data: string): void {
@@ -82,9 +83,15 @@ export class MockTerminal implements ITerminal {
getSelection(): string {
throw new Error('Method not implemented.');
}
+ getSelectionPosition(): ISelectionPosition | undefined {
+ throw new Error('Method not implemented.');
+ }
clearSelection(): void {
throw new Error('Method not implemented.');
}
+ select(column: number, row: number, length: number): void {
+ throw new Error('Method not implemented.');
+ }
selectAll(): void {
throw new Error('Method not implemented.');
}
@@ -109,6 +116,9 @@ export class MockTerminal implements ITerminal {
write(data: string): void {
throw new Error('Method not implemented.');
}
+ writeUtf8(data: Uint8Array): void {
+ throw new Error('Method not implemented.');
+ }
bracketedPasteMode: boolean;
mouseHelper: IMouseHelper;
renderer: IRenderer;
diff --git a/src/Types.ts b/src/Types.ts
index 1e9ac642..a9f30e3d 100644
--- a/src/Types.ts
+++ b/src/Types.ts
@@ -3,15 +3,14 @@
* @license MIT
*/
-import { Terminal as PublicTerminal, ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable } from 'xterm';
+import { ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable, IMarker, ISelectionPosition } from 'xterm';
import { IColorSet, IRenderer } from './renderer/Types';
-import { ICharset } from './core/Types';
+import { ICharset, IAttributeData, ICellData, IBufferLine, CharData } from './core/Types';
import { ICircularList } from './common/Types';
import { IEvent } from './common/EventEmitter2';
export type CustomKeyEventHandler = (event: KeyboardEvent) => boolean;
-export type CharData = [number, string, number, number];
export type LineData = CharData[];
export type LinkMatcherHandler = (event: MouseEvent, uri: string) => void;
@@ -104,6 +103,7 @@ export interface ICompositionHelper {
*/
export interface IInputHandler {
parse(data: string): void;
+ parseUtf8(data: Uint8Array): void;
print(data: Uint32Array, start: number, end: number): void;
/** C0 BEL */ bell(): void;
@@ -196,7 +196,7 @@ export interface ILinkifierEvent {
fg: number;
}
-export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
+export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAccessor, ILinkifierAccessor {
screenElement: HTMLElement;
selectionManager: ISelectionManager;
charMeasure: ICharMeasure;
@@ -221,6 +221,58 @@ export interface ITerminal extends PublicTerminal, IElementAccessor, IBufferAcce
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;
+ onData: IEvent;
+ onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>;
+ onLineFeed: IEvent;
+ onScroll: IEvent;
+ onSelectionChange: IEvent;
+ onRender: IEvent<{ start: number, end: number }>;
+ onResize: IEvent<{ cols: number, rows: number }>;
+ onTitleChange: IEvent;
+ 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 {
buffer: IBuffer;
}
@@ -526,85 +578,6 @@ export interface IEscapeSequenceParser extends IDisposable {
clearErrorHandler(): void;
}
-/** RGB color type */
-export type IColorRGB = [number, number, number];
-
-/** Attribute data */
-export interface IAttributeData {
- fg: number;
- bg: number;
-
- clone(): IAttributeData;
-
- // flags
- isInverse(): number;
- isBold(): number;
- isUnderline(): number;
- isBlink(): number;
- isInvisible(): number;
- isItalic(): number;
- isDim(): number;
-
- // color modes
- getFgColorMode(): number;
- getBgColorMode(): number;
- isFgRGB(): boolean;
- isBgRGB(): boolean;
- isFgPalette(): boolean;
- isBgPalette(): boolean;
- isFgDefault(): boolean;
- isBgDefault(): boolean;
-
- // colors
- getFgColor(): number;
- getBgColor(): number;
-}
-
-/** Cell data */
-export interface ICellData extends IAttributeData {
- content: number;
- combinedData: string;
- isCombined(): number;
- getWidth(): number;
- getChars(): string;
- getCode(): number;
- setFromCharData(value: CharData): void;
- getAsCharData(): CharData;
-}
-
-/**
- * Interface for a line in the terminal buffer.
- */
-export interface IBufferLine {
- length: number;
- isWrapped: boolean;
- get(index: number): CharData;
- set(index: number, value: CharData): void;
- loadCell(index: number, cell: ICellData): ICellData;
- setCell(index: number, cell: ICellData): void;
- setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void;
- addCodepointToCell(index: number, codePoint: number): void;
- insertCells(pos: number, n: number, ch: ICellData): void;
- deleteCells(pos: number, n: number, fill: ICellData): void;
- replaceCells(start: number, end: number, fill: ICellData): void;
- resize(cols: number, fill: ICellData): void;
- fill(fillCellData: ICellData): void;
- copyFrom(line: IBufferLine): void;
- clone(): IBufferLine;
- getTrimmedLength(): number;
- translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string;
-
- /* direct access to cell attrs */
- getWidth(index: number): number;
- hasWidth(index: number): number;
- getFg(index: number): number;
- getBg(index: number): number;
- hasContent(index: number): number;
- getCodePoint(index: number): number;
- isCombined(index: number): number;
- getString(index: number): string;
-}
-
export interface IMouseZoneManager extends IDisposable {
add(zone: IMouseZone): void;
clearAll(start?: number, end?: number): void;
diff --git a/src/WindowsMode.ts b/src/WindowsMode.ts
index ac1d193e..d7b4bcae 100644
--- a/src/WindowsMode.ts
+++ b/src/WindowsMode.ts
@@ -5,7 +5,7 @@
import { IDisposable } from 'xterm';
import { ITerminal } from './Types';
-import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from './Buffer';
+import { CHAR_DATA_CODE_INDEX, NULL_CELL_CODE, WHITESPACE_CELL_CODE } from './core/buffer/BufferLine';
export function applyWindowsMode(terminal: ITerminal): IDisposable {
// Winpty does not support wraparound mode which means that lines will never
diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts
index a1f05895..872537ea 100644
--- a/src/addons/search/Interfaces.ts
+++ b/src/addons/search/Interfaces.ts
@@ -5,15 +5,8 @@
import { Terminal } from 'xterm';
-// TODO: Don't rely on this private API
-export interface ITerminalCore {
- buffer: any;
- selectionManager: any;
-}
-
export interface ISearchAddonTerminal extends Terminal {
__searchHelper?: ISearchHelper;
- _core: ITerminalCore;
}
export interface ISearchHelper {
diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts
index 7db1ed43..e23b14a1 100644
--- a/src/addons/search/SearchHelper.ts
+++ b/src/addons/search/SearchHelper.ts
@@ -35,25 +35,23 @@ export class SearchHelper implements ISearchHelper {
* @return Whether a result was found.
*/
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
- const selectionManager = this._terminal._core.selectionManager;
const {incremental} = searchOptions;
let result: ISearchResult;
if (!term || term.length === 0) {
- selectionManager.clearSelection();
+ this._terminal.clearSelection();
return false;
}
let startCol: number = 0;
- let startRow = this._terminal._core.buffer.ydisp;
+ let startRow = this._terminal.buffer.viewportY;
- if (selectionManager.selectionEnd) {
+ if (this._terminal.hasSelection()) {
// Start from the selection end if there is a selection
// For incremental search, use existing row
- if (this._terminal.getSelection().length !== 0) {
- startRow = incremental ? selectionManager.selectionStart[1] : selectionManager.selectionEnd[1];
- startCol = incremental ? selectionManager.selectionStart[0] : selectionManager.selectionEnd[0];
- }
+ const currentSelection = this._terminal.getSelectionPosition();
+ startRow = incremental ? currentSelection.startRow : currentSelection.endRow;
+ startCol = incremental ? currentSelection.startColumn : currentSelection.endColumn;
}
this._initLinesCache();
@@ -64,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;
}
@@ -75,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
@@ -109,24 +107,22 @@ export class SearchHelper implements ISearchHelper {
* @return Whether a result was found.
*/
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
- const selectionManager = this._terminal._core.selectionManager;
let result: ISearchResult;
if (!term || term.length === 0) {
- selectionManager.clearSelection();
+ this._terminal.clearSelection();
return false;
}
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) {
+ if (this._terminal.hasSelection()) {
// Start from the selection start if there is a selection
- if (this._terminal.getSelection().length !== 0) {
- startRow = selectionManager.selectionStart[1];
- startCol = selectionManager.selectionStart[0];
- }
+ const currentSelection = this._terminal.getSelectionPosition();
+ startRow = currentSelection.startRow;
+ startCol = currentSelection.startColumn;
}
this._initLinesCache();
@@ -139,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--) {
@@ -149,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;
@@ -160,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;
@@ -184,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());
}
@@ -234,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;
@@ -286,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++;
}
@@ -322,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);
@@ -341,8 +337,8 @@ export class SearchHelper implements ISearchHelper {
this._terminal.clearSelection();
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.select(result.col, result.row, result.term.length);
+ this._terminal.scrollLines(result.row - this._terminal.buffer.viewportY);
return true;
}
}
diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts
index 6551fa8b..aee88f50 100644
--- a/src/addons/search/search.test.ts
+++ b/src/addons/search/search.test.ts
@@ -23,11 +23,56 @@ class MockTerminal {
get core(): any {
return this._core;
}
+ get buffer(): IBuffer {
+ // TODO: This is a hacky workaround until we use puppeteer for addon tests
+ const buffer = this._core.buffer;
+ return {
+ cursorY: buffer.y,
+ cursorX: buffer.x,
+ viewportY: buffer.ydisp,
+ baseY: buffer.ybase,
+ length: buffer.length,
+ getLine(y: number): IBufferLine {
+ return {
+ isWrapped: buffer.lines.get(y) ? buffer.lines.get(y).isWrapped : false,
+ getCell(x: number): IBufferCell {
+ return {
+ char: buffer.lines.get(y).get(x)[1/*CHAR_DATA_CHAR_INDEX*/],
+ width: buffer.lines.get(y).get(x)[2/*CHAR_DATA_WIDTH_INDEX*/]
+ };
+ },
+ translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string {
+ return buffer.translateBufferLineToString(y, trimRight);
+ }
+ };
+ }
+ };
+ }
pushWriteData(): void {
this._core._innerWrite();
}
}
+interface IBuffer {
+ readonly cursorY: number;
+ readonly cursorX: number;
+ readonly viewportY: number;
+ readonly baseY: number;
+ readonly length: number;
+ getLine(y: number): IBufferLine | undefined;
+}
+
+interface IBufferLine {
+ readonly isWrapped: boolean;
+ getCell(x: number): IBufferCell;
+ translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string;
+}
+
+interface IBufferCell {
+ readonly char: string;
+ readonly width: number;
+}
+
class TestSearchHelper extends SearchHelper {
public findInLine(term: string, rowNumber: number, searchOptions?: ISearchOptions): ISearchResult {
return this._findInLine(term, rowNumber, 0, searchOptions);
diff --git a/src/addons/webLinks/webLinks.test.ts b/src/addons/webLinks/webLinks.test.ts
index 1e8a4ae7..da5569ab 100644
--- a/src/addons/webLinks/webLinks.test.ts
+++ b/src/addons/webLinks/webLinks.test.ts
@@ -28,63 +28,185 @@ describe('webLinks addon', () => {
});
});
- it('should allow ~ character in URI path', () => {
- const term = new MockTerminal();
- webLinks.webLinksInit(term);
+ describe('should allow simple URI path', () => {
+ it('foo.com', () => {
+ const term = new MockTerminal();
+ webLinks.webLinksInit(term);
- const row = ' http://foo.com/a~b#c~d?e~f ';
+ const row = ' http://foo.com ';
- const match = row.match(term.regex);
- const uri = match[term.options.matchIndex];
+ const match = row.match(term.regex);
+ const uri = match[term.options.matchIndex];
- assert.equal(uri, 'http://foo.com/a~b#c~d?e~f');
+ 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');
+ });
});
- it('should allow : character in URI path', () => {
- const term = new MockTerminal();
- webLinks.webLinksInit(term);
+ 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 row = ' http://foo.com/a~b#c~d?e~f ';
- const match = row.match(term.regex);
- const uri = match[term.options.matchIndex];
+ const match = row.match(term.regex);
+ const uri = match[term.options.matchIndex];
- assert.equal(uri, 'http://foo.com/colon:test');
+ 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');
+ });
});
- it('should not allow : character at the end of a URI path', () => {
- const term = new MockTerminal();
- webLinks.webLinksInit(term);
+ 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 row = ' http://foo.com/colon:test ';
- const match = row.match(term.regex);
- const uri = match[term.options.matchIndex];
+ const match = row.match(term.regex);
+ const uri = match[term.options.matchIndex];
- assert.equal(uri, 'http://foo.com/colon:test');
+ 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');
+ });
});
- it('should not allow " character at the end of a URI enclosed with ""', () => {
- const term = new MockTerminal();
- webLinks.webLinksInit(term);
+ 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/"';
+ const row = ' http://foo.com/colon:test: ';
- const match = row.match(term.regex);
- const uri = match[term.options.matchIndex];
+ const match = row.match(term.regex);
+ const uri = match[term.options.matchIndex];
- assert.equal(uri, 'http://foo.com/');
+ 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');
+ });
});
- it('should not allow \' character at the end of a URI enclosed with \'\'', () => {
- const term = new MockTerminal();
- webLinks.webLinksInit(term);
+ 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 row = '"http://foo.com/"';
- const match = row.match(term.regex);
- const uri = match[term.options.matchIndex];
+ const match = row.match(term.regex);
+ const uri = match[term.options.matchIndex];
- assert.equal(uri, 'http://foo.com/');
+ 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
index f0d69cc5..8a0fec09 100644
--- a/src/addons/webLinks/webLinks.ts
+++ b/src/addons/webLinks/webLinks.ts
@@ -14,7 +14,8 @@ 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 pathCharacterSet = '(\\/[\\/\\w\\.\\-%~:+]*)*([^:"\'\\s])';
+const pathClause = '(' + pathCharacterSet + ')?';
const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*';
const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?';
const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?';
diff --git a/src/common/Types.ts b/src/common/Types.ts
index b2111bfc..717de359 100644
--- a/src/common/Types.ts
+++ b/src/common/Types.ts
@@ -6,6 +6,8 @@
import { IEvent, EventEmitter2 } from './EventEmitter2';
import { IDeleteEvent, IInsertEvent } from './CircularList';
+export const DEFAULT_COLOR = 256;
+
export interface IDisposable {
dispose(): void;
}
diff --git a/src/core/Types.ts b/src/core/Types.ts
index 4001e448..5b97f249 100644
--- a/src/core/Types.ts
+++ b/src/core/Types.ts
@@ -3,6 +3,8 @@
* @license MIT
*/
+import { IDisposable } from '../common/Types';
+
export const enum KeyboardResultType {
SEND_KEY,
SELECT_ALL,
@@ -19,3 +21,88 @@ export interface IKeyboardResult {
export interface ICharset {
[key: string]: string;
}
+
+export type CharData = [number, string, number, number];
+export type IColorRGB = [number, number, number];
+
+/** Attribute data */
+export interface IAttributeData {
+ fg: number;
+ bg: number;
+
+ clone(): IAttributeData;
+
+ // flags
+ isInverse(): number;
+ isBold(): number;
+ isUnderline(): number;
+ isBlink(): number;
+ isInvisible(): number;
+ isItalic(): number;
+ isDim(): number;
+
+ // color modes
+ getFgColorMode(): number;
+ getBgColorMode(): number;
+ isFgRGB(): boolean;
+ isBgRGB(): boolean;
+ isFgPalette(): boolean;
+ isBgPalette(): boolean;
+ isFgDefault(): boolean;
+ isBgDefault(): boolean;
+
+ // colors
+ getFgColor(): number;
+ getBgColor(): number;
+}
+
+/** Cell data */
+export interface ICellData extends IAttributeData {
+ content: number;
+ combinedData: string;
+ isCombined(): number;
+ getWidth(): number;
+ getChars(): string;
+ getCode(): number;
+ setFromCharData(value: CharData): void;
+ getAsCharData(): CharData;
+}
+
+/**
+ * Interface for a line in the terminal buffer.
+ */
+export interface IBufferLine {
+ length: number;
+ isWrapped: boolean;
+ get(index: number): CharData;
+ set(index: number, value: CharData): void;
+ loadCell(index: number, cell: ICellData): ICellData;
+ setCell(index: number, cell: ICellData): void;
+ setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void;
+ addCodepointToCell(index: number, codePoint: number): void;
+ insertCells(pos: number, n: number, ch: ICellData): void;
+ deleteCells(pos: number, n: number, fill: ICellData): void;
+ replaceCells(start: number, end: number, fill: ICellData): void;
+ resize(cols: number, fill: ICellData): void;
+ fill(fillCellData: ICellData): void;
+ copyFrom(line: IBufferLine): void;
+ clone(): IBufferLine;
+ getTrimmedLength(): number;
+ translateToString(trimRight?: boolean, startCol?: number, endCol?: number): string;
+
+ /* direct access to cell attrs */
+ getWidth(index: number): number;
+ hasWidth(index: number): number;
+ getFg(index: number): number;
+ getBg(index: number): number;
+ hasContent(index: number): number;
+ getCodePoint(index: number): number;
+ isCombined(index: number): number;
+ getString(index: number): string;
+}
+
+export interface IMarker extends IDisposable {
+ readonly id: number;
+ readonly isDisposed: boolean;
+ readonly line: number;
+}
diff --git a/src/BufferLine.test.ts b/src/core/buffer/BufferLine.test.ts
similarity index 95%
rename from src/BufferLine.test.ts
rename to src/core/buffer/BufferLine.test.ts
index 5b029cb8..c42b372e 100644
--- a/src/BufferLine.test.ts
+++ b/src/core/buffer/BufferLine.test.ts
@@ -3,10 +3,8 @@
* @license MIT
*/
import * as chai from 'chai';
-import { BufferLine, CellData, Content } from './BufferLine';
-import { CharData, IBufferLine } from './Types';
-import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './Buffer';
-
+import { BufferLine, CellData, Content, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE, DEFAULT_ATTR } from './BufferLine';
+import { CharData, IBufferLine } from '../Types';
class TestBufferLine extends BufferLine {
public get combined(): {[index: number]: string} {
@@ -57,7 +55,7 @@ describe('BufferLine', function(): void {
chai.expect(line.length).equals(10);
chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
chai.expect(line.isWrapped).equals(false);
- line = new TestBufferLine(10, null, true);
+ line = new TestBufferLine(10, undefined, true);
chai.expect(line.length).equals(10);
chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
chai.expect(line.isWrapped).equals(true);
@@ -127,7 +125,7 @@ describe('BufferLine', function(): void {
]);
});
it('clone', function(): void {
- const line = new TestBufferLine(5, null, true);
+ const line = new TestBufferLine(5, undefined, true);
line.setCell(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
line.setCell(1, CellData.fromCharData([2, 'b', 0, 'b'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([3, 'c', 0, 'c'.charCodeAt(0)]));
@@ -167,27 +165,27 @@ describe('BufferLine', function(): void {
it('enlarge(false)', function(): void {
const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
- chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
+ chai.expect(line.toArray()).eql((Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('enlarge(true)', function(): void {
const line = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
- chai.expect(line.toArray()).eql(Array(10).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
+ chai.expect(line.toArray()).eql((Array(10) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink(true) - should apply new size', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
- chai.expect(line.toArray()).eql(Array(5).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
+ chai.expect(line.toArray()).eql((Array(5) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('shrink to 0 length', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
line.resize(0, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
- chai.expect(line.toArray()).eql(Array(0).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
+ chai.expect(line.toArray()).eql((Array(0) as any).fill([1, 'a', 0, 'a'.charCodeAt(0)]));
});
it('should remove combining data on replaced cells after shrinking then enlarging', () => {
const line = new TestBufferLine(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), false);
- line.set(2, [ null, '😁', 1, '😁'.charCodeAt(0) ]);
- line.set(9, [ null, '😁', 1, '😁'.charCodeAt(0) ]);
+ line.set(2, [ 0, '😁', 1, '😁'.charCodeAt(0) ]);
+ line.set(9, [ 0, '😁', 1, '😁'.charCodeAt(0) ]);
chai.expect(line.translateToString()).eql('aa😁aaaaaa😁');
chai.expect(Object.keys(line.combined).length).eql(2);
line.resize(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
@@ -224,7 +222,7 @@ describe('BufferLine', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
- line.setCell(3, CellData.fromCharData([0, '', 0, undefined]));
+ line.setCell(3, CellData.fromCharData([0, '', 0, 0]));
chai.expect(line.getTrimmedLength()).equal(4); // also counts null cell after fullwidth
});
});
@@ -284,11 +282,11 @@ describe('BufferLine', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(2, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
- line.setCell(3, CellData.fromCharData([0, '', 0, undefined]));
+ line.setCell(3, CellData.fromCharData([0, '', 0, 0]));
line.setCell(5, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
- line.setCell(6, CellData.fromCharData([0, '', 0, undefined]));
+ line.setCell(6, CellData.fromCharData([0, '', 0, 0]));
line.setCell(7, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
- line.setCell(8, CellData.fromCharData([0, '', 0, undefined]));
+ line.setCell(8, CellData.fromCharData([0, '', 0, 0]));
chai.expect(line.translateToString(false)).equal('a 1 11 ');
chai.expect(line.translateToString(true)).equal('a 1 11');
chai.expect(line.translateToString(false, 0, 7)).equal('a 1 1');
diff --git a/src/BufferLine.ts b/src/core/buffer/BufferLine.ts
similarity index 93%
rename from src/BufferLine.ts
rename to src/core/buffer/BufferLine.ts
index 0bf4dd69..a1d114fd 100644
--- a/src/BufferLine.ts
+++ b/src/core/buffer/BufferLine.ts
@@ -2,11 +2,46 @@
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
-import { CharData, IBufferLine, ICellData, IColorRGB, IAttributeData } from './Types';
-import { NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, CHAR_DATA_CHAR_INDEX, CHAR_DATA_WIDTH_INDEX, WHITESPACE_CELL_CHAR, CHAR_DATA_ATTR_INDEX } from './Buffer';
-import { stringFromCodePoint } from './core/input/TextDecoder';
-import { FLAGS } from './renderer/Types';
+import { CharData, IBufferLine, ICellData, IColorRGB, IAttributeData } from '../Types';
+import { stringFromCodePoint } from '../input/TextDecoder';
+import { DEFAULT_COLOR } from '../../common/Types';
+
+export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0);
+
+// TODO: This is duplicated from renderer, should be removed after chardata workaround is fixed
+export const enum FLAGS {
+ BOLD = 1,
+ UNDERLINE = 2,
+ BLINK = 4,
+ INVERSE = 8,
+ INVISIBLE = 16,
+ DIM = 32,
+ ITALIC = 64
+}
+
+export const CHAR_DATA_ATTR_INDEX = 0;
+export const CHAR_DATA_CHAR_INDEX = 1;
+export const CHAR_DATA_WIDTH_INDEX = 2;
+export const CHAR_DATA_CODE_INDEX = 3;
+
+/**
+ * Null cell - a real empty cell (containing nothing).
+ * Note that code should always be 0 for a null cell as
+ * several test condition of the buffer line rely on this.
+ */
+export const NULL_CELL_CHAR = '';
+export const NULL_CELL_WIDTH = 1;
+export const NULL_CELL_CODE = 0;
+
+/**
+ * Whitespace cell.
+ * This is meant as a replacement for empty cells when needed
+ * during rendering lines to preserve correct aligment.
+ */
+export const WHITESPACE_CELL_CHAR = ' ';
+export const WHITESPACE_CELL_WIDTH = 1;
+export const WHITESPACE_CELL_CODE = 32;
/**
* buffer memory layout:
@@ -194,6 +229,8 @@ export class AttributeData implements IAttributeData {
}
}
+export const DEFAULT_ATTR_DATA = new AttributeData();
+
/**
* CellData - represents a single Cell in the terminal buffer.
*/
@@ -300,17 +337,15 @@ export class CellData extends AttributeData implements ICellData {
* memory allocs / GC pressure can be greatly reduced by reusing the CellData object.
*/
export class BufferLine implements IBufferLine {
- protected _data: Uint32Array | null = null;
+ protected _data: Uint32Array;
protected _combined: {[index: number]: string} = {};
public length: number;
constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) {
- if (cols) {
- this._data = new Uint32Array(cols * CELL_SIZE);
- const cell = fillCellData || CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
- for (let i = 0; i < cols; ++i) {
- this.setCell(i, cell);
- }
+ this._data = new Uint32Array(cols * CELL_SIZE);
+ const cell = fillCellData || CellData.fromCharData([0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
+ for (let i = 0; i < cols; ++i) {
+ this.setCell(i, cell);
}
this.length = cols;
}
@@ -566,7 +601,7 @@ export class BufferLine implements IBufferLine {
}
}
} else {
- this._data = null;
+ this._data = new Uint32Array(0);
this._combined = {};
}
}
diff --git a/src/BufferReflow.test.ts b/src/core/buffer/BufferReflow.test.ts
similarity index 69%
rename from src/BufferReflow.test.ts
rename to src/core/buffer/BufferReflow.test.ts
index 9c978dc0..d0d97dff 100644
--- a/src/BufferReflow.test.ts
+++ b/src/core/buffer/BufferReflow.test.ts
@@ -3,18 +3,17 @@
* @license MIT
*/
import { assert } from 'chai';
-import { BufferLine } from './BufferLine';
+import { BufferLine, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './BufferLine';
import { reflowSmallerGetNewLineLengths } from './BufferReflow';
-import { NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from './Buffer';
describe('BufferReflow', () => {
describe('reflowSmallerGetNewLineLengths', () => {
it('should return correct line lengths for a small line with wide characters', () => {
const line = new BufferLine(4);
- line.set(0, [null, '汉', 2, '汉'.charCodeAt(0)]);
- line.set(1, [null, '', 0, undefined]);
- line.set(2, [null, '语', 2, '语'.charCodeAt(0)]);
- line.set(3, [null, '', 0, undefined]);
+ line.set(0, [0, '汉', 2, '汉'.charCodeAt(0)]);
+ line.set(1, [0, '', 0, 0]);
+ line.set(2, [0, '语', 2, '语'.charCodeAt(0)]);
+ line.set(3, [0, '', 0, 0]);
assert.equal(line.translateToString(true), '汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 3), [2, 2], 'line: 汉, 语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 2), [2, 2], 'line: 汉, 语');
@@ -22,12 +21,12 @@ describe('BufferReflow', () => {
it('should return correct line lengths for a large line with wide characters', () => {
const line = new BufferLine(12);
for (let i = 0; i < 12; i += 4) {
- line.set(i, [null, '汉', 2, '汉'.charCodeAt(0)]);
- line.set(i + 2, [null, '语', 2, '语'.charCodeAt(0)]);
+ line.set(i, [0, '汉', 2, '汉'.charCodeAt(0)]);
+ line.set(i + 2, [0, '语', 2, '语'.charCodeAt(0)]);
}
for (let i = 1; i < 12; i += 2) {
- line.set(i, [null, '', 0, undefined]);
- line.set(i, [null, '', 0, undefined]);
+ line.set(i, [0, '', 0, 0]);
+ line.set(i, [0, '', 0, 0]);
}
assert.equal(line.translateToString(), '汉语汉语汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 11), [10, 2], 'line: 汉语汉语汉, 语');
@@ -43,12 +42,12 @@ describe('BufferReflow', () => {
});
it('should return correct line lengths for a string with wide and single characters', () => {
const line = new BufferLine(6);
- line.set(0, [null, 'a', 1, 'a'.charCodeAt(0)]);
- line.set(1, [null, '汉', 2, '汉'.charCodeAt(0)]);
- line.set(2, [null, '', 0, undefined]);
- line.set(3, [null, '语', 2, '语'.charCodeAt(0)]);
- line.set(4, [null, '', 0, undefined]);
- line.set(5, [null, 'b', 1, 'b'.charCodeAt(0)]);
+ line.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]);
+ line.set(1, [0, '汉', 2, '汉'.charCodeAt(0)]);
+ line.set(2, [0, '', 0, 0]);
+ line.set(3, [0, '语', 2, '语'.charCodeAt(0)]);
+ line.set(4, [0, '', 0, 0]);
+ line.set(5, [0, 'b', 1, 'b'.charCodeAt(0)]);
assert.equal(line.translateToString(), 'a汉语b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 5), [5, 1], 'line: a汉语b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 4), [3, 3], 'line: a汉, 语b');
@@ -57,19 +56,19 @@ describe('BufferReflow', () => {
});
it('should return correct line lengths for a wrapped line with wide and single characters', () => {
const line1 = new BufferLine(6);
- line1.set(0, [null, 'a', 1, 'a'.charCodeAt(0)]);
- line1.set(1, [null, '汉', 2, '汉'.charCodeAt(0)]);
- line1.set(2, [null, '', 0, undefined]);
- line1.set(3, [null, '语', 2, '语'.charCodeAt(0)]);
- line1.set(4, [null, '', 0, undefined]);
- line1.set(5, [null, 'b', 1, 'b'.charCodeAt(0)]);
+ line1.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]);
+ line1.set(1, [0, '汉', 2, '汉'.charCodeAt(0)]);
+ line1.set(2, [0, '', 0, 0]);
+ line1.set(3, [0, '语', 2, '语'.charCodeAt(0)]);
+ line1.set(4, [0, '', 0, 0]);
+ line1.set(5, [0, 'b', 1, 'b'.charCodeAt(0)]);
const line2 = new BufferLine(6, undefined, true);
- line2.set(0, [null, 'a', 1, 'a'.charCodeAt(0)]);
- line2.set(1, [null, '汉', 2, '汉'.charCodeAt(0)]);
- line2.set(2, [null, '', 0, undefined]);
- line2.set(3, [null, '语', 2, '语'.charCodeAt(0)]);
- line2.set(4, [null, '', 0, undefined]);
- line2.set(5, [null, 'b', 1, 'b'.charCodeAt(0)]);
+ line2.set(0, [0, 'a', 1, 'a'.charCodeAt(0)]);
+ line2.set(1, [0, '汉', 2, '汉'.charCodeAt(0)]);
+ line2.set(2, [0, '', 0, 0]);
+ line2.set(3, [0, '语', 2, '语'.charCodeAt(0)]);
+ line2.set(4, [0, '', 0, 0]);
+ line2.set(5, [0, 'b', 1, 'b'.charCodeAt(0)]);
assert.equal(line1.translateToString(), 'a汉语b');
assert.equal(line2.translateToString(), 'a汉语b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 5), [5, 4, 3], 'lines: a汉语, ba汉, 语b');
@@ -79,11 +78,11 @@ describe('BufferReflow', () => {
});
it('should work on lines ending in null space', () => {
const line = new BufferLine(5);
- line.set(0, [null, '汉', 2, '汉'.charCodeAt(0)]);
- line.set(1, [null, '', 0, undefined]);
- line.set(2, [null, '语', 2, '语'.charCodeAt(0)]);
- line.set(3, [null, '', 0, undefined]);
- line.set(4, [null, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
+ line.set(0, [0, '汉', 2, '汉'.charCodeAt(0)]);
+ line.set(1, [0, '', 0, 0]);
+ line.set(2, [0, '语', 2, '语'.charCodeAt(0)]);
+ line.set(3, [0, '', 0, 0]);
+ line.set(4, [0, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]);
assert.equal(line.translateToString(true), '汉语');
assert.equal(line.translateToString(false), '汉语 ');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 3), [2, 2], 'line: 汉, 语');
diff --git a/src/BufferReflow.ts b/src/core/buffer/BufferReflow.ts
similarity index 98%
rename from src/BufferReflow.ts
rename to src/core/buffer/BufferReflow.ts
index 40e16c74..e363da0b 100644
--- a/src/BufferReflow.ts
+++ b/src/core/buffer/BufferReflow.ts
@@ -4,8 +4,8 @@
*/
import { BufferLine } from './BufferLine';
-import { CircularList } from './common/CircularList';
-import { IBufferLine, ICellData } from './Types';
+import { CircularList } from '../../common/CircularList';
+import { IBufferLine, ICellData } from '../Types';
export interface INewLayoutResult {
layout: number[];
diff --git a/src/core/buffer/Marker.ts b/src/core/buffer/Marker.ts
new file mode 100644
index 00000000..26de0dfe
--- /dev/null
+++ b/src/core/buffer/Marker.ts
@@ -0,0 +1,35 @@
+/**
+ * Copyright (c) 2018 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { EventEmitter2, IEvent } from '../../common/EventEmitter2';
+import { Disposable } from '../../common/Lifecycle';
+import { IMarker } from '../Types';
+
+export class Marker extends Disposable implements IMarker {
+ private static _nextId = 1;
+
+ private _id: number = Marker._nextId++;
+ public isDisposed: boolean = false;
+
+ public get id(): number { return this._id; }
+
+ private _onDispose = new EventEmitter2();
+ public get onDispose(): IEvent { return this._onDispose.event; }
+
+ constructor(
+ public line: number
+ ) {
+ super();
+ }
+
+ public dispose(): void {
+ if (this.isDisposed) {
+ return;
+ }
+ this.isDisposed = true;
+ // Emit before super.dispose such that dispose listeners get a change to react
+ this._onDispose.fire();
+ }
+}
diff --git a/src/core/input/Keyboard.ts b/src/core/input/Keyboard.ts
index 5e4c4e61..36097f91 100644
--- a/src/core/input/Keyboard.ts
+++ b/src/core/input/Keyboard.ts
@@ -352,6 +352,10 @@ export function evaluateKeyboardEvent(
} else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 48 && ev.key.length === 1) {
// Include only keys that that result in a _single_ character; don't include num lock, volume up, etc.
result.key = ev.key;
+ } else if (ev.key && ev.ctrlKey) {
+ if (ev.key === '_') { // ^_
+ result.key = C0.US;
+ }
}
break;
}
diff --git a/src/core/input/TextDecoder.test.ts b/src/core/input/TextDecoder.test.ts
index 12f3099a..dc358bc5 100644
--- a/src/core/input/TextDecoder.test.ts
+++ b/src/core/input/TextDecoder.test.ts
@@ -4,7 +4,41 @@
*/
import { assert } from 'chai';
-import { StringToUtf32, stringFromCodePoint, utf32ToString } from './TextDecoder';
+import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32, utf32ToString } from './TextDecoder';
+import { encode } from 'utf8';
+
+// convert UTF32 codepoints to string
+function toString(data: Uint32Array, length: number): string {
+ if ((String as any).fromCodePoint) {
+ return (String as any).fromCodePoint.apply(null, data.subarray(0, length));
+ }
+ let result = '';
+ for (let i = 0; i < length; ++i) {
+ result += stringFromCodePoint(data[i]);
+ }
+ return result;
+}
+
+// convert "bytestring" (charCode 0-255) to bytes
+function fromByteString(s: string): Uint8Array {
+ const result = new Uint8Array(s.length);
+ for (let i = 0; i < s.length; ++i) {
+ result[i] = s.charCodeAt(i);
+ }
+ return result;
+}
+
+const TEST_STRINGS = [
+ 'Лорем ипсум долор сит амет, ех сеа аццусам диссентиет. Ан еос стет еирмод витуперата. Иус дицерет урбанитас ет. Ан при алтера долорес сплендиде, цу яуо интегре денияуе, игнота волуптариа инструцтиор цу вим.',
+ 'ლორემ იფსუმ დოლორ სით ამეთ, ფაცერ მუციუს ცონსეთეთურ ყუო იდ, ფერ ვივენდუმ ყუაერენდუმ ეა, ესთ ამეთ მოვეთ სუავითათე ცუ. ვითაე სენსიბუს ან ვიხ. ეხერცი დეთერრუისსეთ უთ ყუი. ვოცენთ დებითის ადიფისცი ეთ ფერ. ნეც ან ფეუგაით ფორენსიბუს ინთერესსეთ. იდ დიცო რიდენს იუს. დისსენთიეთ ცონსეყუუნთურ სედ ნე, ნოვუმ მუნერე ეუმ ათ, ნე ეუმ ნიჰილ ირაცუნდია ურბანითას.',
+ 'अधिकांश अमितकुमार प्रोत्साहित मुख्य जाने प्रसारन विश्लेषण विश्व दारी अनुवादक अधिकांश नवंबर विषय गटकउसि गोपनीयता विकास जनित परस्पर गटकउसि अन्तरराष्ट्रीयकरन होसके मानव पुर्णता कम्प्युटर यन्त्रालय प्रति साधन',
+ '覧六子当聞社計文護行情投身斗来。増落世的況上席備界先関権能万。本物挙歯乳全事携供板栃果以。頭月患端撤競見界記引去法条公泊候。決海備駆取品目芸方用朝示上用報。講申務紙約週堂出応理田流団幸稿。起保帯吉対阜庭支肯豪彰属本躍。量抑熊事府募動極都掲仮読岸。自続工就断庫指北速配鳴約事新住米信中験。婚浜袋著金市生交保他取情距。',
+ '八メル務問へふらく博辞説いわょ読全タヨムケ東校どっ知壁テケ禁去フミ人過を装5階がねぜ法逆はじ端40落ミ予竹マヘナセ任1悪た。省ぜりせ製暇ょへそけ風井イ劣手はぼまず郵富法く作断タオイ取座ゅょが出作ホシ月給26島ツチ皇面ユトクイ暮犯リワナヤ断連こうでつ蔭柔薄とレにの。演めけふぱ損田転10得観びトげぎ王物鉄夜がまけ理惜くち牡提づ車惑参ヘカユモ長臓超漫ぼドかわ。',
+ '모든 국민은 행위시의 법률에 의하여 범죄를 구성하지 아니하는 행위로 소추되지 아니하며. 전직대통령의 신분과 예우에 관하여는 법률로 정한다, 국회는 헌법 또는 법률에 특별한 규정이 없는 한 재적의원 과반수의 출석과 출석의원 과반수의 찬성으로 의결한다. 군인·군무원·경찰공무원 기타 법률이 정하는 자가 전투·훈련등 직무집행과 관련하여 받은 손해에 대하여는 법률이 정하는 보상외에 국가 또는 공공단체에 공무원의 직무상 불법행위로 인한 배상은 청구할 수 없다.',
+ 'كان فشكّل الشرقي مع, واحدة للمجهود تزامناً بعض بل. وتم جنوب للصين غينيا لم, ان وبدون وكسبت الأمور ذلك, أسر الخاسر الانجليزية هو. نفس لغزو مواقعها هو. الجو علاقة الصعداء انه أي, كما مع بمباركة للإتحاد الوزراء. ترتيب الأولى أن حدى, الشتوية باستحداث مدن بل, كان قد أوسع عملية. الأوضاع بالمطالبة كل قام, دون إذ شمال الربيع،. هُزم الخاصّة ٣٠ أما, مايو الصينية مع قبل.',
+ 'או סדר החול מיזמי קרימינולוגיה. קהילה בגרסה לויקיפדים אל היא, של צעד ציור ואלקטרוניקה. מדע מה ברית המזנון ארכיאולוגיה, אל טבלאות מבוקשים כלל. מאמרשיחהצפה העריכהגירסאות שכל אל, כתב עיצוב מושגי של. קבלו קלאסיים ב מתן. נבחרים אווירונאוטיקה אם מלא, לוח למנוע ארכיאולוגיה מה. ארץ לערוך בקרבת מונחונים או, עזרה רקטות לויקיפדים אחר גם.',
+ 'Лорем ლორემ अधिकांश 覧六子 八メル 모든 בקרבת 💮 😂 äggg 123€ 𝄞.'
+];
describe('text encodings', () => {
it('stringFromCodePoint/utf32ToString', () => {
@@ -17,7 +51,7 @@ describe('text encodings', () => {
assert.equal(utf32ToString(data), s);
});
- describe('StringToUtf32 Decoder', () => {
+ describe('StringToUtf32 decoder', () => {
describe('full codepoint test', () => {
it('0..65535', () => {
const decoder = new StringToUtf32();
@@ -34,7 +68,8 @@ describe('text encodings', () => {
decoder.clear();
}
});
- it('65536..0x10FFFF (surrogates)', function(): void {
+
+ it('65536..0x10FFFF (surrogates)', function (): void {
this.timeout(20000);
const decoder = new StringToUtf32();
const target = new Uint32Array(5);
@@ -50,6 +85,16 @@ describe('text encodings', () => {
});
});
+ it('test strings', () => {
+ const decoder = new StringToUtf32();
+ const target = new Uint32Array(500);
+ for (let i = 0; i < TEST_STRINGS.length; ++i) {
+ const length = decoder.decode(TEST_STRINGS[i], target);
+ assert.equal(toString(target, length), TEST_STRINGS[i]);
+ decoder.clear();
+ }
+ });
+
describe('stream handling', () => {
it('surrogates mixed advance by 1', () => {
const decoder = new StringToUtf32();
@@ -58,7 +103,114 @@ describe('text encodings', () => {
let decoded = '';
for (let i = 0; i < input.length; ++i) {
const written = decoder.decode(input[i], target);
- decoded += utf32ToString(target, written);
+ decoded += toString(target, written);
+ }
+ assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
+ });
+ });
+ });
+
+ describe('Utf8ToUtf32 decoder', () => {
+ describe('full codepoint test', () => {
+
+ it('0..65535 (1/2/3 byte sequences)', () => {
+ const decoder = new Utf8ToUtf32();
+ const target = new Uint32Array(5);
+ for (let i = 0; i < 65536; ++i) {
+ // skip surrogate pairs
+ if (i >= 0xD800 && i <= 0xDFFF) {
+ continue;
+ }
+ const utf8Data = fromByteString(encode(String.fromCharCode(i)));
+ const length = decoder.decode(utf8Data, target);
+ assert.equal(length, 1);
+ assert.equal(toString(target, length), String.fromCharCode(i));
+ decoder.clear();
+ }
+ });
+
+ it('65536..0x10FFFF (4 byte sequences)', function (): void {
+ this.timeout(20000);
+ const decoder = new Utf8ToUtf32();
+ const target = new Uint32Array(5);
+ for (let i = 65536; i < 0x10FFFF; ++i) {
+ const utf8Data = fromByteString(encode(stringFromCodePoint(i)));
+ const length = decoder.decode(utf8Data, target);
+ assert.equal(length, 1);
+ assert.equal(target[0], i);
+ decoder.clear();
+ }
+ });
+ });
+
+ it('test strings', () => {
+ const decoder = new Utf8ToUtf32();
+ const target = new Uint32Array(500);
+ for (let i = 0; i < TEST_STRINGS.length; ++i) {
+ const utf8Data = fromByteString(encode(TEST_STRINGS[i]));
+ const length = decoder.decode(utf8Data, target);
+ assert.equal(toString(target, length), TEST_STRINGS[i]);
+ decoder.clear();
+ }
+ });
+
+ describe('stream handling', () => {
+ it('2 byte sequences - advance by 1', () => {
+ const decoder = new Utf8ToUtf32();
+ const target = new Uint32Array(5);
+ const utf8Data = fromByteString('\xc3\x84\xc3\x96\xc3\x9c\xc3\x9f\xc3\xb6\xc3\xa4\xc3\xbc');
+ let decoded = '';
+ for (let i = 0; i < utf8Data.length; ++i) {
+ const written = decoder.decode(utf8Data.slice(i, i + 1), target);
+ decoded += toString(target, written);
+ }
+ assert(decoded, 'ÄÖÜßöäü');
+ });
+
+ it('2/3 byte sequences - advance by 1', () => {
+ const decoder = new Utf8ToUtf32();
+ const target = new Uint32Array(5);
+ const utf8Data = fromByteString('\xc3\x84\xe2\x82\xac\xc3\x96\xe2\x82\xac\xc3\x9c\xe2\x82\xac\xc3\x9f\xe2\x82\xac\xc3\xb6\xe2\x82\xac\xc3\xa4\xe2\x82\xac\xc3\xbc');
+ let decoded = '';
+ for (let i = 0; i < utf8Data.length; ++i) {
+ const written = decoder.decode(utf8Data.slice(i, i + 1), target);
+ decoded += toString(target, written);
+ }
+ assert(decoded, 'Āր܀߀ö€ä€ü');
+ });
+
+ it('2/3/4 byte sequences - advance by 1', () => {
+ const decoder = new Utf8ToUtf32();
+ const target = new Uint32Array(5);
+ const utf8Data = fromByteString('\xc3\x84\xe2\x82\xac\xf0\x9d\x84\x9e\xc3\x96\xf0\x9d\x84\x9e\xe2\x82\xac\xc3\x9c\xf0\x9d\x84\x9e\xe2\x82\xac');
+ let decoded = '';
+ for (let i = 0; i < utf8Data.length; ++i) {
+ const written = decoder.decode(utf8Data.slice(i, i + 1), target);
+ decoded += toString(target, written);
+ }
+ assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
+ });
+
+ it('2/3/4 byte sequences - advance by 2', () => {
+ const decoder = new Utf8ToUtf32();
+ const target = new Uint32Array(5);
+ const utf8Data = fromByteString('\xc3\x84\xe2\x82\xac\xf0\x9d\x84\x9e\xc3\x96\xf0\x9d\x84\x9e\xe2\x82\xac\xc3\x9c\xf0\x9d\x84\x9e\xe2\x82\xac');
+ let decoded = '';
+ for (let i = 0; i < utf8Data.length; i += 2) {
+ const written = decoder.decode(utf8Data.slice(i, i + 2), target);
+ decoded += toString(target, written);
+ }
+ assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
+ });
+
+ it('2/3/4 byte sequences - advance by 3', () => {
+ const decoder = new Utf8ToUtf32();
+ const target = new Uint32Array(5);
+ const utf8Data = fromByteString('\xc3\x84\xe2\x82\xac\xf0\x9d\x84\x9e\xc3\x96\xf0\x9d\x84\x9e\xe2\x82\xac\xc3\x9c\xf0\x9d\x84\x9e\xe2\x82\xac');
+ let decoded = '';
+ for (let i = 0; i < utf8Data.length; i += 3) {
+ const written = decoder.decode(utf8Data.slice(i, i + 3), target);
+ decoded += toString(target, written);
}
assert(decoded, 'Ä€𝄞Ö𝄞€Ü𝄞€');
});
diff --git a/src/core/input/TextDecoder.ts b/src/core/input/TextDecoder.ts
index 1ff00940..7e141e02 100644
--- a/src/core/input/TextDecoder.ts
+++ b/src/core/input/TextDecoder.ts
@@ -3,6 +3,45 @@
* @license MIT
*/
+/**
+ * Polyfill - Convert UTF32 codepoint into JS string.
+ * Note: The built-in String.fromCodePoint happens to be much slower
+ * due to additional sanity checks. We can avoid them since
+ * we always operate on legal UTF32 (granted by the input decoders)
+ * and use this faster version instead.
+ */
+export function stringFromCodePoint(codePoint: number): string {
+ if (codePoint > 0xFFFF) {
+ codePoint -= 0x10000;
+ return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00);
+ }
+ return String.fromCharCode(codePoint);
+}
+
+/**
+ * Convert UTF32 char codes into JS string.
+ * Basically the same as `stringFromCodePoint` but for multiple codepoints
+ * in a loop (which is a lot faster).
+ */
+export function utf32ToString(data: Uint32Array, start: number = 0, end: number = data.length): string {
+ let result = '';
+ for (let i = start; i < end; ++i) {
+ let codepoint = data[i];
+ if (codepoint > 0xFFFF) {
+ // JS strings are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate pair
+ // conversion rules:
+ // - subtract 0x10000 from code point, leaving a 20 bit number
+ // - add high 10 bits to 0xD800 --> first surrogate
+ // - add low 10 bits to 0xDC00 --> second surrogate
+ codepoint -= 0x10000;
+ result += String.fromCharCode((codepoint >> 10) + 0xD800) + String.fromCharCode((codepoint % 0x400) + 0xDC00);
+ } else {
+ result += String.fromCharCode(codepoint);
+ }
+ }
+ return result;
+}
+
/**
* StringToUtf32 - decodes UTF16 sequences into UTF32 codepoints.
* To keep the decoder in line with JS strings it handles single surrogates as UCS2.
@@ -73,37 +112,231 @@ export class StringToUtf32 {
}
/**
- * Convert UTF32 codepoint into JS string.
+ * Utf8Decoder - decodes UTF8 byte sequences into UTF32 codepoints.
*/
-export function stringFromCodePoint(codePoint: number): string {
- if (codePoint > 0xFFFF) {
- // UTF32 to UTF16 conversion (see comments in utf32ToString)
- codePoint -= 0x10000;
- return String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00);
- }
- return String.fromCharCode(codePoint);
-}
+export class Utf8ToUtf32 {
+ public interim: Uint8Array = new Uint8Array(3);
-/**
- * Convert UTF32 char codes into JS string.
- * Basically the same as `stringFromCodePoint` but for multiple codepoints
- * in a loop (which is a lot faster).
- */
-export function utf32ToString(data: Uint32Array, start: number = 0, end: number = data.length): string {
- let result = '';
- for (let i = start; i < end; ++i) {
- let codepoint = data[i];
- if (codepoint > 0xFFFF) {
- // JS string are encoded as UTF16, thus a non BMP codepoint gets converted into a surrogate pair
- // conversion rules:
- // - subtract 0x10000 from code point, leaving a 20 bit number
- // - add high 10 bits to 0xD800 --> first surrogate
- // - add low 10 bits to 0xDC00 --> second surrogate
- codepoint -= 0x10000;
- result += String.fromCharCode((codepoint >> 10) + 0xD800) + String.fromCharCode((codepoint % 0x400) + 0xDC00);
- } else {
- result += String.fromCharCode(codepoint);
+ /**
+ * Clears interim bytes and resets decoder to clean state.
+ */
+ public clear(): void {
+ this.interim.fill(0);
+ }
+
+ /**
+ * Decodes UTF8 byte sequences in `input` to UTF32 codepoints in `target`.
+ * The methods assumes stream input and will store partly transmitted bytes
+ * and decode them with the next data chunk.
+ * Note: The method does no bound checks for target, therefore make sure
+ * the provided data chunk does not exceed the size of `target`.
+ * Returns the number of written codepoints in `target`.
+ */
+ decode(input: Uint8Array, target: Uint32Array): number {
+ const length = input.length;
+
+ if (!length) {
+ return 0;
}
+
+ let size = 0;
+ let byte1: number;
+ let byte2: number;
+ let byte3: number;
+ let byte4: number;
+ let codepoint = 0;
+ let startPos = 0;
+
+ // handle leftover bytes
+ if (this.interim[0]) {
+ let discardInterim = false;
+ let cp = this.interim[0];
+ cp &= ((((cp & 0xE0) === 0xC0)) ? 0x1F : (((cp & 0xF0) === 0xE0)) ? 0x0F : 0x07);
+ let pos = 0;
+ let tmp: number;
+ while ((tmp = this.interim[++pos] & 0x3F) && pos < 4) {
+ cp <<= 6;
+ cp |= tmp;
+ }
+ // missing bytes - read ahead from input
+ const type = (((this.interim[0] & 0xE0) === 0xC0)) ? 2 : (((this.interim[0] & 0xF0) === 0xE0)) ? 3 : 4;
+ const missing = type - pos;
+ while (startPos < missing) {
+ if (startPos >= length) {
+ return 0;
+ }
+ tmp = input[startPos++];
+ if ((tmp & 0xC0) !== 0x80) {
+ // wrong continuation, discard interim bytes completely
+ startPos--;
+ discardInterim = true;
+ break;
+ } else {
+ // need to save so we can continue short inputs in next call
+ this.interim[pos++] = tmp;
+ cp <<= 6;
+ cp |= tmp & 0x3F;
+ }
+ }
+ if (!discardInterim) {
+ // final test is type dependent
+ if (type === 2) {
+ if (cp < 0x80) {
+ // wrong starter byte
+ startPos--;
+ } else {
+ target[size++] = cp;
+ }
+ } else if (type === 3) {
+ if (cp < 0x0800 || (cp >= 0xD800 && cp <= 0xDFFF)) {
+ // illegal codepoint
+ } else {
+ target[size++] = cp;
+ }
+ } else {
+ if (codepoint < 0x010000 || codepoint > 0x10FFFF) {
+ // illegal codepoint
+ } else {
+ target[size++] = cp;
+ }
+ }
+ }
+ this.interim.fill(0);
+ }
+
+ // loop through input
+ const fourStop = length - 4;
+ let i = startPos;
+ while (i < length) {
+ /**
+ * ASCII shortcut with loop unrolled to 4 consecutive ASCII chars.
+ * This is a compromise between speed gain for ASCII
+ * and penalty for non ASCII:
+ * For best ASCII performance the char should be stored directly into target,
+ * but even a single attempt to write to target and compare afterwards
+ * penalizes non ASCII really bad (-50%), thus we load the char into byteX first,
+ * which reduces ASCII performance by ~15%.
+ * This trial for ASCII reduces non ASCII performance by ~10% which seems acceptible
+ * compared to the gains.
+ * Note that this optimization only takes place for 4 consecutive ASCII chars,
+ * for any shorter it bails out. Worst case - all 4 bytes being read but
+ * thrown away due to the last being a non ASCII char (-10% performance).
+ */
+ while (i < fourStop
+ && !((byte1 = input[i]) & 0x80)
+ && !((byte2 = input[i + 1]) & 0x80)
+ && !((byte3 = input[i + 2]) & 0x80)
+ && !((byte4 = input[i + 3]) & 0x80))
+ {
+ target[size++] = byte1;
+ target[size++] = byte2;
+ target[size++] = byte3;
+ target[size++] = byte4;
+ i += 4;
+ }
+
+ // reread byte1
+ byte1 = input[i++];
+
+ // 1 byte
+ if (byte1 < 0x80) {
+ target[size++] = byte1;
+
+ // 2 bytes
+ } else if ((byte1 & 0xE0) === 0xC0) {
+ if (i >= length) {
+ this.interim[0] = byte1;
+ return size;
+ }
+ byte2 = input[i++];
+ if ((byte2 & 0xC0) !== 0x80) {
+ // wrong continuation
+ i--;
+ continue;
+ }
+ codepoint = (byte1 & 0x1F) << 6 | (byte2 & 0x3F);
+ if (codepoint < 0x80) {
+ // wrong starter byte
+ i--;
+ continue;
+ }
+ target[size++] = codepoint;
+
+ // 3 bytes
+ } else if ((byte1 & 0xF0) === 0xE0) {
+ if (i >= length) {
+ this.interim[0] = byte1;
+ return size;
+ }
+ byte2 = input[i++];
+ if ((byte2 & 0xC0) !== 0x80) {
+ // wrong continuation
+ i--;
+ continue;
+ }
+ if (i >= length) {
+ this.interim[0] = byte1;
+ this.interim[1] = byte2;
+ return size;
+ }
+ byte3 = input[i++];
+ if ((byte3 & 0xC0) !== 0x80) {
+ // wrong continuation
+ i--;
+ continue;
+ }
+ codepoint = (byte1 & 0x0F) << 12 | (byte2 & 0x3F) << 6 | (byte3 & 0x3F);
+ if (codepoint < 0x0800 || (codepoint >= 0xD800 && codepoint <= 0xDFFF)) {
+ // illegal codepoint, no i-- here
+ continue;
+ }
+ target[size++] = codepoint;
+
+ // 4 bytes
+ } else if ((byte1 & 0xF8) === 0xF0) {
+ if (i >= length) {
+ this.interim[0] = byte1;
+ return size;
+ }
+ byte2 = input[i++];
+ if ((byte2 & 0xC0) !== 0x80) {
+ // wrong continuation
+ i--;
+ continue;
+ }
+ if (i >= length) {
+ this.interim[0] = byte1;
+ this.interim[1] = byte2;
+ return size;
+ }
+ byte3 = input[i++];
+ if ((byte3 & 0xC0) !== 0x80) {
+ // wrong continuation
+ i--;
+ continue;
+ }
+ if (i >= length) {
+ this.interim[0] = byte1;
+ this.interim[1] = byte2;
+ this.interim[2] = byte3;
+ return size;
+ }
+ byte4 = input[i++];
+ if ((byte4 & 0xC0) !== 0x80) {
+ // wrong continuation
+ i--;
+ continue;
+ }
+ codepoint = (byte1 & 0x07) << 18 | (byte2 & 0x3F) << 12 | (byte3 & 0x3F) << 6 | (byte4 & 0x3F);
+ if (codepoint < 0x010000 || codepoint > 0x10FFFF) {
+ // illegal codepoint, no i-- here
+ continue;
+ }
+ target[size++] = codepoint;
+ } else {
+ // illegal byte, just skip
+ }
+ }
+ return size;
}
- return result;
}
diff --git a/src/handlers/AltClickHandler.ts b/src/handlers/AltClickHandler.ts
index 9286421e..5cf60fa6 100644
--- a/src/handlers/AltClickHandler.ts
+++ b/src/handlers/AltClickHandler.ts
@@ -3,7 +3,8 @@
* @license MIT
*/
-import { ITerminal, IBufferLine } from '../Types';
+import { ITerminal } from '../Types';
+import { IBufferLine } from '../core/Types';
import { ICircularList } from '../common/Types';
import { C0 } from '../common/data/EscapeSequences';
diff --git a/src/public/AddonManager.test.ts b/src/public/AddonManager.test.ts
new file mode 100644
index 00000000..8198ce31
--- /dev/null
+++ b/src/public/AddonManager.test.ts
@@ -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);
+ });
+ });
+});
diff --git a/src/public/AddonManager.ts b/src/public/AddonManager.ts
new file mode 100644
index 00000000..b5506514
--- /dev/null
+++ b/src/public/AddonManager.ts
@@ -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(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);
+ }
+}
diff --git a/src/public/Terminal.api.ts b/src/public/Terminal.api.ts
new file mode 100644
index 00000000..2ad5313e
--- /dev/null
+++ b/src/public/Terminal.api.ts
@@ -0,0 +1,436 @@
+/**
+ * Copyright (c) 2019 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import * as puppeteer from 'puppeteer';
+import { assert } from 'chai';
+import { ITerminalOptions } from '../Types';
+
+const APP = 'http://127.0.0.1:3000/test';
+
+let browser: puppeteer.Browser;
+let page: puppeteer.Page;
+const width = 800;
+const height = 600;
+
+describe('API Integration Tests', () => {
+ before(async function(): Promise {
+ this.timeout(10000);
+ browser = await puppeteer.launch({
+ headless: process.argv.indexOf('--headless') !== -1,
+ slowMo: 80,
+ args: [`--window-size=${width},${height}`]
+ });
+ page = (await browser.pages())[0];
+ await page.setViewport({ width, height });
+ });
+
+ after(() => {
+ browser.close();
+ });
+
+ beforeEach(async () => {
+ await page.goto(APP);
+ });
+
+ it('Default options', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal();
+ assert.equal(await page.evaluate(`window.term.cols`), 80);
+ assert.equal(await page.evaluate(`window.term.rows`), 24);
+ });
+
+ it('write', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal();
+ await page.evaluate(`
+ window.term.write('foo');
+ window.term.write('bar');
+ window.term.write('文');
+ `);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文');
+ });
+
+ it('writeln', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal();
+ await page.evaluate(`
+ window.term.writeln('foo');
+ window.term.writeln('bar');
+ window.term.writeln('文');
+ `);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(1).translateToString(true)`), 'bar');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(2).translateToString(true)`), '文');
+ });
+
+ it('writeUtf8', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal();
+ await page.evaluate(`
+ // foo
+ window.term.writeUtf8(new Uint8Array([102, 111, 111]));
+ // bar
+ window.term.writeUtf8(new Uint8Array([98, 97, 114]));
+ // 文
+ window.term.writeUtf8(new Uint8Array([230, 150, 135]));
+ `);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文');
+ });
+
+ it('clear', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal({ rows: 5 });
+ await page.evaluate(`
+ window.term.write('test0');
+ for (let i = 1; i < 10; i++) {
+ window.term.write('\\n\\rtest' + i);
+ }
+ `);
+ await page.evaluate(`window.term.clear()`);
+ assert.equal(await page.evaluate(`window.term.buffer.length`), '5');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'test9');
+ for (let i = 1; i < 5; i++) {
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(${i}).translateToString(true)`), '');
+ }
+ });
+
+ it('getOption, setOption', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal();
+ assert.equal(await page.evaluate(`window.term.getOption('rendererType')`), 'canvas');
+ await page.evaluate(`window.term.setOption('rendererType', 'dom')`);
+ assert.equal(await page.evaluate(`window.term.getOption('rendererType')`), 'dom');
+ });
+
+ it('selection', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal({ rows: 5, cols: 5 });
+ await page.evaluate(`window.term.write('\\n\\nfoo\\n\\n\\rbar\\n\\n\\rbaz')`);
+ assert.equal(await page.evaluate(`window.term.hasSelection()`), false);
+ assert.equal(await page.evaluate(`window.term.getSelection()`), '');
+ assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), undefined);
+ await page.evaluate(`window.term.selectAll()`);
+ assert.equal(await page.evaluate(`window.term.hasSelection()`), true);
+ assert.equal(await page.evaluate(`window.term.getSelection()`), '\n\nfoo\n\nbar\n\nbaz');
+ assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), { startColumn: 0, startRow: 0, endColumn: 5, endRow: 6 });
+ await page.evaluate(`window.term.clearSelection()`);
+ assert.equal(await page.evaluate(`window.term.hasSelection()`), false);
+ assert.equal(await page.evaluate(`window.term.getSelection()`), '');
+ assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), undefined);
+ await page.evaluate(`window.term.select(1, 2, 2)`);
+ assert.equal(await page.evaluate(`window.term.hasSelection()`), true);
+ assert.equal(await page.evaluate(`window.term.getSelection()`), 'oo');
+ assert.deepEqual(await page.evaluate(`window.term.getSelectionPosition()`), { startColumn: 1, startRow: 2, endColumn: 3, endRow: 2 });
+ });
+
+ it('focus, blur', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal();
+ assert.equal(await page.evaluate(`document.activeElement.className`), '');
+ await page.evaluate(`window.term.focus()`);
+ assert.equal(await page.evaluate(`document.activeElement.className`), 'xterm-helper-textarea');
+ await page.evaluate(`window.term.blur()`);
+ 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);
+ await openTerminal();
+ await page.evaluate(`
+ window.callCount = 0;
+ window.term.onCursorMove(e => window.callCount++);
+ window.term.write('foo');
+ `);
+ assert.equal(await page.evaluate(`window.callCount`), 1);
+ await page.evaluate(`window.term.write('bar')`);
+ assert.equal(await page.evaluate(`window.callCount`), 2);
+ });
+
+ it('onData', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal();
+ await page.evaluate(`
+ window.calls = [];
+ window.term.onData(e => calls.push(e));
+ `);
+ await page.type('.xterm-helper-textarea', 'foo');
+ assert.deepEqual(await page.evaluate(`window.calls`), ['f', 'o', 'o']);
+ });
+
+ it('onKey', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal();
+ await page.evaluate(`
+ window.calls = [];
+ window.term.onKey(e => calls.push(e.key));
+ `);
+ await page.type('.xterm-helper-textarea', 'foo');
+ assert.deepEqual(await page.evaluate(`window.calls`), ['f', 'o', 'o']);
+ });
+
+ it('onLineFeed', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal();
+ await page.evaluate(`
+ window.callCount = 0;
+ window.term.onLineFeed(() => callCount++);
+ window.term.writeln('foo');
+ `);
+ assert.equal(await page.evaluate(`window.callCount`), 1);
+ await page.evaluate(`window.term.writeln('bar')`);
+ assert.equal(await page.evaluate(`window.callCount`), 2);
+ });
+
+ it('onScroll', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal({ rows: 5 });
+ await page.evaluate(`
+ window.calls = [];
+ window.term.onScroll(e => window.calls.push(e));
+ for (let i = 0; i < 4; i++) {
+ window.term.writeln('foo');
+ }
+ `);
+ assert.deepEqual(await page.evaluate(`window.calls`), []);
+ await page.evaluate(`window.term.writeln('bar')`);
+ assert.deepEqual(await page.evaluate(`window.calls`), [1]);
+ await page.evaluate(`window.term.writeln('baz')`);
+ assert.deepEqual(await page.evaluate(`window.calls`), [1, 2]);
+ });
+
+ it('onSelectionChange', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal();
+ await page.evaluate(`
+ window.callCount = 0;
+ window.term.onSelectionChange(() => window.callCount++);
+ `);
+ assert.equal(await page.evaluate(`window.callCount`), 0);
+ await page.evaluate(`window.term.selectAll()`);
+ assert.equal(await page.evaluate(`window.callCount`), 1);
+ await page.evaluate(`window.term.clearSelection()`);
+ assert.equal(await page.evaluate(`window.callCount`), 2);
+ });
+
+ it('onRender', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal();
+ await page.evaluate(`
+ window.calls = [];
+ window.term.onRender(e => window.calls.push([e.start, e.end]));
+ `);
+ assert.deepEqual(await page.evaluate(`window.calls`), []);
+ await page.evaluate(`window.term.write('foo')`);
+ assert.deepEqual(await page.evaluate(`window.calls`), [[0, 0]]);
+ await page.evaluate(`window.term.write('bar\\n\\nbaz')`);
+ assert.deepEqual(await page.evaluate(`window.calls`), [[0, 0], [0, 2]]);
+ });
+
+ it('onResize', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal();
+ await page.evaluate(`
+ window.calls = [];
+ window.term.onResize(e => window.calls.push([e.cols, e.rows]));
+ `);
+ assert.deepEqual(await page.evaluate(`window.calls`), []);
+ await page.evaluate(`window.term.resize(10, 5)`);
+ assert.deepEqual(await page.evaluate(`window.calls`), [[10, 5]]);
+ await page.evaluate(`window.term.resize(20, 15)`);
+ assert.deepEqual(await page.evaluate(`window.calls`), [[10, 5], [20, 15]]);
+ });
+
+ it('onTitleChange', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal();
+ await page.evaluate(`
+ window.calls = [];
+ window.term.onTitleChange(e => window.calls.push(e));
+ `);
+ assert.deepEqual(await page.evaluate(`window.calls`), []);
+ await page.evaluate(`window.term.write('\\x1b]2;foo\\x9c')`);
+ assert.deepEqual(await page.evaluate(`window.calls`), ['foo']);
+ });
+ });
+
+ describe('buffer', () => {
+ it('cursorX, cursorY', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal({ rows: 5, cols: 5 });
+ assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 0);
+ assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 0);
+ await page.evaluate(`window.term.write('foo')`);
+ assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 3);
+ assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 0);
+ await page.evaluate(`window.term.write('\\n')`);
+ assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 3);
+ assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 1);
+ await page.evaluate(`window.term.write('\\r')`);
+ assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 0);
+ assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 1);
+ await page.evaluate(`window.term.write('abcde')`);
+ assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 5);
+ assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 1);
+ await page.evaluate(`window.term.write('\\n\\r\\n\\n\\n\\n\\n')`);
+ assert.equal(await page.evaluate(`window.term.buffer.cursorX`), 0);
+ assert.equal(await page.evaluate(`window.term.buffer.cursorY`), 4);
+ });
+
+ it('viewportY', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal({ rows: 5 });
+ assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 0);
+ await page.evaluate(`window.term.write('\\n\\n\\n\\n')`);
+ assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 0);
+ await page.evaluate(`window.term.write('\\n')`);
+ assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 1);
+ await page.evaluate(`window.term.write('\\n\\n\\n\\n')`);
+ assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 5);
+ await page.evaluate(`window.term.scrollLines(-1)`);
+ assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 4);
+ await page.evaluate(`window.term.scrollToTop()`);
+ assert.equal(await page.evaluate(`window.term.buffer.viewportY`), 0);
+ });
+
+ it('baseY', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal({ rows: 5 });
+ assert.equal(await page.evaluate(`window.term.buffer.baseY`), 0);
+ await page.evaluate(`window.term.write('\\n\\n\\n\\n')`);
+ assert.equal(await page.evaluate(`window.term.buffer.baseY`), 0);
+ await page.evaluate(`window.term.write('\\n')`);
+ assert.equal(await page.evaluate(`window.term.buffer.baseY`), 1);
+ await page.evaluate(`window.term.write('\\n\\n\\n\\n')`);
+ assert.equal(await page.evaluate(`window.term.buffer.baseY`), 5);
+ await page.evaluate(`window.term.scrollLines(-1)`);
+ assert.equal(await page.evaluate(`window.term.buffer.baseY`), 5);
+ await page.evaluate(`window.term.scrollToTop()`);
+ assert.equal(await page.evaluate(`window.term.buffer.baseY`), 5);
+ });
+
+ it('length', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal({ rows: 5 });
+ assert.equal(await page.evaluate(`window.term.buffer.length`), 5);
+ await page.evaluate(`window.term.write('\\n\\n\\n\\n')`);
+ assert.equal(await page.evaluate(`window.term.buffer.length`), 5);
+ await page.evaluate(`window.term.write('\\n')`);
+ assert.equal(await page.evaluate(`window.term.buffer.length`), 6);
+ await page.evaluate(`window.term.write('\\n\\n\\n\\n')`);
+ assert.equal(await page.evaluate(`window.term.buffer.length`), 10);
+ });
+
+ describe('getLine', () => {
+ it('invalid index', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal({ rows: 5 });
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(-1)`), undefined);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(5)`), undefined);
+ });
+
+ it('isWrapped', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal({ cols: 5 });
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).isWrapped`), false);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(1).isWrapped`), false);
+ await page.evaluate(`window.term.write('abcde')`);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).isWrapped`), false);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(1).isWrapped`), false);
+ await page.evaluate(`window.term.write('f')`);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).isWrapped`), false);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(1).isWrapped`), true);
+ });
+
+ it('translateToString', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal({ cols: 5 });
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString()`), ' ');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), '');
+ await page.evaluate(`window.term.write('foo')`);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString()`), 'foo ');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foo');
+ await page.evaluate(`window.term.write('bar')`);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString()`), 'fooba');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'fooba');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(1).translateToString(true)`), 'r');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(false, 1)`), 'ooba');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(false, 1, 3)`), 'oo');
+ });
+
+ it('getCell', async function(): Promise {
+ this.timeout(10000);
+ await openTerminal({ cols: 5 });
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(-1)`), undefined);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(5)`), undefined);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).char`), '');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).width`), 1);
+ await page.evaluate(`window.term.write('a文')`);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).char`), 'a');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(0).width`), 1);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(1).char`), '文');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(1).width`), 2);
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(2).char`), '');
+ assert.equal(await page.evaluate(`window.term.buffer.getLine(0).getCell(2).width`), 0);
+ });
+ });
+ });
+});
+
+async function openTerminal(options: ITerminalOptions = {}): Promise {
+ await page.evaluate(`window.term = new Terminal(${JSON.stringify(options)})`);
+ await page.evaluate(`window.term.open(document.querySelector('#terminal-container'))`);
+ if (options.rendererType === 'dom') {
+ await page.waitForSelector('.xterm-rows');
+ } else {
+ await page.waitForSelector('.xterm-text-layer');
+ }
+}
diff --git a/src/public/Terminal.test.ts b/src/public/Terminal.test.ts
index 06c8f1d5..6bad5b04 100644
--- a/src/public/Terminal.test.ts
+++ b/src/public/Terminal.test.ts
@@ -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
diff --git a/src/public/Terminal.ts b/src/public/Terminal.ts
index d05a4f10..f1874a3f 100644
--- a/src/public/Terminal.ts
+++ b/src/public/Terminal.ts
@@ -3,17 +3,21 @@
* @license MIT
*/
-import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings } from 'xterm';
-import { ITerminal } from '../Types';
+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 { return this._core.onCursorMove; }
@@ -30,7 +34,8 @@ export class Terminal implements ITerminalApi {
public get textarea(): HTMLTextAreaElement { return this._core.textarea; }
public get rows(): number { return this._core.rows; }
public get cols(): number { return this._core.cols; }
- public get markers(): IMarker[] { return this._core.markers; }
+ public get buffer(): IBufferApi { return new BufferApiView(this._core.buffer); }
+ public get markers(): ReadonlyArray { return this._core.markers; }
public blur(): void {
this._core.blur();
}
@@ -94,9 +99,15 @@ export class Terminal implements ITerminalApi {
public hasSelection(): boolean {
return this._core.hasSelection();
}
+ public select(column: number, row: number, length: number): void {
+ this._core.select(column, row, length);
+ }
public getSelection(): string {
return this._core.getSelection();
}
+ public getSelectionPosition(): ISelectionPosition | undefined {
+ return this._core.getSelectionPosition();
+ }
public clearSelection(): void {
this._core.clearSelection();
}
@@ -107,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 {
@@ -133,6 +145,9 @@ export class Terminal implements ITerminalApi {
public write(data: string): void {
this._core.write(data);
}
+ public writeUtf8(data: Uint8Array): void {
+ this._core.writeUtf8(data);
+ }
public getOption(key: 'bellSound' | 'bellStyle' | 'cursorStyle' | 'fontFamily' | 'fontWeight' | 'fontWeightBold' | 'rendererType' | 'termName'): string;
public getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean;
public getOption(key: 'colors'): string[];
@@ -165,7 +180,48 @@ 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;
}
}
+
+class BufferApiView implements IBufferApi {
+ constructor(private _buffer: IBuffer) {}
+
+ public get cursorY(): number { return this._buffer.y; }
+ public get cursorX(): number { return this._buffer.x; }
+ public get viewportY(): number { return this._buffer.ydisp; }
+ public get baseY(): number { return this._buffer.ybase; }
+ public get length(): number { return this._buffer.lines.length; }
+ public getLine(y: number): IBufferLineApi | undefined {
+ const line = this._buffer.lines.get(y);
+ if (!line) {
+ return undefined;
+ }
+ return new BufferLineApiView(line);
+ }
+}
+
+class BufferLineApiView implements IBufferLineApi {
+ constructor(private _line: IBufferLine) {}
+
+ public get isWrapped(): boolean { return this._line.isWrapped; }
+ public getCell(x: number): IBufferCellApi | undefined {
+ if (x < 0 || x >= this._line.length) {
+ return undefined;
+ }
+ return new BufferCellApiView(this._line, x);
+ }
+ public translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string {
+ return this._line.translateToString(trimRight, startColumn, endColumn);
+ }
+}
+
+class BufferCellApiView implements IBufferCellApi {
+ constructor(private _line: IBufferLine, private _x: number) {}
+ public get char(): string { return this._line.getString(this._x); }
+ public get width(): number { return this._line.getWidth(this._x); }
+}
diff --git a/src/renderer/BaseRenderLayer.ts b/src/renderer/BaseRenderLayer.ts
index d851b2f2..498b24d0 100644
--- a/src/renderer/BaseRenderLayer.ts
+++ b/src/renderer/BaseRenderLayer.ts
@@ -4,12 +4,13 @@
*/
import { IRenderLayer, IColorSet, IRenderDimensions } from './Types';
-import { ITerminal, ICellData } from '../Types';
-import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier, DEFAULT_COLOR } from './atlas/Types';
+import { ITerminal } from '../Types';
+import { ICellData } from '../core/Types';
+import { DEFAULT_COLOR } from '../common/Types';
+import { DIM_OPACITY, INVERTED_DEFAULT_COLOR, IGlyphIdentifier } from './atlas/Types';
import BaseCharAtlas from './atlas/BaseCharAtlas';
import { acquireCharAtlas } from './atlas/CharAtlasCache';
-import { CellData, AttributeData } from '../BufferLine';
-import { WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../Buffer';
+import { CellData, AttributeData, WHITESPACE_CELL_CHAR, WHITESPACE_CELL_CODE } from '../core/buffer/BufferLine';
export abstract class BaseRenderLayer implements IRenderLayer {
private _canvas: HTMLCanvasElement;
@@ -261,6 +262,9 @@ export abstract class BaseRenderLayer implements IRenderLayer {
protected drawChars(terminal: ITerminal, cell: ICellData, x: number, y: number): void {
// skip cache right away if we draw in RGB
+ // Note: to avoid bad runtime JoinedCellData will be skipped
+ // in the cache handler itself (atlasDidDraw == false) and
+ // fall through to uncached later down below
if (cell.isFgRGB() || cell.isBgRGB()) {
this._drawUncachedChars(terminal, cell, x, y);
return;
diff --git a/src/renderer/CharacterJoinerRegistry.test.ts b/src/renderer/CharacterJoinerRegistry.test.ts
index 2ccfd197..948985a4 100644
--- a/src/renderer/CharacterJoinerRegistry.test.ts
+++ b/src/renderer/CharacterJoinerRegistry.test.ts
@@ -1,3 +1,8 @@
+/**
+ * Copyright (c) 2018 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
import { assert } from 'chai';
import { MockTerminal, MockBuffer } from '../TestUtils.test';
@@ -5,8 +10,8 @@ import { CircularList } from '../common/CircularList';
import { ICharacterJoinerRegistry } from './Types';
import { CharacterJoinerRegistry } from './CharacterJoinerRegistry';
-import { BufferLine, CellData } from '../BufferLine';
-import { IBufferLine } from '../Types';
+import { BufferLine, CellData } from '../core/buffer/BufferLine';
+import { IBufferLine } from '../core/Types';
describe('CharacterJoinerRegistry', () => {
let registry: ICharacterJoinerRegistry;
diff --git a/src/renderer/CharacterJoinerRegistry.ts b/src/renderer/CharacterJoinerRegistry.ts
index 8c521dd9..3302c312 100644
--- a/src/renderer/CharacterJoinerRegistry.ts
+++ b/src/renderer/CharacterJoinerRegistry.ts
@@ -1,7 +1,57 @@
-import { ITerminal, IBufferLine } from '../Types';
+/**
+ * Copyright (c) 2018 The xterm.js authors. All rights reserved.
+ * @license MIT
+ */
+
+import { ITerminal } from '../Types';
+import { IBufferLine, ICellData, CharData } from '../core/Types';
import { ICharacterJoinerRegistry, ICharacterJoiner } from './Types';
-import { CellData } from '../BufferLine';
-import { WHITESPACE_CELL_CHAR } from '../Buffer';
+import { CellData, Content, AttributeData, WHITESPACE_CELL_CHAR } from '../core/buffer/BufferLine';
+
+export class JoinedCellData extends AttributeData implements ICellData {
+ private _width: number;
+ // .content carries no meaning for joined CellData, simply nullify it
+ // thus we have to overload all other .content accessors
+ public content: number = 0;
+ public fg: number;
+ public bg: number;
+ public combinedData: string = '';
+
+ constructor(firstCell: ICellData, chars: string, width: number) {
+ super();
+ this.fg = firstCell.fg;
+ this.bg = firstCell.bg;
+ this.combinedData = chars;
+ this._width = width;
+ }
+
+ public isCombined(): number {
+ // always mark joined cell data as combined
+ return Content.IS_COMBINED_MASK;
+ }
+
+ public getWidth(): number {
+ return this._width;
+ }
+
+ public getChars(): string {
+ return this.combinedData;
+ }
+
+ public getCode(): number {
+ // code always gets the highest possible fake codepoint (read as -1)
+ // this is needed as code is used by caches as identifier
+ return 0x1FFFFF;
+ }
+
+ public setFromCharData(value: CharData): void {
+ throw new Error('not implemented');
+ }
+
+ public getAsCharData(): CharData {
+ return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
+ }
+}
export class CharacterJoinerRegistry implements ICharacterJoinerRegistry {
diff --git a/src/renderer/CursorRenderLayer.ts b/src/renderer/CursorRenderLayer.ts
index c3b751fa..3e8af02d 100644
--- a/src/renderer/CursorRenderLayer.ts
+++ b/src/renderer/CursorRenderLayer.ts
@@ -5,8 +5,9 @@
import { IColorSet, IRenderDimensions } from './Types';
import { BaseRenderLayer } from './BaseRenderLayer';
-import { ITerminal, ICellData } from '../Types';
-import { CellData } from '../BufferLine';
+import { ITerminal } from '../Types';
+import { ICellData } from '../core/Types';
+import { CellData } from '../core/buffer/BufferLine';
interface ICursorState {
x: number;
diff --git a/src/renderer/TextRenderLayer.ts b/src/renderer/TextRenderLayer.ts
index c4bdf19f..8be5665c 100644
--- a/src/renderer/TextRenderLayer.ts
+++ b/src/renderer/TextRenderLayer.ts
@@ -3,12 +3,13 @@
* @license MIT
*/
-import { NULL_CELL_CODE } from '../Buffer';
import { IColorSet, IRenderDimensions, ICharacterJoinerRegistry } from './Types';
-import { CharData, ITerminal, ICellData } from '../Types';
+import { ITerminal } from '../Types';
+import { CharData, ICellData } from '../core/Types';
import { GridCache } from './GridCache';
import { BaseRenderLayer } from './BaseRenderLayer';
-import { CellData, AttributeData, Content } from '../BufferLine';
+import { CellData, AttributeData, Content, NULL_CELL_CODE } from '../core/buffer/BufferLine';
+import { JoinedCellData } from './CharacterJoinerRegistry';
/**
* This CharData looks like a null character, which will forc a clear and render
@@ -89,15 +90,12 @@ export class TextRenderLayer extends BaseRenderLayer {
// We already know the exact start and end column of the joined range,
// so we get the string and width representing it directly
- cell = CellData.fromCharData([
- 0,
+
+ cell = new JoinedCellData(
+ this._workCell,
line.translateToString(true, range[0], range[1]),
- range[1] - range[0],
- 0xFFFFFF
- ]);
- // hacky: patch attrs
- cell.fg = this._workCell.fg;
- cell.bg = this._workCell.bg;
+ range[1] - range[0]
+ );
// Skip over the cells occupied by this range in the loop
lastCharX = range[1] - 1;
diff --git a/src/renderer/atlas/CharAtlasUtils.ts b/src/renderer/atlas/CharAtlasUtils.ts
index 5b1add39..c2eb2e1e 100644
--- a/src/renderer/atlas/CharAtlasUtils.ts
+++ b/src/renderer/atlas/CharAtlasUtils.ts
@@ -5,7 +5,8 @@
import { ITerminal } from '../../Types';
import { IColorSet } from '../Types';
-import { DEFAULT_COLOR, ICharAtlasConfig } from './Types';
+import { ICharAtlasConfig } from './Types';
+import { DEFAULT_COLOR } from '../../common/Types';
export function generateConfig(scaledCharWidth: number, scaledCharHeight: number, terminal: ITerminal, colors: IColorSet): ICharAtlasConfig {
// null out some fields that don't matter
diff --git a/src/renderer/atlas/StaticCharAtlas.ts b/src/renderer/atlas/StaticCharAtlas.ts
index b54c833e..66beb363 100644
--- a/src/renderer/atlas/StaticCharAtlas.ts
+++ b/src/renderer/atlas/StaticCharAtlas.ts
@@ -3,10 +3,11 @@
* @license MIT
*/
-import { DIM_OPACITY, IGlyphIdentifier, DEFAULT_COLOR, ICharAtlasConfig, CHAR_ATLAS_CELL_SPACING } from './Types';
+import { DIM_OPACITY, IGlyphIdentifier, ICharAtlasConfig, CHAR_ATLAS_CELL_SPACING } from './Types';
import { generateStaticCharAtlasTexture } from './CharAtlasGenerator';
import BaseCharAtlas from './BaseCharAtlas';
import { is256Color } from './CharAtlasUtils';
+import { DEFAULT_COLOR } from '../../common/Types';
export default class StaticCharAtlas extends BaseCharAtlas {
private _texture: HTMLCanvasElement | ImageBitmap;
diff --git a/src/renderer/atlas/Types.ts b/src/renderer/atlas/Types.ts
index d03aa1f7..42f00a55 100644
--- a/src/renderer/atlas/Types.ts
+++ b/src/renderer/atlas/Types.ts
@@ -6,7 +6,6 @@
import { FontWeight } from 'xterm';
import { IColorSet } from '../Types';
-export const DEFAULT_COLOR = 256;
export const INVERTED_DEFAULT_COLOR = 257;
export const DIM_OPACITY = 0.5;
diff --git a/src/renderer/dom/DomRenderer.ts b/src/renderer/dom/DomRenderer.ts
index ea3ac5d9..862f55c8 100644
--- a/src/renderer/dom/DomRenderer.ts
+++ b/src/renderer/dom/DomRenderer.ts
@@ -101,7 +101,7 @@ export class DomRenderer extends Disposable implements IRenderer {
}
private _updateDimensions(): void {
- this.dimensions.scaledCharWidth = Math.floor(this._terminal.charMeasure.width * window.devicePixelRatio);
+ this.dimensions.scaledCharWidth = this._terminal.charMeasure.width * window.devicePixelRatio;
this.dimensions.scaledCharHeight = Math.ceil(this._terminal.charMeasure.height * window.devicePixelRatio);
this.dimensions.scaledCellWidth = this.dimensions.scaledCharWidth + Math.round(this._terminal.options.letterSpacing);
this.dimensions.scaledCellHeight = Math.floor(this.dimensions.scaledCharHeight * this._terminal.options.lineHeight);
@@ -157,8 +157,8 @@ export class DomRenderer extends Disposable implements IRenderer {
`${this._terminalSelector} .${ROW_CONTAINER_CLASS} {` +
` color: ${this.colorManager.colors.foreground.css};` +
` background-color: ${this.colorManager.colors.background.css};` +
- ` font-family: ${this._terminal.getOption('fontFamily')};` +
- ` font-size: ${this._terminal.getOption('fontSize')}px;` +
+ ` font-family: ${this._terminal.options.fontFamily};` +
+ ` font-size: ${this._terminal.options.fontSize}px;` +
`}`;
// Text styles
styles +=
@@ -174,9 +174,9 @@ export class DomRenderer extends Disposable implements IRenderer {
// Blink animation
styles +=
`@keyframes blink {` +
- ` 0 % { opacity: 1.0; }` +
+ ` 0% { opacity: 1.0; }` +
` 50% { opacity: 0.0; }` +
- ` 100 % { opacity: 1.0; }` +
+ ` 100% { opacity: 1.0; }` +
`}`;
// Cursor
styles +=
diff --git a/src/renderer/dom/DomRendererRowFactory.test.ts b/src/renderer/dom/DomRendererRowFactory.test.ts
index 076f5d6a..b2530595 100644
--- a/src/renderer/dom/DomRendererRowFactory.test.ts
+++ b/src/renderer/dom/DomRendererRowFactory.test.ts
@@ -6,9 +6,9 @@
import jsdom = require('jsdom');
import { assert } from 'chai';
import { DomRendererRowFactory } from './DomRendererRowFactory';
-import { DEFAULT_ATTR, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR_DATA } from '../../Buffer';
-import { BufferLine, CellData, FgFlags, BgFlags, Attributes } from '../../BufferLine';
-import { IBufferLine, ITerminalOptions } from '../../Types';
+import { BufferLine, CellData, FgFlags, BgFlags, Attributes, NULL_CELL_CODE, NULL_CELL_WIDTH, NULL_CELL_CHAR, DEFAULT_ATTR, DEFAULT_ATTR_DATA } from '../../core/buffer/BufferLine';
+import { ITerminalOptions } from '../../Types';
+import { IBufferLine } from '../../core/Types';
describe('DomRendererRowFactory', () => {
let dom: jsdom.JSDOM;
diff --git a/src/renderer/dom/DomRendererRowFactory.ts b/src/renderer/dom/DomRendererRowFactory.ts
index 60e2d509..85ef586e 100644
--- a/src/renderer/dom/DomRendererRowFactory.ts
+++ b/src/renderer/dom/DomRendererRowFactory.ts
@@ -3,10 +3,10 @@
* @license MIT
*/
-import { NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../Buffer';
-import { IBufferLine, ITerminalOptions } from '../../Types';
+import { ITerminalOptions } from '../../Types';
+import { IBufferLine } from '../../core/Types';
import { INVERTED_DEFAULT_COLOR } from '../atlas/Types';
-import { CellData, AttributeData } from '../../BufferLine';
+import { CellData, AttributeData, NULL_CELL_CODE, WHITESPACE_CELL_CHAR } from '../../core/buffer/BufferLine';
export const BOLD_CLASS = 'xterm-bold';
export const DIM_CLASS = 'xterm-dim';
diff --git a/src/renderer/webgl/GlyphRenderer.ts b/src/renderer/webgl/GlyphRenderer.ts
index 9ef24a71..6a6a873d 100644
--- a/src/renderer/webgl/GlyphRenderer.ts
+++ b/src/renderer/webgl/GlyphRenderer.ts
@@ -5,13 +5,14 @@
import { createProgram, PROJECTION_MATRIX } from './WebglUtils';
import { IColorManager, IRenderDimensions } from '../Types';
-import { ITerminal, IBufferLine } from '../../Types';
-import { NULL_CELL_CODE, CHAR_DATA_CHAR_INDEX, WHITESPACE_CELL_CODE } from '../../Buffer';
+import { ITerminal } from '../../Types';
import WebglCharAtlas from './WebglCharAtlas';
import { IWebGL2RenderingContext, IWebGLVertexArrayObject, IRenderModel, IRasterizedGlyph } from './Types';
import { INDICIES_PER_CELL } from './WebglRenderer';
import { COMBINED_CHAR_BIT_MASK } from './RenderModel';
import { fill, slice } from '../../common/TypedArrayUtils';
+import { NULL_CELL_CODE, WHITESPACE_CELL_CODE, CHAR_DATA_CHAR_INDEX } from '../../core/buffer/BufferLine';
+import { IBufferLine } from '../../core/Types';
interface IVertices {
attributes: Float32Array;
diff --git a/src/renderer/webgl/RectangleRenderer.ts b/src/renderer/webgl/RectangleRenderer.ts
index addc13c5..7399cc83 100644
--- a/src/renderer/webgl/RectangleRenderer.ts
+++ b/src/renderer/webgl/RectangleRenderer.ts
@@ -8,8 +8,9 @@ import { IColorManager, IRenderDimensions, IColor } from '../Types';
import { createProgram, expandFloat32Array, PROJECTION_MATRIX } from './WebglUtils';
import { IRenderModel, IWebGLVertexArrayObject, IWebGL2RenderingContext, ISelectionRenderModel } from './Types';
import { fill } from '../../common/TypedArrayUtils';
-import { INVERTED_DEFAULT_COLOR, DEFAULT_COLOR } from '../atlas/Types';
+import { INVERTED_DEFAULT_COLOR } from '../atlas/Types';
import { is256Color } from '../atlas/CharAtlasUtils';
+import { DEFAULT_COLOR } from '../../common/Types';
const enum VertexAttribLocations {
POSITION = 0,
diff --git a/src/renderer/webgl/WebglCharAtlas.ts b/src/renderer/webgl/WebglCharAtlas.ts
index 6eae9579..6963e714 100644
--- a/src/renderer/webgl/WebglCharAtlas.ts
+++ b/src/renderer/webgl/WebglCharAtlas.ts
@@ -3,14 +3,15 @@
* @license MIT
*/
-import { DIM_OPACITY, IGlyphIdentifier, INVERTED_DEFAULT_COLOR, DEFAULT_COLOR, ICharAtlasConfig } from '../atlas/Types';
+import { DIM_OPACITY, IGlyphIdentifier, INVERTED_DEFAULT_COLOR, ICharAtlasConfig } from '../atlas/Types';
import BaseCharAtlas from '../atlas/BaseCharAtlas';
import { DEFAULT_ANSI_COLORS } from '../ColorManager';
import { IRasterizedGlyph, IBoundingBox, IRasterizedGlyphSet } from './Types';
-import { DEFAULT_ATTR } from '../../Buffer';
import { FLAGS, IColor } from '../Types';
import { is256Color } from '../atlas/CharAtlasUtils';
import { clearColor } from '../atlas/CharAtlasGenerator';
+import { DEFAULT_ATTR } from '../../core/buffer/BufferLine';
+import { DEFAULT_COLOR } from '../../common/Types';
// In practice we're probably never going to exhaust a texture this large. For debugging purposes,
// however, it can be useful to set this to a really tiny value, to verify that LRU eviction works.
diff --git a/src/renderer/webgl/WebglRenderer.ts b/src/renderer/webgl/WebglRenderer.ts
index a144b947..15fe77d9 100644
--- a/src/renderer/webgl/WebglRenderer.ts
+++ b/src/renderer/webgl/WebglRenderer.ts
@@ -15,12 +15,13 @@ import { acquireCharAtlas } from '../atlas/CharAtlasCache';
import WebglCharAtlas from './WebglCharAtlas';
import { ScreenDprMonitor } from '../../ui/ScreenDprMonitor';
import { RectangleRenderer } from './RectangleRenderer';
-import { CHAR_DATA_ATTR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_CHAR_INDEX, NULL_CELL_CODE } from '../../Buffer';
import { IWebGL2RenderingContext } from './Types';
-import { INVERTED_DEFAULT_COLOR, DEFAULT_COLOR } from '../atlas/Types';
+import { INVERTED_DEFAULT_COLOR } from '../atlas/Types';
import { RenderModel, COMBINED_CHAR_BIT_MASK } from './RenderModel';
import { EventEmitter2, IEvent } from '../../common/EventEmitter2';
import { Disposable } from '../../common/Lifecycle';
+import { CHAR_DATA_CHAR_INDEX, CHAR_DATA_CODE_INDEX, CHAR_DATA_ATTR_INDEX, NULL_CELL_CODE } from '../../core/buffer/BufferLine';
+import { DEFAULT_COLOR } from '../../common/Types';
export const INDICIES_PER_CELL = 4;
diff --git a/typings/xterm.d.ts b/typings/xterm.d.ts
index 7267e5e2..a0fb74ce 100644
--- a/typings/xterm.d.ts
+++ b/typings/xterm.d.ts
@@ -275,7 +275,7 @@ declare module 'xterm' {
* A callback that fires when the mouse leaves a link. Note that this can
* happen even when tooltipCallback hasn't fired for the link yet.
*/
- leaveCallback?: (event: MouseEvent, uri: string) => boolean | void;
+ leaveCallback?: () => void;
/**
* The priority of the link matcher, this defines the order in which the link
@@ -355,11 +355,18 @@ declare module 'xterm' {
*/
readonly cols: number;
+ /**
+ * (EXPERIMENTAL) The terminal's current buffer, this might be either the
+ * normal buffer or the alt buffer depending on what's running in the
+ * terminal.
+ */
+ readonly buffer: IBuffer;
+
/**
* (EXPERIMENTAL) Get all markers registered against the buffer. If the alt
* buffer is active this will always return [].
*/
- readonly markers: IMarker[];
+ readonly markers: ReadonlyArray;
/**
* Natural language strings that can be localized.
@@ -546,12 +553,6 @@ declare module 'xterm' {
*/
resize(columns: number, rows: number): void;
- /**
- * Writes text to the terminal, followed by a break line character (\n).
- * @param data The text to write to the terminal.
- */
- writeln(data: string): void;
-
/**
* Opens the terminal within an element.
* @param parent The element to create the terminal within. This element
@@ -594,6 +595,7 @@ declare module 'xterm' {
* @return An IDisposable you can call to remove this handler.
*/
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
+
/**
* (EXPERIMENTAL) Registers a link matcher, allowing custom link patterns to
* be matched and handled.
@@ -668,11 +670,24 @@ declare module 'xterm' {
*/
getSelection(): string;
+ /**
+ * Gets the selection position or undefined if there is no selection.
+ */
+ getSelectionPosition(): ISelectionPosition | undefined;
+
/**
* Clears the current terminal selection.
*/
clearSelection(): void;
+ /**
+ * Selects text within the terminal.
+ * @param column The column the selection starts at..
+ * @param row The row the selection starts at.
+ * @param length The length of the selection.
+ */
+ select(column: number, row: number, length: number): void;
+
/**
* Selects all text within the terminal.
*/
@@ -737,6 +752,20 @@ declare module 'xterm' {
*/
write(data: string): void;
+ /**
+ * Writes text to the terminal, followed by a break line character (\n).
+ * @param data The text to write to the terminal.
+ */
+ writeln(data: string): void;
+
+ /**
+ * Writes UTF8 data to the terminal.
+ * This has a slight performance advantage over the string based write method
+ * due to lesser data conversions needed on the way from the pty to xterm.js.
+ * @param data The data to write to the terminal.
+ */
+ writeUtf8(data: Uint8Array): void;
+
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
@@ -746,7 +775,7 @@ declare module 'xterm' {
* Retrieves an option's value from the terminal.
* @param key The option key.
*/
- getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell'): boolean;
+ getOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'rightClickSelectsWord' | 'popOnBell' | 'screenKeys' | 'useFlowControl' | 'visualBell' | 'windowsMode'): boolean;
/**
* Retrieves an option's value from the terminal.
* @param key The option key.
@@ -797,7 +826,7 @@ declare module 'xterm' {
* @param key The option key.
* @param value The option value.
*/
- setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'screenKeys' | 'useFlowControl' | 'visualBell', value: boolean): void;
+ setOption(key: 'allowTransparency' | 'cancelEvents' | 'convertEol' | 'cursorBlink' | 'debug' | 'disableStdin' | 'enableBold' | 'macOptionIsMeta' | 'popOnBell' | 'rightClickSelectsWord' | 'screenKeys' | 'useFlowControl' | 'visualBell' | 'windowsMode', value: boolean): void;
/**
* Sets an option on the terminal.
* @param key The option key.
@@ -852,7 +881,133 @@ 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;
+ }
+
+ /**
+ * An object representing a selecrtion within the terminal.
+ */
+ interface ISelectionPosition {
+ /**
+ * The start column of the selection.
+ */
+ startColumn: number;
+
+ /**
+ * The start row of the selection.
+ */
+ startRow: number;
+
+ /**
+ * The end column of the selection.
+ */
+ endColumn: number;
+
+ /**
+ * The end row of the selection.
+ */
+ endRow: number;
+ }
+
+ interface IBuffer {
+ /**
+ * The y position of the cursor. This ranges between `0` (when the
+ * cursor is at baseY) and `Terminal.rows - 1` (when the cursor is on the
+ * last row).
+ */
+ readonly cursorY: number;
+
+ /**
+ * The x position of the cursor. This ranges between `0` (left side) and
+ * `Terminal.cols - 1` (right side).
+ */
+ readonly cursorX: number;
+
+ /**
+ * The line within the buffer where the top of the viewport is.
+ */
+ readonly viewportY: number;
+
+ /**
+ * The line within the buffer where the top of the bottom page is (when
+ * fully scrolled down);
+ */
+ readonly baseY: number;
+
+ /**
+ * The amount of lines in the buffer.
+ */
+ readonly length: number;
+
+ /**
+ * Gets a line from the buffer, or undefined if the line index does not exist.
+ *
+ * Note that the result of this function should be used immediately after calling as when the
+ * terminal updates it could lead to unexpected behavior.
+ *
+ * @param y The line index to get.
+ */
+ getLine(y: number): IBufferLine | undefined;
+ }
+
+ interface IBufferLine {
+ /**
+ * Whether the line is wrapped from the previous line.
+ */
+ readonly isWrapped: boolean;
+
+ /**
+ * Gets a cell from the line, or undefined if the line index does not exist.
+ *
+ * Note that the result of this function should be used immediately after calling as when the
+ * terminal updates it could lead to unexpected behavior.
+ *
+ * @param x The character index to get.
+ */
+ getCell(x: number): IBufferCell;
+
+ /**
+ * Gets the line as a string. Note that this is gets only the string for the line, not taking
+ * isWrapped into account.
+ *
+ * @param trimRight Whether to trim any whitespace at the right of the line.
+ * @param startColumn The column to start from (inclusive).
+ * @param endColumn The column to end at (exclusive).
+ */
+ translateToString(trimRight?: boolean, startColumn?: number, endColumn?: number): string;
+ }
+
+ interface IBufferCell {
+ /**
+ * The character within the cell.
+ */
+ readonly char: string;
+
+ /**
+ * The width of the character. Some examples:
+ *
+ * - This is `1` for most cells.
+ * - This is `2` for wide character like CJK glyphs.
+ * - This is `0` for cells immediately following cells with a width of `2`.
+ */
+ readonly width: number;
}
}
diff --git a/yarn.lock b/yarn.lock
index 440aa4f8..321fe6f3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -69,6 +69,13 @@
resolved "https://registry.yarnpkg.com/@types/node/-/node-6.0.108.tgz#852e8496bcfc5e74cae83a5eb3b30e5661e9b7b9"
integrity sha512-5q14jNJCPW+Iwk6Y1JxtA7T5ov1aVRS2VA2PvRgFMZtCjoIo8WT1WO56dSV0MSiHR7BEoe2QNuXigBQNqbWdAw==
+"@types/puppeteer@^1.12.4":
+ version "1.12.4"
+ resolved "https://registry.yarnpkg.com/@types/puppeteer/-/puppeteer-1.12.4.tgz#8388efdb0b30a54a7e7c4831ca0d709191d77ff1"
+ integrity sha512-aaGbJaJ9TuF9vZfTeoh876sBa+rYJWPwtsmHmYr28pGr42ewJnkDTq2aeSKEmS39SqUdkwLj73y/d7rBSp7mDQ==
+ dependencies:
+ "@types/node" "*"
+
"@types/tapable@*":
version "1.0.4"
resolved "https://registry.yarnpkg.com/@types/tapable/-/tapable-1.0.4.tgz#b4ffc7dc97b498c969b360a41eee247f82616370"
@@ -86,6 +93,11 @@
dependencies:
source-map "^0.6.1"
+"@types/utf8@^2.1.6":
+ version "2.1.6"
+ resolved "https://registry.yarnpkg.com/@types/utf8/-/utf8-2.1.6.tgz#430cabb71a42d0a3613cce5621324fe4f5a25753"
+ integrity sha512-pRs2gYF5yoKYrgSaira0DJqVg2tFuF+Qjp838xS7K+mJyY2jJzjsrl6y17GbIa4uMRogMbxs+ghNCvKg6XyNrA==
+
"@types/webpack@^4.4.11":
version "4.4.11"
resolved "https://registry.yarnpkg.com/@types/webpack/-/webpack-4.4.11.tgz#0ca832870d55c4e92498c01d22d00d02b0f62ae9"
@@ -314,6 +326,13 @@ acorn@^5.6.2:
resolved "https://registry.yarnpkg.com/acorn/-/acorn-5.7.2.tgz#91fa871883485d06708800318404e72bfb26dcc5"
integrity sha512-cJrKCNcr2kv8dlDnbw+JPUGjHZzo4myaxOLmpOX8a+rgX94YeTcTMv/LFJUSByRpc+i4GgVnnhLxvMu/2Y+rqw==
+agent-base@^4.1.0:
+ version "4.2.1"
+ resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9"
+ integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg==
+ dependencies:
+ es6-promisify "^5.0.0"
+
ajv-keywords@^3.1.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/ajv-keywords/-/ajv-keywords-3.2.0.tgz#e86b819c602cf8821ad637413698f1dec021847a"
@@ -1307,7 +1326,7 @@ concat-map@0.0.1:
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=
-concat-stream@^1.5.0, concat-stream@^1.6.1:
+concat-stream@1.6.2, concat-stream@^1.5.0, concat-stream@^1.6.1:
version "1.6.2"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.2.tgz#904bdf194cd3122fc675c77fc4ac3d4ff0fd1a34"
integrity sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==
@@ -1606,7 +1625,7 @@ debug@2.6.8:
dependencies:
ms "2.0.0"
-debug@2.X, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8:
+debug@2.6.9, debug@2.X, debug@^2.1.2, debug@^2.2.0, debug@^2.3.3, debug@^2.6.8:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
@@ -1620,6 +1639,13 @@ debug@^3.1.0:
dependencies:
ms "2.0.0"
+debug@^4.1.0:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791"
+ integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==
+ dependencies:
+ ms "^2.1.1"
+
debug@~2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
@@ -1938,6 +1964,18 @@ es6-promise@^3.0.2, es6-promise@^3.1.2:
resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613"
integrity sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM=
+es6-promise@^4.0.3:
+ version "4.2.6"
+ resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-4.2.6.tgz#b685edd8258886365ea62b57d30de28fadcd974f"
+ integrity sha512-aRVgGdnmW2OiySVPUC9e6m+plolMAJKjZnQlCwNSuK5yQ0JN61DZSO1X1Ufd1foqWRAlig0rhduTCHe7sVtK5Q==
+
+es6-promisify@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/es6-promisify/-/es6-promisify-5.0.0.tgz#5109d62f3e56ea967c4b63505aef08291c8a5203"
+ integrity sha1-UQnWLz5W6pZ8S2NQWu8IKRyKUgM=
+ dependencies:
+ es6-promise "^4.0.3"
+
es6-symbol@^3.1.1, es6-symbol@~3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.1.tgz#bf00ef4fdab6ba1b46ecb7b629b4c7ed5715cc77"
@@ -2174,6 +2212,16 @@ extglob@^2.0.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
+extract-zip@^1.6.6:
+ version "1.6.7"
+ resolved "https://registry.yarnpkg.com/extract-zip/-/extract-zip-1.6.7.tgz#a840b4b8af6403264c8db57f4f1a74333ef81fe9"
+ integrity sha1-qEC0uK9kAyZMjbV/Txp0Mz74H+k=
+ dependencies:
+ concat-stream "1.6.2"
+ debug "2.6.9"
+ mkdirp "0.5.1"
+ yauzl "2.4.1"
+
extsprintf@1.3.0:
version "1.3.0"
resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05"
@@ -2213,6 +2261,13 @@ fast-levenshtein@~2.0.4:
resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917"
integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=
+fd-slicer@~1.0.1:
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/fd-slicer/-/fd-slicer-1.0.1.tgz#8b5bcbd9ec327c5041bf9ab023fd6750f1177e65"
+ integrity sha1-i1vL2ewyfFBBv5qwI/1nUPEXfmU=
+ dependencies:
+ pend "~1.2.0"
+
figures@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962"
@@ -3042,6 +3097,14 @@ https-browserify@~0.0.0:
resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-0.0.1.tgz#3f91365cabe60b77ed0ebba24b454e3e09d95a82"
integrity sha1-P5E2XKvmC3ftDruiS0VOPgnZWoI=
+https-proxy-agent@^2.2.1:
+ version "2.2.1"
+ resolved "https://registry.yarnpkg.com/https-proxy-agent/-/https-proxy-agent-2.2.1.tgz#51552970fa04d723e04c56d04178c3f92592bbc0"
+ integrity sha512-HPCTS1LW51bcyMYbxUIOO4HEOlQ1/1qRaFWcyxvwaqUS9TY88aoEuHUY33kuAh1YhVVaDQhLZsnPd+XNARWZlQ==
+ dependencies:
+ agent-base "^4.1.0"
+ debug "^3.1.0"
+
iconv-lite@0.4.19:
version "0.4.19"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b"
@@ -4235,6 +4298,11 @@ mime@1.3.4:
resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
integrity sha1-EV+eO2s9rylZmDyzjxSaLUDrXVM=
+mime@^2.0.3:
+ version "2.4.2"
+ resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.2.tgz#ce5229a5e99ffc313abac806b482c10e7ba6ac78"
+ integrity sha512-zJBfZDkwRu+j3Pdd2aHsR5GfH2jIWhmL1ZzBoc+X+3JEti2hbArWcyJ+1laC1D2/U/W1a/+Cegj0/OnEU2ybjg==
+
mimic-fn@^1.0.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
@@ -4394,6 +4462,11 @@ ms@2.0.0:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+ms@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.1.tgz#30a5864eb3ebb0a66f2ebe6d727af06a09d86e0a"
+ integrity sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
+
multipipe@^0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/multipipe/-/multipipe-0.1.2.tgz#2a8f2ddf70eed564dff2d57f1e1a137d9f05078b"
@@ -5067,6 +5140,11 @@ pbkdf2@^3.0.3:
safe-buffer "^5.0.1"
sha.js "^2.4.8"
+pend@~1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
+ integrity sha1-elfrVQpng/kRUzH89GY9XI4AelA=
+
performance-now@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/performance-now/-/performance-now-2.1.0.tgz#6309f4e0e5fa913ec1c69307ae364b4b377c9e7b"
@@ -5165,6 +5243,11 @@ process@^0.11.10, process@~0.11.0:
resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182"
integrity sha1-czIwDoQBYb2j5podHZGn1LwW8YI=
+progress@^2.0.1:
+ version "2.0.3"
+ resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8"
+ integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==
+
promise-inflight@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
@@ -5178,6 +5261,11 @@ proxy-addr@~1.0.10:
forwarded "~0.1.0"
ipaddr.js "1.0.5"
+proxy-from-env@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.0.0.tgz#33c50398f70ea7eb96d21f7b817630a55791c7ee"
+ integrity sha1-M8UDmPcOp+uW0h97gXYwpVeRx+4=
+
prr@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476"
@@ -5243,6 +5331,20 @@ punycode@^2.1.0:
resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec"
integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==
+puppeteer@^1.15.0:
+ version "1.15.0"
+ resolved "https://registry.yarnpkg.com/puppeteer/-/puppeteer-1.15.0.tgz#1680fac13e51f609143149a5b7fa99eec392b34f"
+ integrity sha512-D2y5kwA9SsYkNUmcBzu9WZ4V1SGHiQTmgvDZSx6sRYFsgV25IebL4V6FaHjF6MbwLK9C6f3G3pmck9qmwM8H3w==
+ dependencies:
+ debug "^4.1.0"
+ extract-zip "^1.6.6"
+ https-proxy-agent "^2.2.1"
+ mime "^2.0.3"
+ progress "^2.0.1"
+ proxy-from-env "^1.0.0"
+ rimraf "^2.6.1"
+ ws "^6.1.0"
+
qs@4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-4.0.0.tgz#c31d9b74ec27df75e543a86c78728ed8d4623607"
@@ -6726,6 +6828,11 @@ user-home@^1.1.1:
resolved "https://registry.yarnpkg.com/user-home/-/user-home-1.1.1.tgz#2b5be23a32b63a7c9deb8d0f28d485724a3df190"
integrity sha1-K1viOjK2Onyd640PKNSFcko98ZA=
+utf8@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/utf8/-/utf8-3.0.0.tgz#f052eed1364d696e769ef058b183df88c87f69d1"
+ integrity sha512-E8VjFIQ/TyQgp+TZfS6l8yp/xWppSAHzidGiRrqe4bK4XP9pTRyKFgGJpO3SN7zdX4DeomTrwaseCHovfpFcqQ==
+
util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
@@ -7100,6 +7207,13 @@ ws@^4.0.0:
async-limiter "~1.0.0"
safe-buffer "~5.1.0"
+ws@^6.1.0:
+ version "6.2.1"
+ resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.1.tgz#442fdf0a47ed64f59b6a5d8ff130f4748ed524fb"
+ integrity sha512-GIyAXC2cB7LjvpgMt9EKS2ldqr0MTrORaleiOno6TweZ6r3TKtoFQWay/2PceJ3RuBasOHzXNn5Lrw1X0bEjqA==
+ dependencies:
+ async-limiter "~1.0.0"
+
xdg-basedir@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/xdg-basedir/-/xdg-basedir-2.0.0.tgz#edbc903cc385fc04523d966a335504b5504d1bd2"
@@ -7122,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-beta8:
+ version "0.1.0-beta8"
+ resolved "https://registry.yarnpkg.com/xterm-addon-attach/-/xterm-addon-attach-0.1.0-beta8.tgz#e469ed9d6ab7e535d0a9ffae23ef4f2efe58163b"
+ integrity sha512-HtQuwqnvcR+SwI9/JbBMd//Il+oEeo3rWrIucLLKHT8sB+OAOkdhmo5KIM/hhnovjI040WJ+tTHkDgPFwIJtmw==
+
+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"
@@ -7222,6 +7351,13 @@ yargs@~3.10.0:
decamelize "^1.0.0"
window-size "0.1.0"
+yauzl@2.4.1:
+ version "2.4.1"
+ resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.4.1.tgz#9528f442dab1b2284e58b4379bb194e22e0c4005"
+ integrity sha1-lSj0QtqxsihOWLQ3m7GU4i4MQAU=
+ dependencies:
+ fd-slicer "~1.0.1"
+
zmodem.js@^0.1.5:
version "0.1.7"
resolved "https://registry.yarnpkg.com/zmodem.js/-/zmodem.js-0.1.7.tgz#247affb76d2b1e3042b3fc8b4a087b9d5db8d1ed"