Merge branch 'master' into webgl2

This commit is contained in:
Daniel Imms
2019-05-18 14:38:42 -07:00
68 changed files with 2372 additions and 460 deletions
+5 -3
View File
@@ -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);
}
}
}
+2 -2
View File
@@ -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;
+5 -60
View File
@@ -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<void>();
public get onDispose(): IEvent<void> { 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
+2 -1
View File
@@ -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';
+1 -2
View File
@@ -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 {
+2 -3
View File
@@ -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', () => {
+29 -3
View File
@@ -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<void>();
@@ -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 ((<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;
+3 -2
View File
@@ -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) {
+3 -2
View File
@@ -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 {}
+4 -2
View File
@@ -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();
}
/**
+3 -4
View File
@@ -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(){};
+2 -3
View File
@@ -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,
+112 -5
View File
@@ -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.
*/
+13 -3
View File
@@ -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;
+56 -83
View File
@@ -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<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 {
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;
+1 -1
View File
@@ -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
-7
View File
@@ -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 {
+28 -32
View File
@@ -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;
}
}
+45
View File
@@ -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);
+157 -35
View File
@@ -28,63 +28,185 @@ describe('webLinks addon', () => {
});
});
it('should allow ~ character in URI path', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>term);
describe('should allow simple URI path', () => {
it('foo.com', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>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(<any>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(<any>term);
describe('should allow ~ character in URI path', () => {
it('foo.com', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>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(<any>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(<any>term);
describe('should allow : character in URI path', () => {
it('foo.com', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>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(<any>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(<any>term);
describe('should not allow : character at the end of a URI path', () => {
it('foo.com', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>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(<any>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(<any>term);
describe('should not allow " character at the end of a URI enclosed with ""', () => {
it('foo.com', () => {
const term = new MockTerminal();
webLinks.webLinksInit(<any>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(<any>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(<any>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(<any>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(<any>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(<any>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');
});
});
});
+2 -1
View File
@@ -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 + ')?';
+2
View File
@@ -6,6 +6,8 @@
import { IEvent, EventEmitter2 } from './EventEmitter2';
import { IDeleteEvent, IInsertEvent } from './CircularList';
export const DEFAULT_COLOR = 256;
export interface IDisposable {
dispose(): void;
}
+87
View File
@@ -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;
}
@@ -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');
@@ -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 = {};
}
}
@@ -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: 汉, 语');
@@ -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[];
+35
View File
@@ -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<void>();
public get onDispose(): IEvent<void> { 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();
}
}
+4
View File
@@ -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;
}
+156 -4
View File
@@ -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, 'Ä€𝄞Ö𝄞€Ü𝄞€');
});
+263 -30
View File
@@ -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;
}
+2 -1
View File
@@ -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';
+54
View File
@@ -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);
});
});
});
+56
View File
@@ -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);
}
}
+436
View File
@@ -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<any> {
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<any> {
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<any> {
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<any> {
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<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> {
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<any> {
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<any> {
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<any> {
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<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);
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<any> {
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<void> {
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');
}
}
+1 -1
View File
@@ -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
+59 -3
View File
@@ -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<void> { 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<IMarker> { 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); }
}
+8 -4
View File
@@ -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;
+7 -2
View File
@@ -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;
+53 -3
View File
@@ -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 {
+3 -2
View File
@@ -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;
+9 -11
View File
@@ -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;
+2 -1
View File
@@ -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
+2 -1
View File
@@ -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;
-1
View File
@@ -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;
+5 -5
View File
@@ -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 +=
@@ -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;
+3 -3
View File
@@ -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';
+3 -2
View File
@@ -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;
+2 -1
View File
@@ -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,
+3 -2
View File
@@ -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.
+3 -2
View File
@@ -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;