mirror of
https://github.com/wavetermdev/xterm.js.git
synced 2026-08-05 13:43:48 -07:00
Merge branch 'master' into xterm-tests
This commit is contained in:
+23
-15
@@ -8,12 +8,11 @@
|
||||
/// <reference path="../typings/xterm.d.ts"/>
|
||||
|
||||
import { Terminal } from '../lib/public/Terminal';
|
||||
import * as attach from '../lib/addons/attach/attach';
|
||||
import { AttachAddon } from 'xterm-addon-attach';
|
||||
import { SearchAddon, ISearchOptions } from 'xterm-addon-search';
|
||||
import { WebLinksAddon } from 'xterm-addon-web-links';
|
||||
|
||||
import * as fit from '../lib/addons/fit/fit';
|
||||
import * as fullscreen from '../lib/addons/fullscreen/fullscreen';
|
||||
import * as search from '../lib/addons/search/search';
|
||||
import * as webLinks from '../lib/addons/webLinks/webLinks';
|
||||
import { ISearchOptions } from '../lib/addons/search/Interfaces';
|
||||
|
||||
// Pulling in the module's types relies on the <reference> above, it's looks a
|
||||
// little weird here as we're importing "this" module
|
||||
@@ -25,14 +24,10 @@ export interface IWindowWithTerminal extends Window {
|
||||
}
|
||||
declare let window: IWindowWithTerminal;
|
||||
|
||||
Terminal.applyAddon(attach);
|
||||
Terminal.applyAddon(fit);
|
||||
Terminal.applyAddon(fullscreen);
|
||||
Terminal.applyAddon(search);
|
||||
Terminal.applyAddon(webLinks);
|
||||
|
||||
|
||||
let term;
|
||||
let searchAddon: SearchAddon;
|
||||
let protocol;
|
||||
let socketURL;
|
||||
let socket;
|
||||
@@ -85,10 +80,18 @@ function createTerminal(): void {
|
||||
while (terminalContainer.children.length) {
|
||||
terminalContainer.removeChild(terminalContainer.children[0]);
|
||||
}
|
||||
|
||||
const isWindows = ['Windows', 'Win16', 'Win32', 'WinCE'].indexOf(navigator.platform) >= 0;
|
||||
term = new Terminal({
|
||||
windowsMode: isWindows
|
||||
} as ITerminalOptions);
|
||||
|
||||
// Load addons
|
||||
const typedTerm = term as TerminalType;
|
||||
typedTerm.loadAddon(new WebLinksAddon());
|
||||
searchAddon = new SearchAddon();
|
||||
typedTerm.loadAddon(searchAddon);
|
||||
|
||||
window.term = term; // Expose `term` to window for debugging purposes
|
||||
term.onResize((size: { cols: number, rows: number }) => {
|
||||
if (!pid) {
|
||||
@@ -104,8 +107,6 @@ function createTerminal(): void {
|
||||
socketURL = protocol + location.hostname + ((location.port) ? (':' + location.port) : '') + '/terminals/';
|
||||
|
||||
term.open(terminalContainer);
|
||||
|
||||
term.webLinksInit();
|
||||
term.fit();
|
||||
term.focus();
|
||||
|
||||
@@ -114,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());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -148,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;
|
||||
}
|
||||
|
||||
|
||||
+1
-2
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<title>xterm.js demo</title>
|
||||
<link rel="stylesheet" href="/src/xterm.css" />
|
||||
<link rel="stylesheet" href="/src/addons/fullscreen/fullscreen.css" />
|
||||
<link rel="stylesheet" href="style.css" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.1.1/es6-promise.auto.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
|
||||
@@ -16,7 +15,7 @@
|
||||
<p>
|
||||
<label>Find next <input id="find-next"/></label>
|
||||
<label>Find previous <input id="find-previous"/></label>
|
||||
<label>Use regex<input type="checkbox" id="regex"/></label>
|
||||
<label>Use regex<input type="checkbox" id="regex"/></label>
|
||||
<label>Case sensitive<input type="checkbox" id="case-sensitive"/></label>
|
||||
<label>Whole word<input type="checkbox" id="whole-word"/></label>
|
||||
</p>
|
||||
|
||||
+29
-2
@@ -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);
|
||||
@@ -36,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);
|
||||
@@ -65,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;
|
||||
@@ -79,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 {
|
||||
|
||||
+3
-1
@@ -25,11 +25,13 @@ const clientConfig = {
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ["source-map-loader"],
|
||||
enforce: "pre"
|
||||
enforce: "pre",
|
||||
exclude: /node_modules/
|
||||
}
|
||||
]
|
||||
},
|
||||
resolve: {
|
||||
modules: [path.resolve(__dirname, '..'), 'node_modules'],
|
||||
extensions: [ '.tsx', '.ts', '.js' ]
|
||||
},
|
||||
output: {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"@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",
|
||||
@@ -40,10 +41,14 @@
|
||||
"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": {
|
||||
|
||||
+28
-1
@@ -12,7 +12,7 @@ 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 { 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';
|
||||
|
||||
@@ -104,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<void>();
|
||||
@@ -318,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 ((<any>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;
|
||||
|
||||
@@ -16,7 +16,7 @@ class TestTerminal extends Terminal {
|
||||
public keyPress(ev: any): boolean { return this._keyPress(ev); }
|
||||
}
|
||||
|
||||
describe('xterm.js', () => {
|
||||
describe('Terminal', () => {
|
||||
let term: TestTerminal;
|
||||
const termOptions = {
|
||||
cols: INIT_COLS,
|
||||
|
||||
+108
-1
@@ -43,7 +43,7 @@ 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';
|
||||
@@ -183,6 +183,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
// user input states
|
||||
public writeBuffer: string[];
|
||||
public writeBufferUtf8: Uint8Array[];
|
||||
private _writeInProgress: boolean;
|
||||
|
||||
/**
|
||||
@@ -340,6 +341,7 @@ export class Terminal extends EventEmitter implements ITerminal, IDisposable, II
|
||||
|
||||
// user input states
|
||||
this.writeBuffer = [];
|
||||
this.writeBufferUtf8 = [];
|
||||
this._writeInProgress = false;
|
||||
|
||||
this._xoffSentToCatchUp = false;
|
||||
@@ -1365,6 +1367,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.
|
||||
@@ -1535,6 +1619,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.
|
||||
@@ -1543,6 +1637,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.
|
||||
*/
|
||||
|
||||
+10
-1
@@ -9,7 +9,7 @@ import { IBufferLine, ICellData, IAttributeData } from './core/Types';
|
||||
import { ICircularList, XtermListener } from './common/Types';
|
||||
import { Buffer } from './Buffer';
|
||||
import * as Browser from './common/Platform';
|
||||
import { ITheme, IDisposable, IMarker, IEvent } from 'xterm';
|
||||
import { ITheme, IDisposable, IMarker, IEvent, ISelectionPosition } from 'xterm';
|
||||
import { Terminal } from './Terminal';
|
||||
import { AttributeData } from './core/buffer/BufferLine';
|
||||
|
||||
@@ -83,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.');
|
||||
}
|
||||
@@ -110,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;
|
||||
|
||||
+51
-46
@@ -3,7 +3,7 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable, IMarker } from 'xterm';
|
||||
import { ITerminalOptions as IPublicTerminalOptions, IEventEmitter, IDisposable, IMarker, ISelectionPosition } from 'xterm';
|
||||
import { IColorSet, IRenderer } from './renderer/Types';
|
||||
import { ICharset, IAttributeData, ICellData, IBufferLine, CharData } from './core/Types';
|
||||
import { ICircularList } from './common/Types';
|
||||
@@ -103,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;
|
||||
@@ -220,52 +221,56 @@ export interface ITerminal extends IPublicTerminal, IElementAccessor, IBufferAcc
|
||||
showCursor(): void;
|
||||
}
|
||||
|
||||
// Portions of the public API that are required by the internal Terminal
|
||||
export interface IPublicTerminal extends IDisposable, IEventEmitter {
|
||||
textarea: HTMLTextAreaElement;
|
||||
rows: number;
|
||||
cols: number;
|
||||
buffer: IBuffer;
|
||||
markers: IMarker[];
|
||||
onCursorMove: IEvent<void>;
|
||||
onData: IEvent<string>;
|
||||
onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>;
|
||||
onLineFeed: IEvent<void>;
|
||||
onScroll: IEvent<number>;
|
||||
onSelectionChange: IEvent<void>;
|
||||
onRender: IEvent<{ start: number, end: number }>;
|
||||
onResize: IEvent<{ cols: number, rows: number }>;
|
||||
onTitleChange: IEvent<string>;
|
||||
blur(): void;
|
||||
focus(): void;
|
||||
resize(columns: number, rows: number): void;
|
||||
writeln(data: string): void;
|
||||
open(parent: HTMLElement): void;
|
||||
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
|
||||
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
|
||||
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
|
||||
deregisterLinkMatcher(matcherId: number): void;
|
||||
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
|
||||
deregisterCharacterJoiner(joinerId: number): void;
|
||||
addMarker(cursorYOffset: number): IMarker;
|
||||
hasSelection(): boolean;
|
||||
getSelection(): string;
|
||||
clearSelection(): 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;
|
||||
getOption(key: string): any;
|
||||
setOption(key: string, value: any): void;
|
||||
refresh(start: number, end: number): void;
|
||||
reset(): void;
|
||||
textarea: HTMLTextAreaElement;
|
||||
rows: number;
|
||||
cols: number;
|
||||
buffer: IBuffer;
|
||||
markers: IMarker[];
|
||||
onCursorMove: IEvent<void>;
|
||||
onData: IEvent<string>;
|
||||
onKey: IEvent<{ key: string, domEvent: KeyboardEvent }>;
|
||||
onLineFeed: IEvent<void>;
|
||||
onScroll: IEvent<number>;
|
||||
onSelectionChange: IEvent<void>;
|
||||
onRender: IEvent<{ start: number, end: number }>;
|
||||
onResize: IEvent<{ cols: number, rows: number }>;
|
||||
onTitleChange: IEvent<string>;
|
||||
blur(): void;
|
||||
focus(): void;
|
||||
resize(columns: number, rows: number): void;
|
||||
writeln(data: string): void;
|
||||
open(parent: HTMLElement): void;
|
||||
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
|
||||
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
|
||||
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
|
||||
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
|
||||
deregisterLinkMatcher(matcherId: number): void;
|
||||
registerCharacterJoiner(handler: (text: string) => [number, number][]): number;
|
||||
deregisterCharacterJoiner(joinerId: number): void;
|
||||
addMarker(cursorYOffset: number): IMarker;
|
||||
hasSelection(): boolean;
|
||||
getSelection(): string;
|
||||
getSelectionPosition(): ISelectionPosition | undefined;
|
||||
clearSelection(): void;
|
||||
select(column: number, row: number, length: number): void;
|
||||
selectAll(): void;
|
||||
selectLines(start: number, end: number): void;
|
||||
dispose(): void;
|
||||
destroy(): void;
|
||||
scrollLines(amount: number): void;
|
||||
scrollPages(pageCount: number): void;
|
||||
scrollToTop(): void;
|
||||
scrollToBottom(): void;
|
||||
scrollToLine(line: number): void;
|
||||
clear(): void;
|
||||
write(data: string): void;
|
||||
writeUtf8(data: Uint8Array): void;
|
||||
getOption(key: string): any;
|
||||
setOption(key: string, value: any): void;
|
||||
refresh(start: number, end: number): void;
|
||||
reset(): void;
|
||||
}
|
||||
|
||||
export interface IBufferAccessor {
|
||||
|
||||
@@ -5,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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, 'Ä€𝄞Ö𝄞€Ü𝄞€');
|
||||
});
|
||||
|
||||
+262
-29
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { assert } from 'chai';
|
||||
import { AddonManager, ILoadedAddon } from './AddonManager';
|
||||
import { ITerminalAddon } from 'xterm';
|
||||
|
||||
class TestAddonManager extends AddonManager {
|
||||
public get addons(): ILoadedAddon[] {
|
||||
return this._addons;
|
||||
}
|
||||
}
|
||||
|
||||
describe('AddonManager', () => {
|
||||
let manager: TestAddonManager;
|
||||
|
||||
beforeEach(() => {
|
||||
manager = new TestAddonManager();
|
||||
});
|
||||
|
||||
describe('loadAddon', () => {
|
||||
it('should call addon constructor', () => {
|
||||
let called = false;
|
||||
class Addon implements ITerminalAddon {
|
||||
activate(terminal: any): void {
|
||||
assert.equal(terminal, 'foo', 'The first constructor arg should be Terminal');
|
||||
called = true;
|
||||
}
|
||||
dispose(): void { }
|
||||
}
|
||||
manager.loadAddon('foo' as any, new Addon());
|
||||
assert.equal(called, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('dispose', () => {
|
||||
it('should dispose all loaded addons', () => {
|
||||
let called = 0;
|
||||
class Addon implements ITerminalAddon {
|
||||
activate(): void {}
|
||||
dispose(): void { called++; }
|
||||
}
|
||||
manager.loadAddon(null, new Addon());
|
||||
manager.loadAddon(null, new Addon());
|
||||
manager.loadAddon(null, new Addon());
|
||||
assert.equal(manager.addons.length, 3);
|
||||
manager.dispose();
|
||||
assert.equal(called, 3);
|
||||
assert.equal(manager.addons.length, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
/**
|
||||
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { ITerminalAddon, IDisposable, Terminal } from 'xterm';
|
||||
|
||||
export interface ILoadedAddon {
|
||||
instance: ITerminalAddon;
|
||||
dispose: () => void;
|
||||
isDisposed: boolean;
|
||||
}
|
||||
|
||||
export class AddonManager implements IDisposable {
|
||||
protected _addons: ILoadedAddon[] = [];
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
for (let i = this._addons.length - 1; i >= 0; i--) {
|
||||
this._addons[i].instance.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public loadAddon(terminal: Terminal, instance: ITerminalAddon): void {
|
||||
const loadedAddon: ILoadedAddon = {
|
||||
instance,
|
||||
dispose: instance.dispose,
|
||||
isDisposed: false
|
||||
};
|
||||
this._addons.push(loadedAddon);
|
||||
instance.dispose = () => this._wrappedAddonDispose(loadedAddon);
|
||||
instance.activate(<any>terminal);
|
||||
}
|
||||
|
||||
private _wrappedAddonDispose(loadedAddon: ILoadedAddon): void {
|
||||
if (loadedAddon.isDisposed) {
|
||||
// Do nothing if already disposed
|
||||
return;
|
||||
}
|
||||
let index = -1;
|
||||
for (let i = 0; i < this._addons.length; i++) {
|
||||
if (this._addons[i] === loadedAddon) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index === -1) {
|
||||
throw new Error('Could not dispose an addon that has not been loaded');
|
||||
}
|
||||
loadedAddon.isDisposed = true;
|
||||
loadedAddon.dispose();
|
||||
this._addons.splice(index, 1);
|
||||
}
|
||||
}
|
||||
@@ -47,8 +47,9 @@ describe('API Integration Tests', () => {
|
||||
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');
|
||||
assert.equal(await page.evaluate(`window.term.buffer.getLine(0).translateToString(true)`), 'foobar文');
|
||||
});
|
||||
|
||||
it('writeln', async function(): Promise<any> {
|
||||
@@ -57,9 +58,25 @@ describe('API Integration Tests', () => {
|
||||
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<any> {
|
||||
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<any> {
|
||||
@@ -89,16 +106,23 @@ describe('API Integration Tests', () => {
|
||||
|
||||
it('selection', async function(): Promise<any> {
|
||||
this.timeout(10000);
|
||||
await openTerminal({ rows: 5 });
|
||||
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<any> {
|
||||
@@ -111,6 +135,52 @@ describe('API Integration Tests', () => {
|
||||
assert.equal(await page.evaluate(`document.activeElement.className`), '');
|
||||
});
|
||||
|
||||
describe('loadAddon', () => {
|
||||
it('constructor', async function(): Promise<any> {
|
||||
this.timeout(10000);
|
||||
await openTerminal({ cols: 5 });
|
||||
await page.evaluate(`
|
||||
window.cols = 0;
|
||||
window.term.loadAddon({
|
||||
activate: (t) => window.cols = t.cols,
|
||||
dispose: () => {}
|
||||
});
|
||||
`);
|
||||
assert.equal(await page.evaluate(`window.cols`), 5);
|
||||
});
|
||||
|
||||
it('dispose (addon)', async function(): Promise<any> {
|
||||
this.timeout(10000);
|
||||
await openTerminal();
|
||||
await page.evaluate(`
|
||||
window.disposeCalled = false
|
||||
window.addon = {
|
||||
activate: () => {},
|
||||
dispose: () => window.disposeCalled = true
|
||||
};
|
||||
window.term.loadAddon(window.addon);
|
||||
`);
|
||||
assert.equal(await page.evaluate(`window.disposeCalled`), false);
|
||||
await page.evaluate(`window.addon.dispose()`);
|
||||
assert.equal(await page.evaluate(`window.disposeCalled`), true);
|
||||
});
|
||||
|
||||
it('dispose (terminal)', async function(): Promise<any> {
|
||||
this.timeout(10000);
|
||||
await openTerminal();
|
||||
await page.evaluate(`
|
||||
window.disposeCalled = false
|
||||
window.term.loadAddon({
|
||||
activate: () => {},
|
||||
dispose: () => window.disposeCalled = true
|
||||
});
|
||||
`);
|
||||
assert.equal(await page.evaluate(`window.disposeCalled`), false);
|
||||
await page.evaluate(`window.term.dispose()`);
|
||||
assert.equal(await page.evaluate(`window.disposeCalled`), true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Events', () => {
|
||||
it('onCursorMove', async function(): Promise<any> {
|
||||
this.timeout(10000);
|
||||
|
||||
@@ -7,7 +7,7 @@ import { assert } from 'chai';
|
||||
import { Terminal } from './Terminal';
|
||||
import * as attach from '../addons/attach/attach';
|
||||
|
||||
describe('Terminal', () => {
|
||||
describe('Terminal', () => {
|
||||
it('should apply addons with Terminal.applyAddon', () => {
|
||||
Terminal.applyAddon(attach);
|
||||
// Test that addon was applied successfully, adding attach to Terminal's
|
||||
|
||||
+17
-1
@@ -3,18 +3,21 @@
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm';
|
||||
import { Terminal as ITerminalApi, ITerminalOptions, IMarker, IDisposable, ILinkMatcherOptions, ITheme, ILocalizableStrings, ITerminalAddon, ISelectionPosition, IBuffer as IBufferApi, IBufferLine as IBufferLineApi, IBufferCell as IBufferCellApi } from 'xterm';
|
||||
import { ITerminal, IBuffer } from '../Types';
|
||||
import { IBufferLine } from '../core/Types';
|
||||
import { Terminal as TerminalCore } from '../Terminal';
|
||||
import * as Strings from '../Strings';
|
||||
import { IEvent } from '../common/EventEmitter2';
|
||||
import { AddonManager } from './AddonManager';
|
||||
|
||||
export class Terminal implements ITerminalApi {
|
||||
private _core: ITerminal;
|
||||
private _addonManager: AddonManager;
|
||||
|
||||
constructor(options?: ITerminalOptions) {
|
||||
this._core = new TerminalCore(options);
|
||||
this._addonManager = new AddonManager();
|
||||
}
|
||||
|
||||
public get onCursorMove(): IEvent<void> { return this._core.onCursorMove; }
|
||||
@@ -96,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();
|
||||
}
|
||||
@@ -109,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 {
|
||||
@@ -135,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[];
|
||||
@@ -167,6 +180,9 @@ export class Terminal implements ITerminalApi {
|
||||
public static applyAddon(addon: any): void {
|
||||
addon.apply(Terminal);
|
||||
}
|
||||
public loadAddon(addon: ITerminalAddon): void {
|
||||
return this._addonManager.loadAddon(this, addon);
|
||||
}
|
||||
public static get strings(): ILocalizableStrings {
|
||||
return Strings;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user