Merge core into common

Part of #1507
This commit is contained in:
Daniel Imms
2019-06-08 16:09:27 -07:00
parent df4a96493c
commit f8e7ff24d9
41 changed files with 166 additions and 192 deletions
+102
View File
@@ -56,3 +56,105 @@ export interface ICircularList<T> {
trimStart(count: number): void;
shiftElements(start: number, count: number, offset: number): void;
}
export const enum KeyboardResultType {
SEND_KEY,
SELECT_ALL,
PAGE_UP,
PAGE_DOWN
}
export interface IKeyboardResult {
type: KeyboardResultType;
cancel: boolean;
key: string | undefined;
}
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;
}
+367
View File
@@ -0,0 +1,367 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import * as chai from 'chai';
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} {
return this._combined;
}
public toArray(): CharData[] {
const result = [];
for (let i = 0; i < this.length; ++i) {
result.push(this.loadCell(i, new CellData()).getAsCharData());
}
return result;
}
}
describe('CellData', () => {
it('CharData <--> CellData equality', () => {
const cell = new CellData();
// ASCII
cell.setFromCharData([123, 'a', 1, 'a'.charCodeAt(0)]);
chai.assert.deepEqual(cell.getAsCharData(), [123, 'a', 1, 'a'.charCodeAt(0)]);
chai.assert.equal(cell.isCombined(), 0);
// combining
cell.setFromCharData([123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, '\u0301'.charCodeAt(0)]);
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
// surrogate
cell.setFromCharData([123, '𝄞', 1, 0x1D11E]);
chai.assert.deepEqual(cell.getAsCharData(), [123, '𝄞', 1, 0x1D11E]);
chai.assert.equal(cell.isCombined(), 0);
// surrogate + combining
cell.setFromCharData([123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]);
chai.assert.deepEqual(cell.getAsCharData(), [123, '𓂀\u0301', 1, '𓂀\u0301'.charCodeAt(2)]);
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
// wide char
cell.setFromCharData([123, '1', 2, '1'.charCodeAt(0)]);
chai.assert.deepEqual(cell.getAsCharData(), [123, '1', 2, '1'.charCodeAt(0)]);
chai.assert.equal(cell.isCombined(), 0);
});
});
describe('BufferLine', function(): void {
it('ctor', function(): void {
let line: IBufferLine = new TestBufferLine(0);
chai.expect(line.length).equals(0);
chai.expect(line.isWrapped).equals(false);
line = new TestBufferLine(10);
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, 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);
line = new TestBufferLine(10, CellData.fromCharData([123, 'a', 456, 'a'.charCodeAt(0)]), true);
chai.expect(line.length).equals(10);
chai.expect(line.loadCell(0, new CellData()).getAsCharData()).eql([123, 'a', 456, 'a'.charCodeAt(0)]);
chai.expect(line.isWrapped).equals(true);
});
it('insertCells', function(): void {
const line = new TestBufferLine(3);
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)]));
line.insertCells(1, 3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
chai.expect(line.toArray()).eql([
[1, 'a', 0, 'a'.charCodeAt(0)],
[4, 'd', 0, 'd'.charCodeAt(0)],
[4, 'd', 0, 'd'.charCodeAt(0)]
]);
});
it('deleteCells', function(): void {
const line = new TestBufferLine(5);
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)]));
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
line.deleteCells(1, 2, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)]));
chai.expect(line.toArray()).eql([
[1, 'a', 0, 'a'.charCodeAt(0)],
[4, 'd', 0, 'd'.charCodeAt(0)],
[5, 'e', 0, 'e'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)]
]);
});
it('replaceCells', function(): void {
const line = new TestBufferLine(5);
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)]));
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
line.replaceCells(2, 4, CellData.fromCharData([6, 'f', 0, 'f'.charCodeAt(0)]));
chai.expect(line.toArray()).eql([
[1, 'a', 0, 'a'.charCodeAt(0)],
[2, 'b', 0, 'b'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)],
[6, 'f', 0, 'f'.charCodeAt(0)],
[5, 'e', 0, 'e'.charCodeAt(0)]
]);
});
it('fill', function(): void {
const line = new TestBufferLine(5);
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)]));
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
line.fill(CellData.fromCharData([123, 'z', 0, 'z'.charCodeAt(0)]));
chai.expect(line.toArray()).eql([
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)],
[123, 'z', 0, 'z'.charCodeAt(0)]
]);
});
it('clone', function(): void {
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)]));
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
const line2 = line.clone();
chai.expect(TestBufferLine.prototype.toArray.apply(line2)).eql(line.toArray());
chai.expect(line2.length).equals(line.length);
chai.expect(line2.isWrapped).equals(line.isWrapped);
});
it('copyFrom', function(): void {
const line = new TestBufferLine(5);
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)]));
line.setCell(3, CellData.fromCharData([4, 'd', 0, 'd'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([5, 'e', 0, 'e'.charCodeAt(0)]));
const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]), true);
line2.copyFrom(line);
chai.expect(line2.toArray()).eql(line.toArray());
chai.expect(line2.length).equals(line.length);
chai.expect(line2.isWrapped).equals(line.isWrapped);
});
it('should support combining chars', function(): void {
// CHAR_DATA_CODE_INDEX resembles current behavior in InputHandler.print
// --> set code to the last charCodeAt value of the string
// Note: needs to be fixed once the string pointer is in place
const line = new TestBufferLine(2, CellData.fromCharData([1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]));
chai.expect(line.toArray()).eql([[1, 'e\u0301', 0, '\u0301'.charCodeAt(0)], [1, 'e\u0301', 0, '\u0301'.charCodeAt(0)]]);
const line2 = new TestBufferLine(5, CellData.fromCharData([1, 'a', 0, '\u0301'.charCodeAt(0)]), true);
line2.copyFrom(line);
chai.expect(line2.toArray()).eql(line.toArray());
const line3 = line.clone();
chai.expect(TestBufferLine.prototype.toArray.apply(line3)).eql(line.toArray());
});
describe('resize', 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) 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) 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) 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) 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, [ 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)]));
chai.expect(line.translateToString()).eql('aa😁aa');
line.resize(10, CellData.fromCharData([1, 'a', 0, 'a'.charCodeAt(0)]));
chai.expect(line.translateToString()).eql('aa😁aaaaaaa');
chai.expect(Object.keys(line.combined).length).eql(1);
});
});
describe('getTrimLength', function(): void {
it('empty line', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
chai.expect(line.getTrimmedLength()).equal(0);
});
it('ASCII', 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, 'a', 1, 'a'.charCodeAt(0)]));
chai.expect(line.getTrimmedLength()).equal(3);
});
it('surrogate', 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, '𝄞'.charCodeAt(0)]));
chai.expect(line.getTrimmedLength()).equal(3);
});
it('combining', 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, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
chai.expect(line.getTrimmedLength()).equal(3);
});
it('fullwidth', 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, 0]));
chai.expect(line.getTrimmedLength()).equal(4); // also counts null cell after fullwidth
});
});
describe('translateToString with and w\'o trimming', function(): void {
it('empty line', function(): void {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
chai.expect(line.translateToString(false)).equal(' ');
chai.expect(line.translateToString(true)).equal('');
});
it('ASCII', 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, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.expect(line.translateToString(false)).equal('a a aa ');
chai.expect(line.translateToString(true)).equal('a a aa');
chai.expect(line.translateToString(false, 0, 5)).equal('a a a');
chai.expect(line.translateToString(false, 0, 4)).equal('a a ');
chai.expect(line.translateToString(false, 0, 3)).equal('a a');
chai.expect(line.translateToString(true, 0, 5)).equal('a a a');
chai.expect(line.translateToString(true, 0, 4)).equal('a a ');
chai.expect(line.translateToString(true, 0, 3)).equal('a a');
});
it('surrogate', 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, '𝄞'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
line.setCell(5, CellData.fromCharData([1, '𝄞', 1, '𝄞'.charCodeAt(0)]));
chai.expect(line.translateToString(false)).equal('a 𝄞 𝄞𝄞 ');
chai.expect(line.translateToString(true)).equal('a 𝄞 𝄞𝄞');
chai.expect(line.translateToString(false, 0, 5)).equal('a 𝄞 𝄞');
chai.expect(line.translateToString(false, 0, 4)).equal('a 𝄞 ');
chai.expect(line.translateToString(false, 0, 3)).equal('a 𝄞');
chai.expect(line.translateToString(true, 0, 5)).equal('a 𝄞 𝄞');
chai.expect(line.translateToString(true, 0, 4)).equal('a 𝄞 ');
chai.expect(line.translateToString(true, 0, 3)).equal('a 𝄞');
});
it('combining', 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, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
line.setCell(5, CellData.fromCharData([1, 'e\u0301', 1, '\u0301'.charCodeAt(0)]));
chai.expect(line.translateToString(false)).equal('a e\u0301 e\u0301e\u0301 ');
chai.expect(line.translateToString(true)).equal('a e\u0301 e\u0301e\u0301');
chai.expect(line.translateToString(false, 0, 5)).equal('a e\u0301 e\u0301');
chai.expect(line.translateToString(false, 0, 4)).equal('a e\u0301 ');
chai.expect(line.translateToString(false, 0, 3)).equal('a e\u0301');
chai.expect(line.translateToString(true, 0, 5)).equal('a e\u0301 e\u0301');
chai.expect(line.translateToString(true, 0, 4)).equal('a e\u0301 ');
chai.expect(line.translateToString(true, 0, 3)).equal('a e\u0301');
});
it('fullwidth', 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, 0]));
line.setCell(5, CellData.fromCharData([1, '1', 2, '1'.charCodeAt(0)]));
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, 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');
chai.expect(line.translateToString(false, 0, 6)).equal('a 1 1');
chai.expect(line.translateToString(false, 0, 5)).equal('a 1 ');
chai.expect(line.translateToString(false, 0, 4)).equal('a 1');
chai.expect(line.translateToString(false, 0, 3)).equal('a 1');
chai.expect(line.translateToString(false, 0, 2)).equal('a ');
chai.expect(line.translateToString(true, 0, 7)).equal('a 1 1');
chai.expect(line.translateToString(true, 0, 6)).equal('a 1 1');
chai.expect(line.translateToString(true, 0, 5)).equal('a 1 ');
chai.expect(line.translateToString(true, 0, 4)).equal('a 1');
chai.expect(line.translateToString(true, 0, 3)).equal('a 1');
chai.expect(line.translateToString(true, 0, 2)).equal('a ');
});
it('space at end', 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, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(4, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(5, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
line.setCell(6, CellData.fromCharData([1, ' ', 1, ' '.charCodeAt(0)]));
chai.expect(line.translateToString(false)).equal('a a aa ');
chai.expect(line.translateToString(true)).equal('a a aa ');
});
it('should always return some sane value', function(): void {
// sanity check - broken line with invalid out of bound null width cells
// this can atm happen with deleting/inserting chars in inputhandler by "breaking"
// fullwidth pairs --> needs to be fixed after settling BufferLine impl
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
chai.expect(line.translateToString(false)).equal(' ');
chai.expect(line.translateToString(true)).equal('');
});
it('should work with endCol=0', () => {
const line = new TestBufferLine(10, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, 0, NULL_CELL_CODE]), false);
line.setCell(0, CellData.fromCharData([1, 'a', 1, 'a'.charCodeAt(0)]));
chai.expect(line.translateToString(true, 0, 0)).equal('');
});
});
describe('addCharToCell', () => {
it('should set width to 1 for empty cell', () => {
const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
line.addCodepointToCell(0, '\u0301'.charCodeAt(0));
const cell = line.loadCell(0, new CellData());
// chars contains single combining char
// width is set to 1
chai.assert.deepEqual(cell.getAsCharData(), [DEFAULT_ATTR, '\u0301', 1, 0x0301]);
// do not account a single combining char as combined
chai.assert.equal(cell.isCombined(), 0);
});
it('should add char to combining string in cell', () => {
const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
const cell = line .loadCell(0, new CellData());
cell.setFromCharData([123, 'e\u0301', 1, 'e\u0301'.charCodeAt(1)]);
line.setCell(0, cell);
line.addCodepointToCell(0, '\u0301'.charCodeAt(0));
line.loadCell(0, cell);
// chars contains 3 chars
// width is set to 1
chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301\u0301', 1, 0x0301]);
// do not account a single combining char as combined
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
});
it('should create combining string on taken cell', () => {
const line = new TestBufferLine(3, CellData.fromCharData([DEFAULT_ATTR, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE]), false);
const cell = line .loadCell(0, new CellData());
cell.setFromCharData([123, 'e', 1, 'e'.charCodeAt(1)]);
line.setCell(0, cell);
line.addCodepointToCell(0, '\u0301'.charCodeAt(0));
line.loadCell(0, cell);
// chars contains 2 chars
// width is set to 1
chai.assert.deepEqual(cell.getAsCharData(), [123, 'e\u0301', 1, 0x0301]);
// do not account a single combining char as combined
chai.assert.equal(cell.isCombined(), Content.IS_COMBINED_MASK);
});
});
});
+664
View File
@@ -0,0 +1,664 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { DEFAULT_COLOR, CharData, IBufferLine, ICellData, IColorRGB, IAttributeData } from 'common/Types';
import { stringFromCodePoint } from 'common/input/TextDecoder';
export const DEFAULT_ATTR = (0 << 18) | (DEFAULT_COLOR << 9) | (256 << 0);
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:
*
* | uint32_t | uint32_t | uint32_t |
* | `content` | `FG` | `BG` |
* | wcwidth(2) comb(1) codepoint(21) | flags(8) R(8) G(8) B(8) | flags(8) R(8) G(8) B(8) |
*/
/** typed array slots taken by one cell */
const CELL_SIZE = 3;
/**
* Cell member indices.
*
* Direct access:
* `content = data[column * CELL_SIZE + Cell.CONTENT];`
* `fg = data[column * CELL_SIZE + Cell.FG];`
* `bg = data[column * CELL_SIZE + Cell.BG];`
*/
const enum Cell {
CONTENT = 0,
FG = 1, // currently simply holds all known attrs
BG = 2 // currently unused
}
/**
* Bitmasks for accessing data in `content`.
*/
export const enum Content {
/**
* bit 1..21 codepoint, max allowed in UTF32 is 0x10FFFF (21 bits taken)
* read: `codepoint = content & Content.codepointMask;`
* write: `content |= codepoint & Content.codepointMask;`
* shortcut if precondition `codepoint <= 0x10FFFF` is met:
* `content |= codepoint;`
*/
CODEPOINT_MASK = 0x1FFFFF,
/**
* bit 22 flag indication whether a cell contains combined content
* read: `isCombined = content & Content.isCombined;`
* set: `content |= Content.isCombined;`
* clear: `content &= ~Content.isCombined;`
*/
IS_COMBINED_MASK = 0x200000, // 1 << 21
/**
* bit 1..22 mask to check whether a cell contains any string data
* we need to check for codepoint and isCombined bits to see
* whether a cell contains anything
* read: `isEmpty = !(content & Content.hasContent)`
*/
HAS_CONTENT_MASK = 0x3FFFFF,
/**
* bit 23..24 wcwidth value of cell, takes 2 bits (ranges from 0..2)
* read: `width = (content & Content.widthMask) >> Content.widthShift;`
* `hasWidth = content & Content.widthMask;`
* as long as wcwidth is highest value in `content`:
* `width = content >> Content.widthShift;`
* write: `content |= (width << Content.widthShift) & Content.widthMask;`
* shortcut if precondition `0 <= width <= 3` is met:
* `content |= width << Content.widthShift;`
*/
WIDTH_MASK = 0xC00000, // 3 << 22
WIDTH_SHIFT = 22
}
export const enum Attributes {
/**
* bit 1..8 blue in RGB, color in P256 and P16
*/
BLUE_MASK = 0xFF,
BLUE_SHIFT = 0,
PCOLOR_MASK = 0xFF,
PCOLOR_SHIFT = 0,
/**
* bit 9..16 green in RGB
*/
GREEN_MASK = 0xFF00,
GREEN_SHIFT = 8,
/**
* bit 17..24 red in RGB
*/
RED_MASK = 0xFF0000,
RED_SHIFT = 16,
/**
* bit 25..26 color mode: DEFAULT (0) | P16 (1) | P256 (2) | RGB (3)
*/
CM_MASK = 0x3000000,
CM_DEFAULT = 0,
CM_P16 = 0x1000000,
CM_P256 = 0x2000000,
CM_RGB = 0x3000000,
/**
* bit 1..24 RGB room
*/
RGB_MASK = 0xFFFFFF
}
export const enum FgFlags {
/**
* bit 27..31 (32th bit unused)
*/
INVERSE = 0x4000000,
BOLD = 0x8000000,
UNDERLINE = 0x10000000,
BLINK = 0x20000000,
INVISIBLE = 0x40000000
}
export const enum BgFlags {
/**
* bit 27..32 (upper 4 unused)
*/
ITALIC = 0x4000000,
DIM = 0x8000000
}
export class AttributeData implements IAttributeData {
static toColorRGB(value: number): IColorRGB {
return [
value >>> Attributes.RED_SHIFT & 255,
value >>> Attributes.GREEN_SHIFT & 255,
value & 255
];
}
static fromColorRGB(value: IColorRGB): number {
return (value[0] & 255) << Attributes.RED_SHIFT | (value[1] & 255) << Attributes.GREEN_SHIFT | value[2] & 255;
}
public clone(): IAttributeData {
const newObj = new AttributeData();
newObj.fg = this.fg;
newObj.bg = this.bg;
return newObj;
}
// data
public fg: number = 0;
public bg: number = 0;
// flags
public isInverse(): number { return this.fg & FgFlags.INVERSE; }
public isBold(): number { return this.fg & FgFlags.BOLD; }
public isUnderline(): number { return this.fg & FgFlags.UNDERLINE; }
public isBlink(): number { return this.fg & FgFlags.BLINK; }
public isInvisible(): number { return this.fg & FgFlags.INVISIBLE; }
public isItalic(): number { return this.bg & BgFlags.ITALIC; }
public isDim(): number { return this.bg & BgFlags.DIM; }
// color modes
public getFgColorMode(): number { return this.fg & Attributes.CM_MASK; }
public getBgColorMode(): number { return this.bg & Attributes.CM_MASK; }
public isFgRGB(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_RGB; }
public isBgRGB(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_RGB; }
public isFgPalette(): boolean { return (this.fg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.fg & Attributes.CM_MASK) === Attributes.CM_P256; }
public isBgPalette(): boolean { return (this.bg & Attributes.CM_MASK) === Attributes.CM_P16 || (this.bg & Attributes.CM_MASK) === Attributes.CM_P256; }
public isFgDefault(): boolean { return (this.fg & Attributes.CM_MASK) === 0; }
public isBgDefault(): boolean { return (this.bg & Attributes.CM_MASK) === 0; }
// colors
public getFgColor(): number {
switch (this.fg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256: return this.fg & Attributes.PCOLOR_MASK;
case Attributes.CM_RGB: return this.fg & Attributes.RGB_MASK;
default: return -1; // CM_DEFAULT defaults to -1
}
}
public getBgColor(): number {
switch (this.bg & Attributes.CM_MASK) {
case Attributes.CM_P16:
case Attributes.CM_P256: return this.bg & Attributes.PCOLOR_MASK;
case Attributes.CM_RGB: return this.bg & Attributes.RGB_MASK;
default: return -1; // CM_DEFAULT defaults to -1
}
}
}
export const DEFAULT_ATTR_DATA = Object.freeze(new AttributeData());
/**
* CellData - represents a single Cell in the terminal buffer.
*/
export class CellData extends AttributeData implements ICellData {
/** Helper to create CellData from CharData. */
public static fromCharData(value: CharData): CellData {
const obj = new CellData();
obj.setFromCharData(value);
return obj;
}
/** Primitives from terminal buffer. */
public content: number = 0;
public fg: number = 0;
public bg: number = 0;
public combinedData: string = '';
/** Whether cell contains a combined string. */
public isCombined(): number {
return this.content & Content.IS_COMBINED_MASK;
}
/** Width of the cell. */
public getWidth(): number {
return this.content >> Content.WIDTH_SHIFT;
}
/** JS string of the content. */
public getChars(): string {
if (this.content & Content.IS_COMBINED_MASK) {
return this.combinedData;
}
if (this.content & Content.CODEPOINT_MASK) {
return stringFromCodePoint(this.content & Content.CODEPOINT_MASK);
}
return '';
}
/**
* Codepoint of cell
* Note this returns the UTF32 codepoint of single chars,
* if content is a combined string it returns the codepoint
* of the last char in string to be in line with code in CharData.
* */
public getCode(): number {
return (this.isCombined())
? this.combinedData.charCodeAt(this.combinedData.length - 1)
: this.content & Content.CODEPOINT_MASK;
}
/** Set data from CharData */
public setFromCharData(value: CharData): void {
this.fg = value[CHAR_DATA_ATTR_INDEX];
this.bg = 0;
let combined = false;
// surrogates and combined strings need special treatment
if (value[CHAR_DATA_CHAR_INDEX].length > 2) {
combined = true;
} else if (value[CHAR_DATA_CHAR_INDEX].length === 2) {
const code = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0);
// if the 2-char string is a surrogate create single codepoint
// everything else is combined
if (0xD800 <= code && code <= 0xDBFF) {
const second = value[CHAR_DATA_CHAR_INDEX].charCodeAt(1);
if (0xDC00 <= second && second <= 0xDFFF) {
this.content = ((code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
} else {
combined = true;
}
} else {
combined = true;
}
} else {
this.content = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
}
if (combined) {
this.combinedData = value[CHAR_DATA_CHAR_INDEX];
this.content = Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
}
}
/** Get data as CharData. */
public getAsCharData(): CharData {
return [this.fg, this.getChars(), this.getWidth(), this.getCode()];
}
}
/**
* Typed array based bufferline implementation.
*
* There are 2 ways to insert data into the cell buffer:
* - `setCellFromCodepoint` + `addCodepointToCell`
* Use these for data that is already UTF32.
* Used during normal input in `InputHandler` for faster buffer access.
* - `setCell`
* This method takes a CellData object and stores the data in the buffer.
* Use `CellData.fromCharData` to create the CellData object (e.g. from JS string).
*
* To retrieve data from the buffer use either one of the primitive methods
* (if only one particular value is needed) or `loadCell`. For `loadCell` in a loop
* memory allocs / GC pressure can be greatly reduced by reusing the CellData object.
*/
export class BufferLine implements IBufferLine {
protected _data: Uint32Array;
protected _combined: {[index: number]: string} = {};
public length: number;
constructor(cols: number, fillCellData?: ICellData, public isWrapped: boolean = false) {
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;
}
/**
* Get cell data CharData.
* @deprecated
*/
public get(index: number): CharData {
const content = this._data[index * CELL_SIZE + Cell.CONTENT];
const cp = content & Content.CODEPOINT_MASK;
return [
this._data[index * CELL_SIZE + Cell.FG],
(content & Content.IS_COMBINED_MASK)
? this._combined[index]
: (cp) ? stringFromCodePoint(cp) : '',
content >> Content.WIDTH_SHIFT,
(content & Content.IS_COMBINED_MASK)
? this._combined[index].charCodeAt(this._combined[index].length - 1)
: cp
];
}
/**
* Set cell data from CharData.
* @deprecated
*/
public set(index: number, value: CharData): void {
this._data[index * CELL_SIZE + Cell.FG] = value[CHAR_DATA_ATTR_INDEX];
if (value[CHAR_DATA_CHAR_INDEX].length > 1) {
this._combined[index] = value[1];
this._data[index * CELL_SIZE + Cell.CONTENT] = index | Content.IS_COMBINED_MASK | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
} else {
this._data[index * CELL_SIZE + Cell.CONTENT] = value[CHAR_DATA_CHAR_INDEX].charCodeAt(0) | (value[CHAR_DATA_WIDTH_INDEX] << Content.WIDTH_SHIFT);
}
}
/**
* primitive getters
* use these when only one value is needed, otherwise use `loadCell`
*/
public getWidth(index: number): number {
return this._data[index * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT;
}
/** Test whether content has width. */
public hasWidth(index: number): number {
return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.WIDTH_MASK;
}
/** Get FG cell component. */
public getFg(index: number): number {
return this._data[index * CELL_SIZE + Cell.FG];
}
/** Get BG cell component. */
public getBg(index: number): number {
return this._data[index * CELL_SIZE + Cell.BG];
}
/**
* Test whether contains any chars.
* Basically an empty has no content, but other cells might differ in FG/BG
* from real empty cells.
* */
public hasContent(index: number): number {
return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK;
}
/**
* Get codepoint of the cell.
* To be in line with `code` in CharData this either returns
* a single UTF32 codepoint or the last codepoint of a combined string.
*/
public getCodePoint(index: number): number {
const content = this._data[index * CELL_SIZE + Cell.CONTENT];
if (content & Content.IS_COMBINED_MASK) {
return this._combined[index].charCodeAt(this._combined[index].length - 1);
}
return content & Content.CODEPOINT_MASK;
}
/** Test whether the cell contains a combined string. */
public isCombined(index: number): number {
return this._data[index * CELL_SIZE + Cell.CONTENT] & Content.IS_COMBINED_MASK;
}
/** Returns the string content of the cell. */
public getString(index: number): string {
const content = this._data[index * CELL_SIZE + Cell.CONTENT];
if (content & Content.IS_COMBINED_MASK) {
return this._combined[index];
}
if (content & Content.CODEPOINT_MASK) {
return stringFromCodePoint(content & Content.CODEPOINT_MASK);
}
// return empty string for empty cells
return '';
}
/**
* Load data at `index` into `cell`. This is used to access cells in a way that's more friendly
* to GC as it significantly reduced the amount of new objects/references needed.
*/
public loadCell(index: number, cell: ICellData): ICellData {
const startIndex = index * CELL_SIZE;
cell.content = this._data[startIndex + Cell.CONTENT];
cell.fg = this._data[startIndex + Cell.FG];
cell.bg = this._data[startIndex + Cell.BG];
if (cell.content & Content.IS_COMBINED_MASK) {
cell.combinedData = this._combined[index];
}
return cell;
}
/**
* Set data at `index` to `cell`.
*/
public setCell(index: number, cell: ICellData): void {
if (cell.content & Content.IS_COMBINED_MASK) {
this._combined[index] = cell.combinedData;
}
this._data[index * CELL_SIZE + Cell.CONTENT] = cell.content;
this._data[index * CELL_SIZE + Cell.FG] = cell.fg;
this._data[index * CELL_SIZE + Cell.BG] = cell.bg;
}
/**
* Set cell data from input handler.
* Since the input handler see the incoming chars as UTF32 codepoints,
* it gets an optimized access method.
*/
public setCellFromCodePoint(index: number, codePoint: number, width: number, fg: number, bg: number): void {
this._data[index * CELL_SIZE + Cell.CONTENT] = codePoint | (width << Content.WIDTH_SHIFT);
this._data[index * CELL_SIZE + Cell.FG] = fg;
this._data[index * CELL_SIZE + Cell.BG] = bg;
}
/**
* Add a codepoint to a cell from input handler.
* During input stage combining chars with a width of 0 follow and stack
* onto a leading char. Since we already set the attrs
* by the previous `setDataFromCodePoint` call, we can omit it here.
*/
public addCodepointToCell(index: number, codePoint: number): void {
let content = this._data[index * CELL_SIZE + Cell.CONTENT];
if (content & Content.IS_COMBINED_MASK) {
// we already have a combined string, simply add
this._combined[index] += stringFromCodePoint(codePoint);
} else {
if (content & Content.CODEPOINT_MASK) {
// normal case for combining chars:
// - move current leading char + new one into combined string
// - set combined flag
this._combined[index] = stringFromCodePoint(content & Content.CODEPOINT_MASK) + stringFromCodePoint(codePoint);
content &= ~Content.CODEPOINT_MASK; // set codepoint in buffer to 0
content |= Content.IS_COMBINED_MASK;
} else {
// should not happen - we actually have no data in the cell yet
// simply set the data in the cell buffer with a width of 1
content = codePoint | (1 << Content.WIDTH_SHIFT);
}
this._data[index * CELL_SIZE + Cell.CONTENT] = content;
}
}
public insertCells(pos: number, n: number, fillCellData: ICellData): void {
pos %= this.length;
if (n < this.length - pos) {
const cell = new CellData();
for (let i = this.length - pos - n - 1; i >= 0; --i) {
this.setCell(pos + n + i, this.loadCell(pos + i, cell));
}
for (let i = 0; i < n; ++i) {
this.setCell(pos + i, fillCellData);
}
} else {
for (let i = pos; i < this.length; ++i) {
this.setCell(i, fillCellData);
}
}
}
public deleteCells(pos: number, n: number, fillCellData: ICellData): void {
pos %= this.length;
if (n < this.length - pos) {
const cell = new CellData();
for (let i = 0; i < this.length - pos - n; ++i) {
this.setCell(pos + i, this.loadCell(pos + n + i, cell));
}
for (let i = this.length - n; i < this.length; ++i) {
this.setCell(i, fillCellData);
}
} else {
for (let i = pos; i < this.length; ++i) {
this.setCell(i, fillCellData);
}
}
}
public replaceCells(start: number, end: number, fillCellData: ICellData): void {
while (start < end && start < this.length) {
this.setCell(start++, fillCellData);
}
}
public resize(cols: number, fillCellData: ICellData): void {
if (cols === this.length) {
return;
}
if (cols > this.length) {
const data = new Uint32Array(cols * CELL_SIZE);
if (this.length) {
if (cols * CELL_SIZE < this._data.length) {
data.set(this._data.subarray(0, cols * CELL_SIZE));
} else {
data.set(this._data);
}
}
this._data = data;
for (let i = this.length; i < cols; ++i) {
this.setCell(i, fillCellData);
}
} else {
if (cols) {
const data = new Uint32Array(cols * CELL_SIZE);
data.set(this._data.subarray(0, cols * CELL_SIZE));
this._data = data;
// Remove any cut off combined data
const keys = Object.keys(this._combined);
for (let i = 0; i < keys.length; i++) {
const key = parseInt(keys[i], 10);
if (key >= cols) {
delete this._combined[key];
}
}
} else {
this._data = new Uint32Array(0);
this._combined = {};
}
}
this.length = cols;
}
/** fill a line with fillCharData */
public fill(fillCellData: ICellData): void {
this._combined = {};
for (let i = 0; i < this.length; ++i) {
this.setCell(i, fillCellData);
}
}
/** alter to a full copy of line */
public copyFrom(line: BufferLine): void {
if (this.length !== line.length) {
this._data = new Uint32Array(line._data);
} else {
// use high speed copy if lengths are equal
this._data.set(line._data);
}
this.length = line.length;
this._combined = {};
for (const el in line._combined) {
this._combined[el] = line._combined[el];
}
this.isWrapped = line.isWrapped;
}
/** create a new clone */
public clone(): IBufferLine {
const newLine = new BufferLine(0);
newLine._data = new Uint32Array(this._data);
newLine.length = this.length;
for (const el in this._combined) {
newLine._combined[el] = this._combined[el];
}
newLine.isWrapped = this.isWrapped;
return newLine;
}
public getTrimmedLength(): number {
for (let i = this.length - 1; i >= 0; --i) {
if ((this._data[i * CELL_SIZE + Cell.CONTENT] & Content.HAS_CONTENT_MASK)) {
return i + (this._data[i * CELL_SIZE + Cell.CONTENT] >> Content.WIDTH_SHIFT);
}
}
return 0;
}
public copyCellsFrom(src: BufferLine, srcCol: number, destCol: number, length: number, applyInReverse: boolean): void {
const srcData = src._data;
if (applyInReverse) {
for (let cell = length - 1; cell >= 0; cell--) {
for (let i = 0; i < CELL_SIZE; i++) {
this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i];
}
}
} else {
for (let cell = 0; cell < length; cell++) {
for (let i = 0; i < CELL_SIZE; i++) {
this._data[(destCol + cell) * CELL_SIZE + i] = srcData[(srcCol + cell) * CELL_SIZE + i];
}
}
}
// Move any combined data over as needed
const srcCombinedKeys = Object.keys(src._combined);
for (let i = 0; i < srcCombinedKeys.length; i++) {
const key = parseInt(srcCombinedKeys[i], 10);
if (key >= srcCol) {
this._combined[key - srcCol + destCol] = src._combined[key];
}
}
}
public translateToString(trimRight: boolean = false, startCol: number = 0, endCol: number = this.length): string {
if (trimRight) {
endCol = Math.min(endCol, this.getTrimmedLength());
}
let result = '';
while (startCol < endCol) {
const content = this._data[startCol * CELL_SIZE + Cell.CONTENT];
const cp = content & Content.CODEPOINT_MASK;
result += (content & Content.IS_COMBINED_MASK) ? this._combined[startCol] : (cp) ? stringFromCodePoint(cp) : WHITESPACE_CELL_CHAR;
startCol += (content >> Content.WIDTH_SHIFT) || 1; // always advance by 1
}
return result;
}
}
+92
View File
@@ -0,0 +1,92 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { BufferLine, NULL_CELL_CHAR, NULL_CELL_WIDTH, NULL_CELL_CODE } from 'common/buffer/BufferLine';
import { reflowSmallerGetNewLineLengths } from 'common/buffer/BufferReflow';
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, [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: 汉, 语');
});
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, [0, '汉', 2, '汉'.charCodeAt(0)]);
line.set(i + 2, [0, '语', 2, '语'.charCodeAt(0)]);
}
for (let i = 1; i < 12; i += 2) {
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: 汉语汉语汉, 语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 10), [10, 2], 'line: 汉语汉语汉, 语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 9), [8, 4], 'line: 汉语汉语, 汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 8), [8, 4], 'line: 汉语汉语, 汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 7), [6, 6], 'line: 汉语汉, 语汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 6), [6, 6], 'line: 汉语汉, 语汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 5), [4, 4, 4], 'line: 汉语, 汉语, 汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 4), [4, 4, 4], 'line: 汉语, 汉语, 汉语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 3), [2, 2, 2, 2, 2, 2], 'line: 汉, 语, 汉, 语, 汉, 语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 12, 2), [2, 2, 2, 2, 2, 2], 'line: 汉, 语, 汉, 语, 汉, 语');
});
it('should return correct line lengths for a string with wide and single characters', () => {
const line = new BufferLine(6);
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');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 3), [3, 3], 'line: a汉, 语b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 6, 2), [1, 2, 2, 1], 'line: a, 汉, 语, b');
});
it('should return correct line lengths for a wrapped line with wide and single characters', () => {
const line1 = new BufferLine(6);
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, [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');
assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 4), [3, 4, 4, 1], 'lines: a汉, 语ba, 汉语, b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 3), [3, 3, 3, 3], 'lines: a汉, 语b, a汉, 语b');
assert.deepEqual(reflowSmallerGetNewLineLengths([line1, line2], 6, 2), [1, 2, 2, 2, 2, 2, 1], 'lines: a, 汉, 语, ba, 汉, 语, b');
});
it('should work on lines ending in null space', () => {
const line = new BufferLine(5);
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: 汉, 语');
assert.deepEqual(reflowSmallerGetNewLineLengths([line], 4, 2), [2, 2], 'line: 汉, 语');
});
});
});
+220
View File
@@ -0,0 +1,220 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { BufferLine } from 'common/buffer/BufferLine';
import { CircularList } from 'common/CircularList';
import { IBufferLine, ICellData } from 'common/Types';
export interface INewLayoutResult {
layout: number[];
countRemoved: number;
}
/**
* Evaluates and returns indexes to be removed after a reflow larger occurs. Lines will be removed
* when a wrapped line unwraps.
* @param lines The buffer lines.
* @param newCols The columns after resize.
*/
export function reflowLargerGetLinesToRemove(lines: CircularList<IBufferLine>, oldCols: number, newCols: number, bufferAbsoluteY: number, nullCell: ICellData): number[] {
// Gather all BufferLines that need to be removed from the Buffer here so that they can be
// batched up and only committed once
const toRemove: number[] = [];
for (let y = 0; y < lines.length - 1; y++) {
// Check if this row is wrapped
let i = y;
let nextLine = lines.get(++i) as BufferLine;
if (!nextLine.isWrapped) {
continue;
}
// Check how many lines it's wrapped for
const wrappedLines: BufferLine[] = [lines.get(y) as BufferLine];
while (i < lines.length && nextLine.isWrapped) {
wrappedLines.push(nextLine);
nextLine = lines.get(++i) as BufferLine;
}
// If these lines contain the cursor don't touch them, the program will handle fixing up wrapped
// lines with the cursor
if (bufferAbsoluteY >= y && bufferAbsoluteY < i) {
y += wrappedLines.length - 1;
continue;
}
// Copy buffer data to new locations
let destLineIndex = 0;
let destCol = getWrappedLineTrimmedLength(wrappedLines, destLineIndex, oldCols);
let srcLineIndex = 1;
let srcCol = 0;
while (srcLineIndex < wrappedLines.length) {
const srcTrimmedTineLength = getWrappedLineTrimmedLength(wrappedLines, srcLineIndex, oldCols);
const srcRemainingCells = srcTrimmedTineLength - srcCol;
const destRemainingCells = newCols - destCol;
const cellsToCopy = Math.min(srcRemainingCells, destRemainingCells);
wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[srcLineIndex], srcCol, destCol, cellsToCopy, false);
destCol += cellsToCopy;
if (destCol === newCols) {
destLineIndex++;
destCol = 0;
}
srcCol += cellsToCopy;
if (srcCol === srcTrimmedTineLength) {
srcLineIndex++;
srcCol = 0;
}
// Make sure the last cell isn't wide, if it is copy it to the current dest
if (destCol === 0 && destLineIndex !== 0) {
if (wrappedLines[destLineIndex - 1].getWidth(newCols - 1) === 2) {
wrappedLines[destLineIndex].copyCellsFrom(wrappedLines[destLineIndex - 1], newCols - 1, destCol++, 1, false);
// Null out the end of the last row
wrappedLines[destLineIndex - 1].setCell(newCols - 1, nullCell);
}
}
}
// Clear out remaining cells or fragments could remain;
wrappedLines[destLineIndex].replaceCells(destCol, newCols, nullCell);
// Work backwards and remove any rows at the end that only contain null cells
let countToRemove = 0;
for (let i = wrappedLines.length - 1; i > 0; i--) {
if (i > destLineIndex || wrappedLines[i].getTrimmedLength() === 0) {
countToRemove++;
} else {
break;
}
}
if (countToRemove > 0) {
toRemove.push(y + wrappedLines.length - countToRemove); // index
toRemove.push(countToRemove);
}
y += wrappedLines.length - 1;
}
return toRemove;
}
/**
* Creates and return the new layout for lines given an array of indexes to be removed.
* @param lines The buffer lines.
* @param toRemove The indexes to remove.
*/
export function reflowLargerCreateNewLayout(lines: CircularList<IBufferLine>, toRemove: number[]): INewLayoutResult {
const layout: number[] = [];
// First iterate through the list and get the actual indexes to use for rows
let nextToRemoveIndex = 0;
let nextToRemoveStart = toRemove[nextToRemoveIndex];
let countRemovedSoFar = 0;
for (let i = 0; i < lines.length; i++) {
if (nextToRemoveStart === i) {
const countToRemove = toRemove[++nextToRemoveIndex];
// Tell markers that there was a deletion
lines.onDeleteEmitter.fire({
index: i - countRemovedSoFar,
amount: countToRemove
});
i += countToRemove - 1;
countRemovedSoFar += countToRemove;
nextToRemoveStart = toRemove[++nextToRemoveIndex];
} else {
layout.push(i);
}
}
return {
layout,
countRemoved: countRemovedSoFar
};
}
/**
* Applies a new layout to the buffer. This essentially does the same as many splice calls but it's
* done all at once in a single iteration through the list since splice is very expensive.
* @param lines The buffer lines.
* @param newLayout The new layout to apply.
*/
export function reflowLargerApplyNewLayout(lines: CircularList<IBufferLine>, newLayout: number[]): void {
// Record original lines so they don't get overridden when we rearrange the list
const newLayoutLines: BufferLine[] = [];
for (let i = 0; i < newLayout.length; i++) {
newLayoutLines.push(lines.get(newLayout[i]) as BufferLine);
}
// Rearrange the list
for (let i = 0; i < newLayoutLines.length; i++) {
lines.set(i, newLayoutLines[i]);
}
lines.length = newLayout.length;
}
/**
* Gets the new line lengths for a given wrapped line. The purpose of this function it to pre-
* compute the wrapping points since wide characters may need to be wrapped onto the following line.
* This function will return an array of numbers of where each line wraps to, the resulting array
* will only contain the values `newCols` (when the line does not end with a wide character) and
* `newCols - 1` (when the line does end with a wide character), except for the last value which
* will contain the remaining items to fill the line.
*
* Calling this with a `newCols` value of `1` will lock up.
*
* @param wrappedLines The wrapped lines to evaluate.
* @param oldCols The columns before resize.
* @param newCols The columns after resize.
*/
export function reflowSmallerGetNewLineLengths(wrappedLines: BufferLine[], oldCols: number, newCols: number): number[] {
const newLineLengths: number[] = [];
const cellsNeeded = wrappedLines.map((l, i) => getWrappedLineTrimmedLength(wrappedLines, i, oldCols)).reduce((p, c) => p + c);
// Use srcCol and srcLine to find the new wrapping point, use that to get the cellsAvailable and
// linesNeeded
let srcCol = 0;
let srcLine = 0;
let cellsAvailable = 0;
while (cellsAvailable < cellsNeeded) {
if (cellsNeeded - cellsAvailable < newCols) {
// Add the final line and exit the loop
newLineLengths.push(cellsNeeded - cellsAvailable);
break;
}
srcCol += newCols;
const oldTrimmedLength = getWrappedLineTrimmedLength(wrappedLines, srcLine, oldCols);
if (srcCol > oldTrimmedLength) {
srcCol -= oldTrimmedLength;
srcLine++;
}
const endsWithWide = wrappedLines[srcLine].getWidth(srcCol - 1) === 2;
if (endsWithWide) {
srcCol--;
}
const lineLength = endsWithWide ? newCols - 1 : newCols;
newLineLengths.push(lineLength);
cellsAvailable += lineLength;
}
return newLineLengths;
}
export function getWrappedLineTrimmedLength(lines: BufferLine[], i: number, cols: number): number {
// If this is the last row in the wrapped line, get the actual trimmed length
if (i === lines.length - 1) {
return lines[i].getTrimmedLength();
}
// Detect whether the following line starts with a wide character and the end of the current line
// is null, if so then we can be pretty sure the null character should be excluded from the line
// length]
const endsInNull = !(lines[i].hasContent(cols - 1)) && lines[i].getWidth(cols - 1) === 1;
const followingLineStartsWithWide = lines[i + 1].getWidth(0) === 2;
if (endsInNull && followingLineStartsWithWide) {
return cols - 1;
}
return cols;
}
+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 'common/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();
}
}
+255
View File
@@ -0,0 +1,255 @@
/**
* Copyright (c) 2016 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ICharset } from 'common/Types';
/**
* The character sets supported by the terminal. These enable several languages
* to be represented within the terminal with only 8-bit encoding. See ISO 2022
* for a discussion on character sets. Only VT100 character sets are supported.
*/
export const CHARSETS: { [key: string]: ICharset | null } = {};
/**
* The default character set, US.
*/
export const DEFAULT_CHARSET: ICharset | null = CHARSETS['B'];
/**
* DEC Special Character and Line Drawing Set.
* Reference: http://vt100.net/docs/vt102-ug/table5-13.html
* A lot of curses apps use this if they see TERM=xterm.
* testing: echo -e '\e(0a\e(B'
* The xterm output sometimes seems to conflict with the
* reference above. xterm seems in line with the reference
* when running vttest however.
* The table below now uses xterm's output from vttest.
*/
CHARSETS['0'] = {
'`': '\u25c6', // '◆'
'a': '\u2592', // '▒'
'b': '\u0009', // '\t'
'c': '\u000c', // '\f'
'd': '\u000d', // '\r'
'e': '\u000a', // '\n'
'f': '\u00b0', // '°'
'g': '\u00b1', // '±'
'h': '\u2424', // '\u2424' (NL)
'i': '\u000b', // '\v'
'j': '\u2518', // '┘'
'k': '\u2510', // '┐'
'l': '\u250c', // '┌'
'm': '\u2514', // '└'
'n': '\u253c', // '┼'
'o': '\u23ba', // '⎺'
'p': '\u23bb', // '⎻'
'q': '\u2500', // '─'
'r': '\u23bc', // '⎼'
's': '\u23bd', // '⎽'
't': '\u251c', // '├'
'u': '\u2524', // '┤'
'v': '\u2534', // '┴'
'w': '\u252c', // '┬'
'x': '\u2502', // '│'
'y': '\u2264', // '≤'
'z': '\u2265', // '≥'
'{': '\u03c0', // 'π'
'|': '\u2260', // '≠'
'}': '\u00a3', // '£'
'~': '\u00b7' // '·'
};
/**
* British character set
* ESC (A
* Reference: http://vt100.net/docs/vt220-rm/table2-5.html
*/
CHARSETS['A'] = {
'#': '£'
};
/**
* United States character set
* ESC (B
*/
CHARSETS['B'] = null;
/**
* Dutch character set
* ESC (4
* Reference: http://vt100.net/docs/vt220-rm/table2-6.html
*/
CHARSETS['4'] = {
'#': '£',
'@': '¾',
'[': 'ij',
'\\': '½',
']': '|',
'{': '¨',
'|': 'f',
'}': '¼',
'~': '´'
};
/**
* Finnish character set
* ESC (C or ESC (5
* Reference: http://vt100.net/docs/vt220-rm/table2-7.html
*/
CHARSETS['C'] =
CHARSETS['5'] = {
'[': 'Ä',
'\\': 'Ö',
']': 'Å',
'^': 'Ü',
'`': 'é',
'{': 'ä',
'|': 'ö',
'}': 'å',
'~': 'ü'
};
/**
* French character set
* ESC (R
* Reference: http://vt100.net/docs/vt220-rm/table2-8.html
*/
CHARSETS['R'] = {
'#': '£',
'@': 'à',
'[': '°',
'\\': 'ç',
']': '§',
'{': 'é',
'|': 'ù',
'}': 'è',
'~': '¨'
};
/**
* French Canadian character set
* ESC (Q
* Reference: http://vt100.net/docs/vt220-rm/table2-9.html
*/
CHARSETS['Q'] = {
'@': 'à',
'[': 'â',
'\\': 'ç',
']': 'ê',
'^': 'î',
'`': 'ô',
'{': 'é',
'|': 'ù',
'}': 'è',
'~': 'û'
};
/**
* German character set
* ESC (K
* Reference: http://vt100.net/docs/vt220-rm/table2-10.html
*/
CHARSETS['K'] = {
'@': '§',
'[': 'Ä',
'\\': 'Ö',
']': 'Ü',
'{': 'ä',
'|': 'ö',
'}': 'ü',
'~': 'ß'
};
/**
* Italian character set
* ESC (Y
* Reference: http://vt100.net/docs/vt220-rm/table2-11.html
*/
CHARSETS['Y'] = {
'#': '£',
'@': '§',
'[': '°',
'\\': 'ç',
']': 'é',
'`': 'ù',
'{': 'à',
'|': 'ò',
'}': 'è',
'~': 'ì'
};
/**
* Norwegian/Danish character set
* ESC (E or ESC (6
* Reference: http://vt100.net/docs/vt220-rm/table2-12.html
*/
CHARSETS['E'] =
CHARSETS['6'] = {
'@': 'Ä',
'[': 'Æ',
'\\': 'Ø',
']': 'Å',
'^': 'Ü',
'`': 'ä',
'{': 'æ',
'|': 'ø',
'}': 'å',
'~': 'ü'
};
/**
* Spanish character set
* ESC (Z
* Reference: http://vt100.net/docs/vt220-rm/table2-13.html
*/
CHARSETS['Z'] = {
'#': '£',
'@': '§',
'[': '¡',
'\\': 'Ñ',
']': '¿',
'{': '°',
'|': 'ñ',
'}': 'ç'
};
/**
* Swedish character set
* ESC (H or ESC (7
* Reference: http://vt100.net/docs/vt220-rm/table2-14.html
*/
CHARSETS['H'] =
CHARSETS['7'] = {
'@': 'É',
'[': 'Ä',
'\\': 'Ö',
']': 'Å',
'^': 'Ü',
'`': 'é',
'{': 'ä',
'|': 'ö',
'}': 'å',
'~': 'ü'
};
/**
* Swiss character set
* ESC (=
* Reference: http://vt100.net/docs/vt220-rm/table2-15.html
*/
CHARSETS['='] = {
'#': 'ù',
'@': 'à',
'[': 'é',
'\\': 'ç',
']': 'ê',
'^': 'î',
'_': 'è',
'`': 'ô',
'{': 'ä',
'|': 'ö',
'}': 'ü',
'~': 'û'
};
+295
View File
@@ -0,0 +1,295 @@
import { assert } from 'chai';
import { evaluateKeyboardEvent } from 'common/input/Keyboard';
import { IKeyboardResult, IKeyboardEvent } from 'common/Types';
/**
* A helper function for testing which allows passing in a partial event and defaults will be filled
* in on it.
*/
function testEvaluateKeyboardEvent(partialEvent: {
altKey?: boolean;
ctrlKey?: boolean;
shiftKey?: boolean;
metaKey?: boolean;
keyCode?: number;
key?: string;
type?: string;
}, partialOptions: {
applicationCursorMode?: boolean;
isMac?: boolean;
macOptionIsMeta?: boolean;
} = {}): IKeyboardResult {
const event: IKeyboardEvent = {
altKey: partialEvent.altKey || false,
ctrlKey: partialEvent.ctrlKey || false,
shiftKey: partialEvent.shiftKey || false,
metaKey: partialEvent.metaKey || false,
keyCode: partialEvent.keyCode !== undefined ? partialEvent.keyCode : 0,
key: partialEvent.key || '',
type: partialEvent.type || ''
};
const options = {
applicationCursorMode: partialOptions.applicationCursorMode || false,
isMac: partialOptions.isMac || false,
macOptionIsMeta: partialOptions.macOptionIsMeta || false
};
return evaluateKeyboardEvent(event, options.applicationCursorMode, options.isMac, options.macOptionIsMeta);
}
describe('Keyboard', () => {
describe('evaluateKeyEscapeSequence', () => {
it('should return the correct escape sequence for unmodified keys', () => {
// Backspace
assert.equal(testEvaluateKeyboardEvent({ keyCode: 8 }).key, '\x7f'); // ^?
// Tab
assert.equal(testEvaluateKeyboardEvent({ keyCode: 9 }).key, '\t');
// Return/enter
assert.equal(testEvaluateKeyboardEvent({ keyCode: 13 }).key, '\r'); // CR
// Escape
assert.equal(testEvaluateKeyboardEvent({ keyCode: 27 }).key, '\x1b');
// Page up, page down
assert.equal(testEvaluateKeyboardEvent({ keyCode: 33 }).key, '\x1b[5~'); // CSI 5 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 34 }).key, '\x1b[6~'); // CSI 6 ~
// End, Home
assert.equal(testEvaluateKeyboardEvent({ keyCode: 35 }).key, '\x1b[F'); // SS3 F
assert.equal(testEvaluateKeyboardEvent({ keyCode: 36 }).key, '\x1b[H'); // SS3 H
// Left, up, right, down arrows
assert.equal(testEvaluateKeyboardEvent({ keyCode: 37 }).key, '\x1b[D'); // CSI D
assert.equal(testEvaluateKeyboardEvent({ keyCode: 38 }).key, '\x1b[A'); // CSI A
assert.equal(testEvaluateKeyboardEvent({ keyCode: 39 }).key, '\x1b[C'); // CSI C
assert.equal(testEvaluateKeyboardEvent({ keyCode: 40 }).key, '\x1b[B'); // CSI B
// Insert
assert.equal(testEvaluateKeyboardEvent({ keyCode: 45 }).key, '\x1b[2~'); // CSI 2 ~
// Delete
assert.equal(testEvaluateKeyboardEvent({ keyCode: 46 }).key, '\x1b[3~'); // CSI 3 ~
// F1-F12
assert.equal(testEvaluateKeyboardEvent({ keyCode: 112 }).key, '\x1bOP'); // SS3 P
assert.equal(testEvaluateKeyboardEvent({ keyCode: 113 }).key, '\x1bOQ'); // SS3 Q
assert.equal(testEvaluateKeyboardEvent({ keyCode: 114 }).key, '\x1bOR'); // SS3 R
assert.equal(testEvaluateKeyboardEvent({ keyCode: 115 }).key, '\x1bOS'); // SS3 S
assert.equal(testEvaluateKeyboardEvent({ keyCode: 116 }).key, '\x1b[15~'); // CSI 1 5 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 117 }).key, '\x1b[17~'); // CSI 1 7 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 118 }).key, '\x1b[18~'); // CSI 1 8 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 119 }).key, '\x1b[19~'); // CSI 1 9 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 120 }).key, '\x1b[20~'); // CSI 2 0 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 121 }).key, '\x1b[21~'); // CSI 2 1 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 122 }).key, '\x1b[23~'); // CSI 2 3 ~
assert.equal(testEvaluateKeyboardEvent({ keyCode: 123 }).key, '\x1b[24~'); // CSI 2 4 ~
});
it('should return \\x1b[3;5~ for ctrl+delete', () => {
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 46 }).key, '\x1b[3;5~');
});
it('should return \\x1b[3;2~ for shift+delete', () => {
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 46 }).key, '\x1b[3;2~');
});
it('should return \\x1b[3;3~ for alt+delete', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 46 }).key, '\x1b[3;3~');
});
it('should return \\x1b[5D for ctrl+left', () => {
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 37 }).key, '\x1b[1;5D'); // CSI 5 D
});
it('should return \\x1b[5C for ctrl+right', () => {
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 39 }).key, '\x1b[1;5C'); // CSI 5 C
});
it('should return \\x1b[5A for ctrl+up', () => {
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
});
it('should return \\x1b[5B for ctrl+down', () => {
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
});
describe('On non-macOS platforms', () => {
// Evalueate alt + arrow key movement, which is a feature of terminal emulators but not VT100
// http://unix.stackexchange.com/a/108106
it('should return \\x1b[5D for alt+left', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 37 }, { isMac: false }).key, '\x1b[1;5D'); // CSI 5 D
});
it('should return \\x1b[5C for alt+right', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: false }).key, '\x1b[1;5C'); // CSI 5 C
});
it('should return \\x1ba for alt+a', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: false }).key, '\x1ba');
});
});
describe('On macOS platforms', () => {
it('should return \\x1bb for alt+left', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 37 }, { isMac: true }).key, '\x1bb'); // CSI 5 D
});
it('should return \\x1bf for alt+right', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 39 }, { isMac: true }).key, '\x1bf'); // CSI 5 C
});
it('should return undefined for alt+a', () => {
assert.strictEqual(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true }).key, undefined), { isMac: true };
});
});
describe('with macOptionIsMeta', () => {
it('should return \\x1ba for alt+a', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 65 }, { isMac: true, macOptionIsMeta: true }).key, '\x1ba');
});
});
it('should return \\x1b[5A for alt+up', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 38 }).key, '\x1b[1;5A'); // CSI 5 A
});
it('should return \\x1b[5B for alt+down', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 40 }).key, '\x1b[1;5B'); // CSI 5 B
});
it('should return the correct escape sequence for modified F1-F12 keys', () => {
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 112 }).key, '\x1b[1;2P');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 113 }).key, '\x1b[1;2Q');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 114 }).key, '\x1b[1;2R');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 115 }).key, '\x1b[1;2S');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 116 }).key, '\x1b[15;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 117 }).key, '\x1b[17;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 118 }).key, '\x1b[18;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 119 }).key, '\x1b[19;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 120 }).key, '\x1b[20;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 121 }).key, '\x1b[21;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 122 }).key, '\x1b[23;2~');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 123 }).key, '\x1b[24;2~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 112 }).key, '\x1b[1;3P');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 113 }).key, '\x1b[1;3Q');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 114 }).key, '\x1b[1;3R');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 115 }).key, '\x1b[1;3S');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 116 }).key, '\x1b[15;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 117 }).key, '\x1b[17;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 118 }).key, '\x1b[18;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 119 }).key, '\x1b[19;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 120 }).key, '\x1b[20;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 121 }).key, '\x1b[21;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 122 }).key, '\x1b[23;3~');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, keyCode: 123 }).key, '\x1b[24;3~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 112 }).key, '\x1b[1;5P');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 113 }).key, '\x1b[1;5Q');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 114 }).key, '\x1b[1;5R');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 115 }).key, '\x1b[1;5S');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 116 }).key, '\x1b[15;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 117 }).key, '\x1b[17;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 118 }).key, '\x1b[18;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 119 }).key, '\x1b[19;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 120 }).key, '\x1b[20;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 121 }).key, '\x1b[21;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 122 }).key, '\x1b[23;5~');
assert.equal(testEvaluateKeyboardEvent({ ctrlKey: true, keyCode: 123 }).key, '\x1b[24;5~');
});
// Characters using ctrl+alt sequences
it('should return proper sequence for ctrl+alt+a', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, ctrlKey: true, keyCode: 65 }).key, '\x1b\x01');
});
// Characters using alt sequences (numbers)
it('should return proper sequences for alt+0', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 48 }).key, '\x1b0');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 48 }).key, '\x1b)');
});
it('should return proper sequences for alt+1', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 49 }).key, '\x1b1');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 49 }).key, '\x1b!');
});
it('should return proper sequences for alt+2', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 50 }).key, '\x1b2');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 50 }).key, '\x1b@');
});
it('should return proper sequences for alt+3', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 51 }).key, '\x1b3');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 51 }).key, '\x1b#');
});
it('should return proper sequences for alt+4', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 52 }).key, '\x1b4');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 52 }).key, '\x1b$');
});
it('should return proper sequences for alt+5', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 53 }).key, '\x1b5');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 53 }).key, '\x1b%');
});
it('should return proper sequences for alt+6', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 54 }).key, '\x1b6');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 54 }).key, '\x1b^');
});
it('should return proper sequences for alt+7', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 55 }).key, '\x1b7');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 55 }).key, '\x1b&');
});
it('should return proper sequences for alt+8', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 56 }).key, '\x1b8');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 56 }).key, '\x1b*');
});
it('should return proper sequences for alt+9', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 57 }).key, '\x1b9');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 57 }).key, '\x1b(');
});
// Characters using alt sequences (special chars)
it('should return proper sequences for alt+;', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 186 }).key, '\x1b;');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 186 }).key, '\x1b:');
});
it('should return proper sequences for alt+=', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 187 }).key, '\x1b=');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 187 }).key, '\x1b+');
});
it('should return proper sequences for alt+,', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 188 }).key, '\x1b,');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 188 }).key, '\x1b<');
});
it('should return proper sequences for alt+-', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 189 }).key, '\x1b-');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 189 }).key, '\x1b_');
});
it('should return proper sequences for alt+.', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 190 }).key, '\x1b.');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 190 }).key, '\x1b>');
});
it('should return proper sequences for alt+/', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 191 }).key, '\x1b/');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 191 }).key, '\x1b?');
});
it('should return proper sequences for alt+~', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 192 }).key, '\x1b`');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 192 }).key, '\x1b~');
});
it('should return proper sequences for alt+[', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 219 }).key, '\x1b[');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 219 }).key, '\x1b{');
});
it('should return proper sequences for alt+\\', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 220 }).key, '\x1b\\');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 220 }).key, '\x1b|');
});
it('should return proper sequences for alt+]', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 221 }).key, '\x1b]');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 221 }).key, '\x1b}');
});
it('should return proper sequences for alt+\'', () => {
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: false, keyCode: 222 }).key, '\x1b\'');
assert.equal(testEvaluateKeyboardEvent({ altKey: true, shiftKey: true, keyCode: 222 }).key, '\x1b"');
});
it('should handle mobile arrow events', () => {
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputUpArrow' }).key, '\x1b[A');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputUpArrow' }, { applicationCursorMode: true }).key, '\x1bOA');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputLeftArrow' }).key, '\x1b[D');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputLeftArrow' }, { applicationCursorMode: true }).key, '\x1bOD');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputRightArrow' }).key, '\x1b[C');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputRightArrow' }, { applicationCursorMode: true }).key, '\x1bOC');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }).key, '\x1b[B');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }, { applicationCursorMode: true }).key, '\x1bOB');
});
it('should handle lowercase letters', () => {
assert.equal(testEvaluateKeyboardEvent({ keyCode: 65, key: 'a' }).key, 'a');
assert.equal(testEvaluateKeyboardEvent({ keyCode: 189, key: '-' }).key, '-');
});
it('should handle uppercase letters', () => {
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 65, key: 'A' }).key, 'A');
assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 49, key: '!' }).key, '!');
});
});
});
+363
View File
@@ -0,0 +1,363 @@
/**
* Copyright (c) 2014 The xterm.js authors. All rights reserved.
* Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
* @license MIT
*/
import { IKeyboardEvent, IKeyboardResult, KeyboardResultType } from 'common/Types';
import { C0 } from 'common/data/EscapeSequences';
// reg + shift key mappings for digits and special chars
const KEYCODE_KEY_MAPPINGS: { [key: number]: [string, string]} = {
// digits 0-9
48: ['0', ')'],
49: ['1', '!'],
50: ['2', '@'],
51: ['3', '#'],
52: ['4', '$'],
53: ['5', '%'],
54: ['6', '^'],
55: ['7', '&'],
56: ['8', '*'],
57: ['9', '('],
// special chars
186: [';', ':'],
187: ['=', '+'],
188: [',', '<'],
189: ['-', '_'],
190: ['.', '>'],
191: ['/', '?'],
192: ['`', '~'],
219: ['[', '{'],
220: ['\\', '|'],
221: [']', '}'],
222: ['\'', '"']
};
export function evaluateKeyboardEvent(
ev: IKeyboardEvent,
applicationCursorMode: boolean,
isMac: boolean,
macOptionIsMeta: boolean
): IKeyboardResult {
const result: IKeyboardResult = {
type: KeyboardResultType.SEND_KEY,
// Whether to cancel event propagation (NOTE: this may not be needed since the event is
// canceled at the end of keyDown
cancel: false,
// The new key even to emit
key: undefined
};
const modifiers = (ev.shiftKey ? 1 : 0) | (ev.altKey ? 2 : 0) | (ev.ctrlKey ? 4 : 0) | (ev.metaKey ? 8 : 0);
switch (ev.keyCode) {
case 0:
if (ev.key === 'UIKeyInputUpArrow') {
if (applicationCursorMode) {
result.key = C0.ESC + 'OA';
} else {
result.key = C0.ESC + '[A';
}
}
else if (ev.key === 'UIKeyInputLeftArrow') {
if (applicationCursorMode) {
result.key = C0.ESC + 'OD';
} else {
result.key = C0.ESC + '[D';
}
}
else if (ev.key === 'UIKeyInputRightArrow') {
if (applicationCursorMode) {
result.key = C0.ESC + 'OC';
} else {
result.key = C0.ESC + '[C';
}
}
else if (ev.key === 'UIKeyInputDownArrow') {
if (applicationCursorMode) {
result.key = C0.ESC + 'OB';
} else {
result.key = C0.ESC + '[B';
}
}
break;
case 8:
// backspace
if (ev.shiftKey) {
result.key = C0.BS; // ^H
break;
} else if (ev.altKey) {
result.key = C0.ESC + C0.DEL; // \e ^?
break;
}
result.key = C0.DEL; // ^?
break;
case 9:
// tab
if (ev.shiftKey) {
result.key = C0.ESC + '[Z';
break;
}
result.key = C0.HT;
result.cancel = true;
break;
case 13:
// return/enter
result.key = C0.CR;
result.cancel = true;
break;
case 27:
// escape
result.key = C0.ESC;
result.cancel = true;
break;
case 37:
// left-arrow
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'D';
// HACK: Make Alt + left-arrow behave like Ctrl + left-arrow: move one word backwards
// http://unix.stackexchange.com/a/108106
// macOS uses different escape sequences than linux
if (result.key === C0.ESC + '[1;3D') {
result.key = isMac ? C0.ESC + 'b' : C0.ESC + '[1;5D';
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OD';
} else {
result.key = C0.ESC + '[D';
}
break;
case 39:
// right-arrow
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'C';
// HACK: Make Alt + right-arrow behave like Ctrl + right-arrow: move one word forward
// http://unix.stackexchange.com/a/108106
// macOS uses different escape sequences than linux
if (result.key === C0.ESC + '[1;3C') {
result.key = isMac ? C0.ESC + 'f' : C0.ESC + '[1;5C';
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OC';
} else {
result.key = C0.ESC + '[C';
}
break;
case 38:
// up-arrow
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'A';
// HACK: Make Alt + up-arrow behave like Ctrl + up-arrow
// http://unix.stackexchange.com/a/108106
if (result.key === C0.ESC + '[1;3A') {
result.key = C0.ESC + '[1;5A';
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OA';
} else {
result.key = C0.ESC + '[A';
}
break;
case 40:
// down-arrow
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'B';
// HACK: Make Alt + down-arrow behave like Ctrl + down-arrow
// http://unix.stackexchange.com/a/108106
if (result.key === C0.ESC + '[1;3B') {
result.key = C0.ESC + '[1;5B';
}
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OB';
} else {
result.key = C0.ESC + '[B';
}
break;
case 45:
// insert
if (!ev.shiftKey && !ev.ctrlKey) {
// <Ctrl> or <Shift> + <Insert> are used to
// copy-paste on some systems.
result.key = C0.ESC + '[2~';
}
break;
case 46:
// delete
if (modifiers) {
result.key = C0.ESC + '[3;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[3~';
}
break;
case 36:
// home
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'H';
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OH';
} else {
result.key = C0.ESC + '[H';
}
break;
case 35:
// end
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'F';
} else if (applicationCursorMode) {
result.key = C0.ESC + 'OF';
} else {
result.key = C0.ESC + '[F';
}
break;
case 33:
// page up
if (ev.shiftKey) {
result.type = KeyboardResultType.PAGE_UP;
} else {
result.key = C0.ESC + '[5~';
}
break;
case 34:
// page down
if (ev.shiftKey) {
result.type = KeyboardResultType.PAGE_DOWN;
} else {
result.key = C0.ESC + '[6~';
}
break;
case 112:
// F1-F12
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'P';
} else {
result.key = C0.ESC + 'OP';
}
break;
case 113:
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'Q';
} else {
result.key = C0.ESC + 'OQ';
}
break;
case 114:
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'R';
} else {
result.key = C0.ESC + 'OR';
}
break;
case 115:
if (modifiers) {
result.key = C0.ESC + '[1;' + (modifiers + 1) + 'S';
} else {
result.key = C0.ESC + 'OS';
}
break;
case 116:
if (modifiers) {
result.key = C0.ESC + '[15;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[15~';
}
break;
case 117:
if (modifiers) {
result.key = C0.ESC + '[17;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[17~';
}
break;
case 118:
if (modifiers) {
result.key = C0.ESC + '[18;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[18~';
}
break;
case 119:
if (modifiers) {
result.key = C0.ESC + '[19;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[19~';
}
break;
case 120:
if (modifiers) {
result.key = C0.ESC + '[20;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[20~';
}
break;
case 121:
if (modifiers) {
result.key = C0.ESC + '[21;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[21~';
}
break;
case 122:
if (modifiers) {
result.key = C0.ESC + '[23;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[23~';
}
break;
case 123:
if (modifiers) {
result.key = C0.ESC + '[24;' + (modifiers + 1) + '~';
} else {
result.key = C0.ESC + '[24~';
}
break;
default:
// a-z and space
if (ev.ctrlKey && !ev.shiftKey && !ev.altKey && !ev.metaKey) {
if (ev.keyCode >= 65 && ev.keyCode <= 90) {
result.key = String.fromCharCode(ev.keyCode - 64);
} else if (ev.keyCode === 32) {
// NUL
result.key = String.fromCharCode(0);
} else if (ev.keyCode >= 51 && ev.keyCode <= 55) {
// escape, file sep, group sep, record sep, unit sep
result.key = String.fromCharCode(ev.keyCode - 51 + 27);
} else if (ev.keyCode === 56) {
// delete
result.key = String.fromCharCode(127);
} else if (ev.keyCode === 219) {
// ^[ - Control Sequence Introducer (CSI)
result.key = String.fromCharCode(27);
} else if (ev.keyCode === 220) {
// ^\ - String Terminator (ST)
result.key = String.fromCharCode(28);
} else if (ev.keyCode === 221) {
// ^] - Operating System Command (OSC)
result.key = String.fromCharCode(29);
}
} else if ((!isMac || macOptionIsMeta) && ev.altKey && !ev.metaKey) {
// On macOS this is a third level shift when !macOptionIsMeta. Use <Esc> instead.
const keyMapping = KEYCODE_KEY_MAPPINGS[ev.keyCode];
const key = keyMapping && keyMapping[!ev.shiftKey ? 0 : 1];
if (key) {
result.key = C0.ESC + key;
} else if (ev.keyCode >= 65 && ev.keyCode <= 90) {
const keyCode = ev.ctrlKey ? ev.keyCode - 64 : ev.keyCode + 32;
result.key = C0.ESC + String.fromCharCode(keyCode);
}
} else if (isMac && !ev.altKey && !ev.ctrlKey && ev.metaKey) {
if (ev.keyCode === 65) { // cmd + a
result.type = KeyboardResultType.SELECT_ALL;
}
} 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;
}
return result;
}
+219
View File
@@ -0,0 +1,219 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { assert } from 'chai';
import { StringToUtf32, stringFromCodePoint, Utf8ToUtf32, utf32ToString } from 'common/input/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', () => {
const s = 'abcdefg';
const data = new Uint32Array(s.length);
for (let i = 0; i < s.length; ++i) {
data[i] = s.charCodeAt(i);
assert.equal(stringFromCodePoint(data[i]), s[i]);
}
assert.equal(utf32ToString(data), s);
});
describe('StringToUtf32 decoder', () => {
describe('full codepoint test', () => {
it('0..65535', () => {
const decoder = new StringToUtf32();
const target = new Uint32Array(5);
for (let i = 0; i < 65536; ++i) {
// skip surrogate pairs
if (i >= 0xD800 && i <= 0xDFFF) {
continue;
}
const length = decoder.decode(String.fromCharCode(i), target);
assert.equal(length, 1);
assert.equal(target[0], i);
assert.equal(utf32ToString(target, 0, length), String.fromCharCode(i));
decoder.clear();
}
});
it('65536..0x10FFFF (surrogates)', function (): void {
this.timeout(20000);
const decoder = new StringToUtf32();
const target = new Uint32Array(5);
for (let i = 65536; i < 0x10FFFF; ++i) {
const codePoint = i - 0x10000;
const s = String.fromCharCode((codePoint >> 10) + 0xD800) + String.fromCharCode((codePoint % 0x400) + 0xDC00);
const length = decoder.decode(s, target);
assert.equal(length, 1);
assert.equal(target[0], i);
assert.equal(utf32ToString(target, 0, length), s);
decoder.clear();
}
});
});
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();
const target = new Uint32Array(5);
const input = 'Ä€𝄞Ö𝄞€Ü𝄞€';
let decoded = '';
for (let i = 0; i < input.length; ++i) {
const written = decoder.decode(input[i], target);
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, 'Ä€𝄞Ö𝄞€Ü𝄞€');
});
});
});
});
+342
View File
@@ -0,0 +1,342 @@
/**
* Copyright (c) 2019 The xterm.js authors. All rights reserved.
* @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.
*/
export class StringToUtf32 {
private _interim: number = 0;
/**
* Clears interim and resets decoder to clean state.
*/
public clear(): void {
this._interim = 0;
}
/**
* Decode JS string to UTF32 codepoints.
* The methods assumes stream input and will store partly transmitted
* surrogate pairs and decode them with the next data chunk.
* Note: The method does no bound checks for target, therefore make sure
* the provided input data does not exceed the size of `target`.
* Returns the number of written codepoints in `target`.
*/
decode(input: string, target: Uint32Array): number {
const length = input.length;
if (!length) {
return 0;
}
let size = 0;
let startPos = 0;
// handle leftover surrogate high
if (this._interim) {
const second = input.charCodeAt(startPos++);
if (0xDC00 <= second && second <= 0xDFFF) {
target[size++] = (this._interim - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
} else {
// illegal codepoint (USC2 handling)
target[size++] = this._interim;
target[size++] = second;
}
this._interim = 0;
}
for (let i = startPos; i < length; ++i) {
const code = input.charCodeAt(i);
// surrogate pair first
if (0xD800 <= code && code <= 0xDBFF) {
if (++i >= length) {
this._interim = code;
return size;
}
const second = input.charCodeAt(i);
if (0xDC00 <= second && second <= 0xDFFF) {
target[size++] = (code - 0xD800) * 0x400 + second - 0xDC00 + 0x10000;
} else {
// illegal codepoint (USC2 handling)
target[size++] = code;
target[size++] = second;
}
continue;
}
target[size++] = code;
}
return size;
}
}
/**
* Utf8Decoder - decodes UTF8 byte sequences into UTF32 codepoints.
*/
export class Utf8ToUtf32 {
public interim: Uint8Array = new Uint8Array(3);
/**
* 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;
}
}
File diff suppressed because it is too large Load Diff
+610
View File
@@ -0,0 +1,610 @@
/**
* Copyright (c) 2018 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { ParserState, ParserAction, IParsingState, IDcsHandler, IEscapeSequenceParser } from 'common/parser/Types';
import { Disposable } from 'common/Lifecycle';
import { utf32ToString } from 'common/input/TextDecoder';
import { IDisposable } from 'common/Types';
import { fill } from 'common/TypedArrayUtils';
interface IHandlerCollection<T> {
[key: string]: T[];
}
type CsiHandler = (params: number[], collect: string) => boolean | void;
type OscHandler = (data: string) => boolean | void;
/**
* Table values are generated like this:
* index: currentState << TableValue.INDEX_STATE_SHIFT | charCode
* value: action << TableValue.TRANSITION_ACTION_SHIFT | nextState
*/
const enum TableAccess {
TRANSITION_ACTION_SHIFT = 4,
TRANSITION_STATE_MASK = 15,
INDEX_STATE_SHIFT = 8
}
/**
* Transition table for EscapeSequenceParser.
*/
export class TransitionTable {
public table: Uint8Array;
constructor(length: number) {
this.table = new Uint8Array(length);
}
/**
* Set default transition.
* @param action default action
* @param next default next state
*/
public setDefault(action: ParserAction, next: ParserState): void {
fill(this.table, action << TableAccess.TRANSITION_ACTION_SHIFT | next);
}
/**
* Add a transition to the transition table.
* @param code input character code
* @param state current parser state
* @param action parser action to be done
* @param next next parser state
*/
public add(code: number, state: ParserState, action: ParserAction, next: ParserState): void {
this.table[state << TableAccess.INDEX_STATE_SHIFT | code] = action << TableAccess.TRANSITION_ACTION_SHIFT | next;
}
/**
* Add transitions for multiple input character codes.
* @param codes input character code array
* @param state current parser state
* @param action parser action to be done
* @param next next parser state
*/
public addMany(codes: number[], state: ParserState, action: ParserAction, next: ParserState): void {
for (let i = 0; i < codes.length; i++) {
this.table[state << TableAccess.INDEX_STATE_SHIFT | codes[i]] = action << TableAccess.TRANSITION_ACTION_SHIFT | next;
}
}
}
// Pseudo-character placeholder for printable non-ascii characters (unicode).
const NON_ASCII_PRINTABLE = 0xA0;
/**
* VT500 compatible transition table.
* Taken from https://vt100.net/emu/dec_ansi_parser.
*/
export const VT500_TRANSITION_TABLE = (function (): TransitionTable {
const table: TransitionTable = new TransitionTable(4095);
// range macro for byte
const BYTE_VALUES = 256;
const blueprint = Array.apply(null, Array(BYTE_VALUES)).map((unused: any, i: number) => i);
const r = (start: number, end: number) => blueprint.slice(start, end);
// Default definitions.
const PRINTABLES = r(0x20, 0x7f); // 0x20 (SP) included, 0x7F (DEL) excluded
const EXECUTABLES = r(0x00, 0x18);
EXECUTABLES.push(0x19);
EXECUTABLES.push.apply(EXECUTABLES, r(0x1c, 0x20));
const states: number[] = r(ParserState.GROUND, ParserState.DCS_PASSTHROUGH + 1);
let state: any;
// set default transition
table.setDefault(ParserAction.ERROR, ParserState.GROUND);
// printables
table.addMany(PRINTABLES, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);
// global anywhere rules
for (state in states) {
table.addMany([0x18, 0x1a, 0x99, 0x9a], state, ParserAction.EXECUTE, ParserState.GROUND);
table.addMany(r(0x80, 0x90), state, ParserAction.EXECUTE, ParserState.GROUND);
table.addMany(r(0x90, 0x98), state, ParserAction.EXECUTE, ParserState.GROUND);
table.add(0x9c, state, ParserAction.IGNORE, ParserState.GROUND); // ST as terminator
table.add(0x1b, state, ParserAction.CLEAR, ParserState.ESCAPE); // ESC
table.add(0x9d, state, ParserAction.OSC_START, ParserState.OSC_STRING); // OSC
table.addMany([0x98, 0x9e, 0x9f], state, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
table.add(0x9b, state, ParserAction.CLEAR, ParserState.CSI_ENTRY); // CSI
table.add(0x90, state, ParserAction.CLEAR, ParserState.DCS_ENTRY); // DCS
}
// rules for executables and 7f
table.addMany(EXECUTABLES, ParserState.GROUND, ParserAction.EXECUTE, ParserState.GROUND);
table.addMany(EXECUTABLES, ParserState.ESCAPE, ParserAction.EXECUTE, ParserState.ESCAPE);
table.add(0x7f, ParserState.ESCAPE, ParserAction.IGNORE, ParserState.ESCAPE);
table.addMany(EXECUTABLES, ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);
table.addMany(EXECUTABLES, ParserState.CSI_ENTRY, ParserAction.EXECUTE, ParserState.CSI_ENTRY);
table.add(0x7f, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_ENTRY);
table.addMany(EXECUTABLES, ParserState.CSI_PARAM, ParserAction.EXECUTE, ParserState.CSI_PARAM);
table.add(0x7f, ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_PARAM);
table.addMany(EXECUTABLES, ParserState.CSI_IGNORE, ParserAction.EXECUTE, ParserState.CSI_IGNORE);
table.addMany(EXECUTABLES, ParserState.CSI_INTERMEDIATE, ParserAction.EXECUTE, ParserState.CSI_INTERMEDIATE);
table.add(0x7f, ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_INTERMEDIATE);
table.addMany(EXECUTABLES, ParserState.ESCAPE_INTERMEDIATE, ParserAction.EXECUTE, ParserState.ESCAPE_INTERMEDIATE);
table.add(0x7f, ParserState.ESCAPE_INTERMEDIATE, ParserAction.IGNORE, ParserState.ESCAPE_INTERMEDIATE);
// osc
table.add(0x5d, ParserState.ESCAPE, ParserAction.OSC_START, ParserState.OSC_STRING);
table.addMany(PRINTABLES, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
table.add(0x7f, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
table.addMany([0x9c, 0x1b, 0x18, 0x1a, 0x07], ParserState.OSC_STRING, ParserAction.OSC_END, ParserState.GROUND);
table.addMany(r(0x1c, 0x20), ParserState.OSC_STRING, ParserAction.IGNORE, ParserState.OSC_STRING);
// sos/pm/apc does nothing
table.addMany([0x58, 0x5e, 0x5f], ParserState.ESCAPE, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
table.addMany(PRINTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
table.addMany(EXECUTABLES, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
table.add(0x9c, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.GROUND);
table.add(0x7f, ParserState.SOS_PM_APC_STRING, ParserAction.IGNORE, ParserState.SOS_PM_APC_STRING);
// csi entries
table.add(0x5b, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.CSI_ENTRY);
table.addMany(r(0x40, 0x7f), ParserState.CSI_ENTRY, ParserAction.CSI_DISPATCH, ParserState.GROUND);
table.addMany(r(0x30, 0x3a), ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM);
table.add(0x3b, ParserState.CSI_ENTRY, ParserAction.PARAM, ParserState.CSI_PARAM);
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_PARAM);
table.addMany(r(0x30, 0x3a), ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM);
table.add(0x3b, ParserState.CSI_PARAM, ParserAction.PARAM, ParserState.CSI_PARAM);
table.addMany(r(0x40, 0x7f), ParserState.CSI_PARAM, ParserAction.CSI_DISPATCH, ParserState.GROUND);
table.addMany([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], ParserState.CSI_PARAM, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.addMany(r(0x20, 0x40), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.add(0x7f, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.addMany(r(0x40, 0x7f), ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.GROUND);
table.add(0x3a, ParserState.CSI_ENTRY, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.addMany(r(0x20, 0x30), ParserState.CSI_ENTRY, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
table.addMany(r(0x20, 0x30), ParserState.CSI_INTERMEDIATE, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
table.addMany(r(0x30, 0x40), ParserState.CSI_INTERMEDIATE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.addMany(r(0x40, 0x7f), ParserState.CSI_INTERMEDIATE, ParserAction.CSI_DISPATCH, ParserState.GROUND);
table.addMany(r(0x20, 0x30), ParserState.CSI_PARAM, ParserAction.COLLECT, ParserState.CSI_INTERMEDIATE);
// esc_intermediate
table.addMany(r(0x20, 0x30), ParserState.ESCAPE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);
table.addMany(r(0x20, 0x30), ParserState.ESCAPE_INTERMEDIATE, ParserAction.COLLECT, ParserState.ESCAPE_INTERMEDIATE);
table.addMany(r(0x30, 0x7f), ParserState.ESCAPE_INTERMEDIATE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
table.addMany(r(0x30, 0x50), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
table.addMany(r(0x51, 0x58), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
table.addMany([0x59, 0x5a, 0x5c], ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
table.addMany(r(0x60, 0x7f), ParserState.ESCAPE, ParserAction.ESC_DISPATCH, ParserState.GROUND);
// dcs entry
table.add(0x50, ParserState.ESCAPE, ParserAction.CLEAR, ParserState.DCS_ENTRY);
table.addMany(EXECUTABLES, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
table.add(0x7f, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
table.addMany(r(0x1c, 0x20), ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_ENTRY);
table.addMany(r(0x20, 0x30), ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
table.add(0x3a, ParserState.DCS_ENTRY, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x30, 0x3a), ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM);
table.add(0x3b, ParserState.DCS_ENTRY, ParserAction.PARAM, ParserState.DCS_PARAM);
table.addMany([0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_ENTRY, ParserAction.COLLECT, ParserState.DCS_PARAM);
table.addMany(EXECUTABLES, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x20, 0x80), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x1c, 0x20), ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(EXECUTABLES, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
table.add(0x7f, ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
table.addMany(r(0x1c, 0x20), ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_PARAM);
table.addMany(r(0x30, 0x3a), ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM);
table.add(0x3b, ParserState.DCS_PARAM, ParserAction.PARAM, ParserState.DCS_PARAM);
table.addMany([0x3a, 0x3c, 0x3d, 0x3e, 0x3f], ParserState.DCS_PARAM, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x20, 0x30), ParserState.DCS_PARAM, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
table.addMany(EXECUTABLES, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
table.add(0x7f, ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
table.addMany(r(0x1c, 0x20), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_INTERMEDIATE);
table.addMany(r(0x20, 0x30), ParserState.DCS_INTERMEDIATE, ParserAction.COLLECT, ParserState.DCS_INTERMEDIATE);
table.addMany(r(0x30, 0x40), ParserState.DCS_INTERMEDIATE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.addMany(r(0x40, 0x7f), ParserState.DCS_INTERMEDIATE, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);
table.addMany(r(0x40, 0x7f), ParserState.DCS_PARAM, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);
table.addMany(r(0x40, 0x7f), ParserState.DCS_ENTRY, ParserAction.DCS_HOOK, ParserState.DCS_PASSTHROUGH);
table.addMany(EXECUTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
table.addMany(PRINTABLES, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
table.add(0x7f, ParserState.DCS_PASSTHROUGH, ParserAction.IGNORE, ParserState.DCS_PASSTHROUGH);
table.addMany([0x1b, 0x9c], ParserState.DCS_PASSTHROUGH, ParserAction.DCS_UNHOOK, ParserState.GROUND);
// special handling of unicode chars
table.add(NON_ASCII_PRINTABLE, ParserState.GROUND, ParserAction.PRINT, ParserState.GROUND);
table.add(NON_ASCII_PRINTABLE, ParserState.OSC_STRING, ParserAction.OSC_PUT, ParserState.OSC_STRING);
table.add(NON_ASCII_PRINTABLE, ParserState.CSI_IGNORE, ParserAction.IGNORE, ParserState.CSI_IGNORE);
table.add(NON_ASCII_PRINTABLE, ParserState.DCS_IGNORE, ParserAction.IGNORE, ParserState.DCS_IGNORE);
table.add(NON_ASCII_PRINTABLE, ParserState.DCS_PASSTHROUGH, ParserAction.DCS_PUT, ParserState.DCS_PASSTHROUGH);
return table;
})();
/**
* Dummy DCS handler as default fallback.
*/
class DcsDummy implements IDcsHandler {
hook(collect: string, params: number[], flag: number): void { }
put(data: Uint32Array, start: number, end: number): void { }
unhook(): void { }
}
/**
* EscapeSequenceParser.
* This class implements the ANSI/DEC compatible parser described by
* Paul Williams (https://vt100.net/emu/dec_ansi_parser).
* To implement custom ANSI compliant escape sequences it is not needed to
* alter this parser, instead consider registering a custom handler.
* For non ANSI compliant sequences change the transition table with
* the optional `transitions` contructor argument and
* reimplement the `parse` method.
* NOTE: The parameter element notation is currently not supported.
* TODO: implement error recovery hook via error handler return values
*/
export class EscapeSequenceParser extends Disposable implements IEscapeSequenceParser {
public initialState: number;
public currentState: number;
public precedingCodepoint: number;
// buffers over several parse calls
protected _osc: string;
protected _params: number[];
protected _collect: string;
// handler lookup containers
protected _printHandler: (data: Uint32Array, start: number, end: number) => void;
protected _executeHandlers: any;
protected _csiHandlers: IHandlerCollection<CsiHandler>;
protected _escHandlers: any;
protected _oscHandlers: IHandlerCollection<OscHandler>;
protected _dcsHandlers: any;
protected _activeDcsHandler: IDcsHandler | null;
protected _errorHandler: (state: IParsingState) => IParsingState;
// fallback handlers
protected _printHandlerFb: (data: Uint32Array, start: number, end: number) => void;
protected _executeHandlerFb: (code: number) => void;
protected _csiHandlerFb: (collect: string, params: number[], flag: number) => void;
protected _escHandlerFb: (collect: string, flag: number) => void;
protected _oscHandlerFb: (identifier: number, data: string) => void;
protected _dcsHandlerFb: IDcsHandler;
protected _errorHandlerFb: (state: IParsingState) => IParsingState;
constructor(readonly TRANSITIONS: TransitionTable = VT500_TRANSITION_TABLE) {
super();
this.initialState = ParserState.GROUND;
this.currentState = this.initialState;
this._osc = '';
this._params = [0];
this._collect = '';
this.precedingCodepoint = 0;
// set default fallback handlers and handler lookup containers
this._printHandlerFb = (data, start, end): void => { };
this._executeHandlerFb = (code: number): void => { };
this._csiHandlerFb = (collect: string, params: number[], flag: number): void => { };
this._escHandlerFb = (collect: string, flag: number): void => { };
this._oscHandlerFb = (identifier: number, data: string): void => { };
this._dcsHandlerFb = new DcsDummy();
this._errorHandlerFb = (state: IParsingState): IParsingState => state;
this._printHandler = this._printHandlerFb;
this._executeHandlers = Object.create(null);
this._csiHandlers = Object.create(null);
this._escHandlers = Object.create(null);
this._oscHandlers = Object.create(null);
this._dcsHandlers = Object.create(null);
this._activeDcsHandler = null;
this._errorHandler = this._errorHandlerFb;
// swallow 7bit ST (ESC+\)
this.setEscHandler('\\', () => {});
}
public dispose(): void {
this._executeHandlers = null;
this._escHandlers = null;
this._dcsHandlers = null;
this._activeDcsHandler = null;
}
setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void {
this._printHandler = callback;
}
clearPrintHandler(): void {
this._printHandler = this._printHandlerFb;
}
setExecuteHandler(flag: string, callback: () => void): void {
this._executeHandlers[flag.charCodeAt(0)] = callback;
}
clearExecuteHandler(flag: string): void {
if (this._executeHandlers[flag.charCodeAt(0)]) delete this._executeHandlers[flag.charCodeAt(0)];
}
setExecuteHandlerFallback(callback: (code: number) => void): void {
this._executeHandlerFb = callback;
}
addCsiHandler(flag: string, callback: CsiHandler): IDisposable {
const index = flag.charCodeAt(0);
if (this._csiHandlers[index] === undefined) {
this._csiHandlers[index] = [];
}
const handlerList = this._csiHandlers[index];
handlerList.push(callback);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(callback);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void {
this._csiHandlers[flag.charCodeAt(0)] = [callback];
}
clearCsiHandler(flag: string): void {
if (this._csiHandlers[flag.charCodeAt(0)]) delete this._csiHandlers[flag.charCodeAt(0)];
}
setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void {
this._csiHandlerFb = callback;
}
setEscHandler(collectAndFlag: string, callback: () => void): void {
this._escHandlers[collectAndFlag] = callback;
}
clearEscHandler(collectAndFlag: string): void {
if (this._escHandlers[collectAndFlag]) delete this._escHandlers[collectAndFlag];
}
setEscHandlerFallback(callback: (collect: string, flag: number) => void): void {
this._escHandlerFb = callback;
}
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
if (this._oscHandlers[ident] === undefined) {
this._oscHandlers[ident] = [];
}
const handlerList = this._oscHandlers[ident];
handlerList.push(callback);
return {
dispose: () => {
const handlerIndex = handlerList.indexOf(callback);
if (handlerIndex !== -1) {
handlerList.splice(handlerIndex, 1);
}
}
};
}
setOscHandler(ident: number, callback: (data: string) => void): void {
this._oscHandlers[ident] = [callback];
}
clearOscHandler(ident: number): void {
if (this._oscHandlers[ident]) delete this._oscHandlers[ident];
}
setOscHandlerFallback(callback: (identifier: number, data: string) => void): void {
this._oscHandlerFb = callback;
}
setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void {
this._dcsHandlers[collectAndFlag] = handler;
}
clearDcsHandler(collectAndFlag: string): void {
if (this._dcsHandlers[collectAndFlag]) delete this._dcsHandlers[collectAndFlag];
}
setDcsHandlerFallback(handler: IDcsHandler): void {
this._dcsHandlerFb = handler;
}
setErrorHandler(callback: (state: IParsingState) => IParsingState): void {
this._errorHandler = callback;
}
clearErrorHandler(): void {
this._errorHandler = this._errorHandlerFb;
}
reset(): void {
this.currentState = this.initialState;
this._osc = '';
this._params = [0];
this._collect = '';
this._activeDcsHandler = null;
this.precedingCodepoint = 0;
}
/**
* Parse UTF32 codepoints in `data` up to `length`.
* Note: For several actions with high data load the parsing is optimized
* by using local read ahead loops with hardcoded conditions to
* avoid costly table lookups. Make sure that any change of table values
* will be reflected in the loop conditions as well. Affected states/actions:
* - GROUND:PRINT
* - CSI_PARAM:PARAM
* - DCS_PARAM:PARAM
* - OSC_STRING:OSC_PUT
* - DCS_PASSTHROUGH:DCS_PUT
*/
parse(data: Uint32Array, length: number): void {
let code = 0;
let transition = 0;
let currentState = this.currentState;
let osc = this._osc;
let collect = this._collect;
let params = this._params;
const table: Uint8Array = this.TRANSITIONS.table;
let dcsHandler: IDcsHandler | null = this._activeDcsHandler;
let callback: Function | null = null;
// process input string
for (let i = 0; i < length; ++i) {
code = data[i];
// normal transition & action lookup
transition = table[currentState << TableAccess.INDEX_STATE_SHIFT | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)];
switch (transition >> TableAccess.TRANSITION_ACTION_SHIFT) {
case ParserAction.PRINT:
// read ahead with loop unrolling
// Note: 0x20 (SP) is included, 0x7F (DEL) is excluded
for (let j = i + 1; ; ++j) {
if (j >= length || (code = data[j]) < 0x20 || (code > 0x7e && code < NON_ASCII_PRINTABLE)) {
this._printHandler(data, i, j);
i = j - 1;
break;
}
if (++j >= length || (code = data[j]) < 0x20 || (code > 0x7e && code < NON_ASCII_PRINTABLE)) {
this._printHandler(data, i, j);
i = j - 1;
break;
}
if (++j >= length || (code = data[j]) < 0x20 || (code > 0x7e && code < NON_ASCII_PRINTABLE)) {
this._printHandler(data, i, j);
i = j - 1;
break;
}
if (++j >= length || (code = data[j]) < 0x20 || (code > 0x7e && code < NON_ASCII_PRINTABLE)) {
this._printHandler(data, i, j);
i = j - 1;
break;
}
}
break;
case ParserAction.EXECUTE:
this.precedingCodepoint = 0;
callback = this._executeHandlers[code];
if (callback) callback();
else this._executeHandlerFb(code);
break;
case ParserAction.IGNORE:
break;
case ParserAction.ERROR:
const inject: IParsingState = this._errorHandler(
{
position: i,
code,
currentState,
osc,
collect,
params,
abort: false
});
if (inject.abort) return;
// inject values: currently not implemented
break;
case ParserAction.CSI_DISPATCH:
// dont reset preceding codepoint for REP itself
if (code !== 98) { // 'b'
this.precedingCodepoint = 0;
}
// Trigger CSI Handler
const handlers = this._csiHandlers[code];
let j = handlers ? handlers.length - 1 : -1;
for (; j >= 0; j--) {
// undefined or true means success and to stop bubbling
if (handlers[j](params, collect) !== false) {
break;
}
}
if (j < 0) {
this._csiHandlerFb(collect, params, code);
}
break;
case ParserAction.PARAM:
// inner loop: digits (0x30 - 0x39) and ; (0x3b)
do {
if (code === 0x3b) params.push(0);
else params[params.length - 1] = params[params.length - 1] * 10 + code - 48;
} while (++i < length && (code = data[i]) > 0x2f && (code < 0x3a || code === 0x3b));
i--;
break;
case ParserAction.COLLECT:
collect += String.fromCharCode(code);
break;
case ParserAction.ESC_DISPATCH:
this.precedingCodepoint = 0;
callback = this._escHandlers[collect + String.fromCharCode(code)];
if (callback) callback(collect, code);
else this._escHandlerFb(collect, code);
break;
case ParserAction.CLEAR:
osc = '';
params = [0];
collect = '';
break;
case ParserAction.DCS_HOOK:
this.precedingCodepoint = 0;
dcsHandler = this._dcsHandlers[collect + String.fromCharCode(code)];
if (!dcsHandler) dcsHandler = this._dcsHandlerFb;
dcsHandler.hook(collect, params, code);
break;
case ParserAction.DCS_PUT:
// inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f
// unhook triggered by: 0x1b, 0x9c
for (let j = i + 1; ; ++j) {
if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {
if (dcsHandler) {
dcsHandler.put(data, i, j);
}
i = j - 1;
break;
}
}
break;
case ParserAction.DCS_UNHOOK:
if (dcsHandler) {
dcsHandler.unhook();
dcsHandler = null;
}
if (code === 0x1b) transition |= ParserState.ESCAPE;
osc = '';
params = [0];
collect = '';
break;
case ParserAction.OSC_START:
osc = '';
break;
case ParserAction.OSC_PUT:
// inner loop: 0x20 (SP) included, 0x7F (DEL) included
for (let j = i + 1; ; j++) {
if (j >= length || (code = data[j]) < 0x20 || (code > 0x7f && code <= 0x9f)) {
osc += utf32ToString(data, i, j);
i = j - 1;
break;
}
}
break;
case ParserAction.OSC_END:
this.precedingCodepoint = 0;
if (osc && code !== 0x18 && code !== 0x1a) {
// NOTE: OSC subparsing is not part of the original parser
// we do basic identifier parsing here to offer a jump table for OSC as well
const idx = osc.indexOf(';');
if (idx === -1) {
this._oscHandlerFb(-1, osc); // this is an error (malformed OSC)
} else {
// Note: NaN is not handled here
// either catch it with the fallback handler
// or with an explicit NaN OSC handler
const identifier = parseInt(osc.substring(0, idx));
const content = osc.substring(idx + 1);
// Trigger OSC Handler
const handlers = this._oscHandlers[identifier];
let j = handlers ? handlers.length - 1 : -1;
for (; j >= 0; j--) {
// undefined or true means success and to stop bubbling
if (handlers[j](content) !== false) {
break;
}
}
if (j < 0) {
this._oscHandlerFb(identifier, content);
}
}
}
if (code === 0x1b) transition |= ParserState.ESCAPE;
osc = '';
params = [0];
collect = '';
break;
}
currentState = transition & TableAccess.TRANSITION_STATE_MASK;
}
// save non pushable buffers
this._osc = osc;
this._collect = collect;
this._params = params;
// save active dcs handler reference
this._activeDcsHandler = dcsHandler;
// save state
this.currentState = currentState;
}
}
+149
View File
@@ -0,0 +1,149 @@
/**
* Copyright (c) 2017 The xterm.js authors. All rights reserved.
* @license MIT
*/
import { IDisposable } from 'common/Types';
/**
* Internal states of EscapeSequenceParser.
*/
export const enum ParserState {
GROUND = 0,
ESCAPE = 1,
ESCAPE_INTERMEDIATE = 2,
CSI_ENTRY = 3,
CSI_PARAM = 4,
CSI_INTERMEDIATE = 5,
CSI_IGNORE = 6,
SOS_PM_APC_STRING = 7,
OSC_STRING = 8,
DCS_ENTRY = 9,
DCS_PARAM = 10,
DCS_IGNORE = 11,
DCS_INTERMEDIATE = 12,
DCS_PASSTHROUGH = 13
}
/**
* Internal actions of EscapeSequenceParser.
*/
export const enum ParserAction {
IGNORE = 0,
ERROR = 1,
PRINT = 2,
EXECUTE = 3,
OSC_START = 4,
OSC_PUT = 5,
OSC_END = 6,
CSI_DISPATCH = 7,
PARAM = 8,
COLLECT = 9,
ESC_DISPATCH = 10,
CLEAR = 11,
DCS_HOOK = 12,
DCS_PUT = 13,
DCS_UNHOOK = 14
}
/**
* Internal state of EscapeSequenceParser.
* Used as argument of the error handler to allow
* introspection at runtime on parse errors.
* Return it with altered values to recover from
* faulty states (not yet supported).
* Set `abort` to `true` to abort the current parsing.
*/
export interface IParsingState {
// position in parse string
position: number;
// actual character code
code: number;
// current parser state
currentState: ParserState;
// osc string buffer
osc: string;
// collect buffer with intermediate characters
collect: string;
// params buffer
params: number[];
// should abort (default: false)
abort: boolean;
}
/**
* DCS handler signature for EscapeSequenceParser.
* EscapeSequenceParser handles DCS commands via separate
* subparsers that get hook/unhooked and can handle
* arbitrary amount of data.
*
* On entering a DSC sequence `hook` is called by
* `EscapeSequenceParser`. Use it to initialize or reset
* states needed to handle the current DCS sequence.
* Note: A DCS parser is only instantiated once, therefore
* you cannot rely on the ctor to reinitialize state.
*
* EscapeSequenceParser will call `put` several times if the
* parsed data got split, therefore you might have to collect
* `data` until `unhook` is called.
* Note: `data` is borrowed, if you cannot process the data
* in chunks you have to copy it, doing otherwise will lead to
* data losses or corruption.
*
* `unhook` marks the end of the current DCS sequence.
*/
export interface IDcsHandler {
hook(collect: string, params: number[], flag: number): void;
put(data: Uint32Array, start: number, end: number): void;
unhook(): void;
}
/**
* EscapeSequenceParser interface.
*/
export interface IEscapeSequenceParser extends IDisposable {
/**
* Preceding codepoint to get REP working correctly.
* This must be set by the print handler as last action.
* It gets reset by the parser for any valid sequence beside REP itself.
*/
precedingCodepoint: number;
/**
* Reset the parser to its initial state (handlers are kept).
*/
reset(): void;
/**
* Parse string `data`.
* @param data The data to parse.
*/
parse(data: Uint32Array, length: number): void;
setPrintHandler(callback: (data: Uint32Array, start: number, end: number) => void): void;
clearPrintHandler(): void;
setExecuteHandler(flag: string, callback: () => void): void;
clearExecuteHandler(flag: string): void;
setExecuteHandlerFallback(callback: (code: number) => void): void;
setCsiHandler(flag: string, callback: (params: number[], collect: string) => void): void;
clearCsiHandler(flag: string): void;
setCsiHandlerFallback(callback: (collect: string, params: number[], flag: number) => void): void;
addCsiHandler(flag: string, callback: (params: number[], collect: string) => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
setEscHandler(collectAndFlag: string, callback: () => void): void;
clearEscHandler(collectAndFlag: string): void;
setEscHandlerFallback(callback: (collect: string, flag: number) => void): void;
setOscHandler(ident: number, callback: (data: string) => void): void;
clearOscHandler(ident: number): void;
setOscHandlerFallback(callback: (identifier: number, data: string) => void): void;
setDcsHandler(collectAndFlag: string, handler: IDcsHandler): void;
clearDcsHandler(collectAndFlag: string): void;
setDcsHandlerFallback(handler: IDcsHandler): void;
setErrorHandler(callback: (state: IParsingState) => IParsingState): void;
clearErrorHandler(): void;
}