Merge branch 'master' into Fix-bug-of-some-IMEs-cannot-input-in-terminal

This commit is contained in:
Daniel Imms
2021-03-18 07:00:43 -07:00
committed by GitHub
23 changed files with 2691 additions and 1064 deletions
+244 -230
View File
@@ -55,22 +55,30 @@ describe('Terminal', () => {
// term.onData(() => done());
// term.handler('fake');
// });
it('should fire the onCursorMove event', (done) => {
term.onCursorMove(() => done());
term.writeSync('foo');
it('should fire the onCursorMove event', () => {
return new Promise(async r => {
term.onCursorMove(() => r());
await term.writeP('foo');
});
});
it('should fire the onLineFeed event', (done) => {
term.onLineFeed(() => done());
term.writeSync('\n');
it('should fire the onLineFeed event', () => {
return new Promise(async r => {
term.onLineFeed(() => r());
await term.writeP('\n');
});
});
it('should fire a scroll event when scrollback is created', (done) => {
term.onScroll(() => done());
term.writeSync('\n'.repeat(INIT_ROWS));
it('should fire a scroll event when scrollback is created', () => {
return new Promise(async r => {
term.onScroll(() => r());
await term.writeP('\n'.repeat(INIT_ROWS));
});
});
it('should fire a scroll event when scrollback is cleared', (done) => {
term.writeSync('\n'.repeat(INIT_ROWS));
term.onScroll(() => done());
term.clear();
it('should fire a scroll event when scrollback is cleared', () => {
return new Promise(async r => {
await term.writeP('\n'.repeat(INIT_ROWS));
term.onScroll(() => r());
term.clear();
});
});
it('should fire a key event after a keypress DOM event', (done) => {
term.onKey(e => {
@@ -177,10 +185,10 @@ describe('Terminal', () => {
assert.deepEqual(term.buffer.lines.get(i), term.buffer.getBlankLine(DEFAULT_ATTR_DATA));
}
});
it('should clear a buffer larger than rows', () => {
it('should clear a buffer larger than rows', async () => {
// Fill the buffer with dummy rows
for (let i = 0; i < term.rows * 2; i++) {
term.writeSync('test\n');
await term.writeP('test\n');
}
const promptLine = term.buffer.lines.get(term.buffer.ybase + term.buffer.y);
@@ -224,22 +232,24 @@ describe('Terminal', () => {
});
term.paste('\r\nfoo\nbar\r');
});
it('should respect bracketed paste mode', done => {
term.onData(e => {
assert.equal(e, '\x1b[200~foo\x1b[201~');
done();
it('should respect bracketed paste mode', () => {
return new Promise(async r => {
term.onData(e => {
assert.equal(e, '\x1b[200~foo\x1b[201~');
r();
});
await term.writeP('\x1b[?2004h');
term.paste('foo');
});
term.writeSync('\x1b[?2004h');
term.paste('foo');
});
});
describe('scroll', () => {
describe('scrollLines', () => {
let startYDisp: number;
beforeEach(() => {
beforeEach(async () => {
for (let i = 0; i < INIT_ROWS * 2; i++) {
term.writeSync('test\r\n');
await term.writeP('test\r\n');
}
startYDisp = INIT_ROWS + 1;
});
@@ -272,9 +282,9 @@ describe('Terminal', () => {
describe('scrollPages', () => {
let startYDisp: number;
beforeEach(() => {
beforeEach(async () => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeSync('test\r\n');
await term.writeP('test\r\n');
}
startYDisp = (term.rows * 2) + 1;
});
@@ -295,9 +305,9 @@ describe('Terminal', () => {
});
describe('scrollToTop', () => {
beforeEach(() => {
beforeEach(async () => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeSync('test\r\n');
await term.writeP('test\r\n');
}
});
it('should scroll to the top', () => {
@@ -309,9 +319,9 @@ describe('Terminal', () => {
describe('scrollToBottom', () => {
let startYDisp: number;
beforeEach(() => {
beforeEach(async () => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeSync('test\r\n');
await term.writeP('test\r\n');
}
startYDisp = (term.rows * 2) + 1;
});
@@ -330,9 +340,9 @@ describe('Terminal', () => {
describe('scrollToLine', () => {
let startYDisp: number;
beforeEach(() => {
beforeEach(async () => {
for (let i = 0; i < term.rows * 3; i++) {
term.writeSync('test\r\n');
await term.writeP('test\r\n');
}
startYDisp = (term.rows * 2) + 1;
});
@@ -374,10 +384,10 @@ describe('Terminal', () => {
assert.equal(term.buffer.ydisp, term.buffer.ybase);
});
it('should not scroll down, when a custom keydown handler prevents the event', () => {
it('should not scroll down, when a custom keydown handler prevents the event', async () => {
// Add some output to the terminal
for (let i = 0; i < term.rows * 3; i++) {
term.writeSync('test\r\n');
await term.writeP('test\r\n');
}
const startYDisp = (term.rows * 2) + 1;
term.attachCustomKeyEventHandler(() => {
@@ -716,12 +726,12 @@ describe('Terminal', () => {
});
describe('unicode - surrogates', () => {
it('2 characters per cell', function (): void {
it('2 characters per cell', async function (): Promise<void> {
this.timeout(10000); // This is needed because istanbul patches code and slows it down
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.writeSync(high + String.fromCharCode(i));
await term.writeP(high + String.fromCharCode(i));
const tchar = term.buffer.lines.get(0)!.loadCell(0, cell);
assert.equal(tchar.getChars(), high + String.fromCharCode(i));
assert.equal(tchar.getChars().length, 2);
@@ -730,25 +740,26 @@ describe('Terminal', () => {
term.reset();
}
});
it('2 characters at last cell', () => {
it('2 characters at last cell', async () => {
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.writeSync(high + String.fromCharCode(i));
await term.writeP(high + String.fromCharCode(i));
assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars(), high + String.fromCharCode(i));
assert.equal(term.buffer.lines.get(0)!.loadCell(term.buffer.x - 1, cell).getChars().length, 2);
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), '');
term.reset();
}
});
it('2 characters per cell over line end with autowrap', () => {
it('2 characters per cell over line end with autowrap', async function (): Promise<void> {
this.timeout(10000);
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.writeSync('a' + high + String.fromCharCode(i));
await term.writeP('a' + high + String.fromCharCode(i));
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), 'a');
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars(), high + String.fromCharCode(i));
assert.equal(term.buffer.lines.get(1)!.loadCell(0, cell).getChars().length, 2);
@@ -756,17 +767,18 @@ describe('Terminal', () => {
term.reset();
}
});
it('2 characters per cell over line end without autowrap', () => {
it('2 characters per cell over line end without autowrap', async function (): Promise<void> {
this.timeout(10000);
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.buffer.x = term.cols - 1;
term.writeSync('\x1b[?7l'); // Disable wraparound mode
await term.writeP('\x1b[?7l'); // Disable wraparound mode
const width = wcwidth((0xD800 - 0xD800) * 0x400 + i - 0xDC00 + 0x10000);
if (width !== 1) {
continue;
}
term.writeSync('a' + high + String.fromCharCode(i));
await term.writeP('a' + high + String.fromCharCode(i));
// auto wraparound mode should cut off the rest of the line
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars(), high + String.fromCharCode(i));
assert.equal(term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell).getChars().length, 2);
@@ -774,12 +786,12 @@ describe('Terminal', () => {
term.reset();
}
});
it('splitted surrogates', () => {
it('splitted surrogates', async function (): Promise<void> {
this.timeout(10000);
const high = String.fromCharCode(0xD800);
const cell = new CellData();
for (let i = 0xDC00; i <= 0xDCFF; ++i) {
term.writeSync(high);
term.writeSync(String.fromCharCode(i));
await term.writeP(high + String.fromCharCode(i));
const tchar = term.buffer.lines.get(0)!.loadCell(0, cell);
assert.equal(tchar.getChars(), high + String.fromCharCode(i));
assert.equal(tchar.getChars().length, 2);
@@ -792,16 +804,16 @@ describe('Terminal', () => {
describe('unicode - combining characters', () => {
const cell = new CellData();
it('café', () => {
term.writeSync('cafe\u0301');
it('café', async () => {
await term.writeP('cafe\u0301');
term.buffer.lines.get(0)!.loadCell(3, cell);
assert.equal(cell.getChars(), 'e\u0301');
assert.equal(cell.getChars().length, 2);
assert.equal(cell.getWidth(), 1);
});
it('café - end of line', () => {
it('café - end of line', async () => {
term.buffer.x = term.cols - 1 - 3;
term.writeSync('cafe\u0301');
await term.writeP('cafe\u0301');
term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell);
assert.equal(cell.getChars(), 'e\u0301');
assert.equal(cell.getChars().length, 2);
@@ -811,8 +823,8 @@ describe('Terminal', () => {
assert.equal(cell.getChars().length, 0);
assert.equal(cell.getWidth(), 1);
});
it('multiple combined é', () => {
term.writeSync(Array(100).join('e\u0301'));
it('multiple combined é', async () => {
await term.writeP(Array(100).join('e\u0301'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0)!.loadCell(i, cell);
assert.equal(cell.getChars(), 'e\u0301');
@@ -824,8 +836,8 @@ describe('Terminal', () => {
assert.equal(cell.getChars().length, 2);
assert.equal(cell.getWidth(), 1);
});
it('multiple surrogate with combined', () => {
term.writeSync(Array(100).join('\uD800\uDC00\u0301'));
it('multiple surrogate with combined', async () => {
await term.writeP(Array(100).join('\uD800\uDC00\u0301'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0)!.loadCell(i, cell);
assert.equal(cell.getChars(), '\uD800\uDC00\u0301');
@@ -841,19 +853,19 @@ describe('Terminal', () => {
describe('unicode - fullwidth characters', () => {
const cell = new CellData();
it('cursor movement even', () => {
it('cursor movement even', async () => {
assert.equal(term.buffer.x, 0);
term.writeSync('¥');
await term.writeP('¥');
assert.equal(term.buffer.x, 2);
});
it('cursor movement odd', () => {
it('cursor movement odd', async () => {
term.buffer.x = 1;
assert.equal(term.buffer.x, 1);
term.writeSync('¥');
await term.writeP('¥');
assert.equal(term.buffer.x, 3);
});
it('line of ¥ even', () => {
term.writeSync(Array(50).join('¥'));
it('line of ¥ even', async () => {
await term.writeP(Array(50).join('¥'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0)!.loadCell(i, cell);
if (i % 2) {
@@ -871,9 +883,9 @@ describe('Terminal', () => {
assert.equal(cell.getChars().length, 1);
assert.equal(cell.getWidth(), 2);
});
it('line of ¥ odd', () => {
it('line of ¥ odd', async () => {
term.buffer.x = 1;
term.writeSync(Array(50).join('¥'));
await term.writeP(Array(50).join('¥'));
for (let i = 1; i < term.cols - 1; ++i) {
term.buffer.lines.get(0)!.loadCell(i, cell);
if (!(i % 2)) {
@@ -895,9 +907,9 @@ describe('Terminal', () => {
assert.equal(cell.getChars().length, 1);
assert.equal(cell.getWidth(), 2);
});
it('line of ¥ with combining odd', () => {
it('line of ¥ with combining odd', async () => {
term.buffer.x = 1;
term.writeSync(Array(50).join('¥\u0301'));
await term.writeP(Array(50).join('¥\u0301'));
for (let i = 1; i < term.cols - 1; ++i) {
term.buffer.lines.get(0)!.loadCell(i, cell);
if (!(i % 2)) {
@@ -919,8 +931,8 @@ describe('Terminal', () => {
assert.equal(cell.getChars().length, 2);
assert.equal(cell.getWidth(), 2);
});
it('line of ¥ with combining even', () => {
term.writeSync(Array(50).join('¥\u0301'));
it('line of ¥ with combining even', async () => {
await term.writeP(Array(50).join('¥\u0301'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0)!.loadCell(i, cell);
if (i % 2) {
@@ -938,9 +950,9 @@ describe('Terminal', () => {
assert.equal(cell.getChars().length, 2);
assert.equal(cell.getWidth(), 2);
});
it('line of surrogate fullwidth with combining odd', () => {
it('line of surrogate fullwidth with combining odd', async () => {
term.buffer.x = 1;
term.writeSync(Array(50).join('\ud843\ude6d\u0301'));
await term.writeP(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 1; i < term.cols - 1; ++i) {
term.buffer.lines.get(0)!.loadCell(i, cell);
if (!(i % 2)) {
@@ -962,8 +974,8 @@ describe('Terminal', () => {
assert.equal(cell.getChars().length, 3);
assert.equal(cell.getWidth(), 2);
});
it('line of surrogate fullwidth with combining even', () => {
term.writeSync(Array(50).join('\ud843\ude6d\u0301'));
it('line of surrogate fullwidth with combining even', async () => {
await term.writeP(Array(50).join('\ud843\ude6d\u0301'));
for (let i = 0; i < term.cols; ++i) {
term.buffer.lines.get(0)!.loadCell(i, cell);
if (i % 2) {
@@ -985,24 +997,24 @@ describe('Terminal', () => {
describe('insert mode', () => {
const cell = new CellData();
it('halfwidth - all', () => {
term.writeSync(Array(9).join('0123456789').slice(-80));
it('halfwidth - all', async () => {
await term.writeP(Array(9).join('0123456789').slice(-80));
term.buffer.x = 10;
term.buffer.y = 0;
term.write('\x1b[4h');
term.writeSync('abcde');
await term.writeP('abcde');
assert.equal(term.buffer.lines.get(0)!.length, term.cols);
assert.equal(term.buffer.lines.get(0)!.loadCell(10, cell).getChars(), 'a');
assert.equal(term.buffer.lines.get(0)!.loadCell(14, cell).getChars(), 'e');
assert.equal(term.buffer.lines.get(0)!.loadCell(15, cell).getChars(), '0');
assert.equal(term.buffer.lines.get(0)!.loadCell(79, cell).getChars(), '4');
});
it('fullwidth - insert', () => {
term.writeSync(Array(9).join('0123456789').slice(-80));
it('fullwidth - insert', async () => {
await term.writeP(Array(9).join('0123456789').slice(-80));
term.buffer.x = 10;
term.buffer.y = 0;
term.write('\x1b[4h');
term.writeSync('¥¥¥');
await term.writeP('¥¥¥');
assert.equal(term.buffer.lines.get(0)!.length, term.cols);
assert.equal(term.buffer.lines.get(0)!.loadCell(10, cell).getChars(), '¥');
assert.equal(term.buffer.lines.get(0)!.loadCell(11, cell).getChars(), '');
@@ -1010,17 +1022,17 @@ describe('Terminal', () => {
assert.equal(term.buffer.lines.get(0)!.loadCell(15, cell).getChars(), '');
assert.equal(term.buffer.lines.get(0)!.loadCell(79, cell).getChars(), '3');
});
it('fullwidth - right border', () => {
term.writeSync(Array(41).join('¥'));
it('fullwidth - right border', async () => {
await term.writeP(Array(41).join('¥'));
term.buffer.x = 10;
term.buffer.y = 0;
term.write('\x1b[4h');
term.writeSync('a');
await term.writeP('a');
assert.equal(term.buffer.lines.get(0)!.length, term.cols);
assert.equal(term.buffer.lines.get(0)!.loadCell(10, cell).getChars(), 'a');
assert.equal(term.buffer.lines.get(0)!.loadCell(11, cell).getChars(), '¥');
assert.equal(term.buffer.lines.get(0)!.loadCell(79, cell).getChars(), ''); // fullwidth char got replaced
term.writeSync('b');
await term.writeP('b');
assert.equal(term.buffer.lines.get(0)!.length, term.cols);
assert.equal(term.buffer.lines.get(0)!.loadCell(11, cell).getChars(), 'b');
assert.equal(term.buffer.lines.get(0)!.loadCell(12, cell).getChars(), '¥');
@@ -1042,85 +1054,87 @@ describe('Terminal', () => {
linkifier.attachToDom({} as any, mouseZoneManager);
});
function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[], done: Mocha.Done): void {
terminal.writeSync(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
linkifier.linkifyRows();
// Allow linkify to happen
setTimeout(() => {
assert.equal(mouseZoneManager.zones.length, links.length);
links.forEach((l, i) => {
assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1);
assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1);
assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1);
assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1);
});
done();
}, 0);
function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[]): Promise<void> {
return new Promise(async r => {
await terminal.writeP(rowText);
linkifier.registerLinkMatcher(linkMatcherRegex, () => {});
linkifier.linkifyRows();
// Allow linkify to happen
setTimeout(() => {
assert.equal(mouseZoneManager.zones.length, links.length);
links.forEach((l, i) => {
assert.equal(mouseZoneManager.zones[i].x1, l.x1 + 1);
assert.equal(mouseZoneManager.zones[i].x2, l.x2 + 1);
assert.equal(mouseZoneManager.zones[i].y1, l.y1 + 1);
assert.equal(mouseZoneManager.zones[i].y2, l.y2 + 1);
});
r();
}, 0);
});
}
describe('unicode before the match', () => {
it('combining - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
it('combining - match within one line', () => {
return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}]);
});
it('combining - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
it('combining - match over two lines', () => {
return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]);
});
it('surrogate - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
it('surrogate - match within one line', () => {
return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}]);
});
it('surrogate - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
it('surrogate - match over two lines', () => {
return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]);
});
it('combining surrogate - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}], done);
it('combining surrogate - match within one line', () => {
return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}]);
});
it('combining surrogate - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
it('combining surrogate - match over two lines', () => {
return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]);
});
it('fullwidth - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
it('fullwidth - match within one line', () => {
return assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}]);
});
it('fullwidth - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
it('fullwidth - match over two lines', () => {
return assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]);
});
it('combining fullwidth - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
it('combining fullwidth - match within one line', () => {
return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}]);
});
it('combining fullwidth - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}], done);
it('combining fullwidth - match over two lines', () => {
return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 8, x2: 1, y1: 0, y2: 1}]);
});
});
describe('unicode within the match', () => {
it('combining - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
it('combining - match within one line', () => {
return assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}]);
});
it('combining - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
it('combining - match over two lines', () => {
return assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}]);
});
it('surrogate - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
it('surrogate - match within one line', () => {
return assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{x1: 5, x2: 8, y1: 0, y2: 0}]);
});
it('surrogate - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
it('surrogate - match over two lines', () => {
return assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}]);
});
it('combining surrogate - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}], done);
it('combining surrogate - match within one line', () => {
return assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 5, x2: 8, y1: 0, y2: 0}]);
});
it('combining surrogate - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}], done);
it('combining surrogate - match over two lines', () => {
return assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}]);
});
it('fullwidth - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
it('fullwidth - match within one line', () => {
return assertLinkifiesInTerminal('test a1b', /a1b/, [{x1: 5, x2: 9, y1: 0, y2: 0}]);
});
it('fullwidth - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
it('fullwidth - match over two lines', () => {
return assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}]);
});
it('combining fullwidth - match within one line', function(done: () => void): void {
assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}], done);
it('combining fullwidth - match within one line', () => {
return assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{x1: 5, x2: 9, y1: 0, y2: 0}]);
});
it('combining fullwidth - match over two lines', function(done: () => void): void {
assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}], done);
it('combining fullwidth - match over two lines', () => {
return assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}]);
});
});
});
@@ -1132,9 +1146,9 @@ describe('Terminal', () => {
terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
});
it('multiline ascii', () => {
it('multiline ascii', async () => {
const input = 'This is ASCII text spanning multiple lines.';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < input.length; ++i) {
@@ -1143,9 +1157,9 @@ describe('Terminal', () => {
}
});
it('combining e\u0301 in a sentence', () => {
it('combining e\u0301 in a sentence', async () => {
const input = 'Sitting in the cafe\u0301 drinking coffee.';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < 19; ++i) {
@@ -1163,9 +1177,9 @@ describe('Terminal', () => {
}
});
it('multiline combining e\u0301', () => {
it('multiline combining e\u0301', async () => {
const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// every buffer cell index contains 2 string indices
@@ -1175,9 +1189,9 @@ describe('Terminal', () => {
}
});
it('surrogate char in a sentence', () => {
it('surrogate char in a sentence', async () => {
const input = 'The 𝄞 is a clef widely used in modern notation.';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < 5; ++i) {
@@ -1195,9 +1209,9 @@ describe('Terminal', () => {
}
});
it('multiline surrogate char', () => {
it('multiline surrogate char', async () => {
const input = '𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞𝄞';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// every buffer cell index contains 2 string indices
@@ -1207,10 +1221,10 @@ describe('Terminal', () => {
}
});
it('surrogate char with combining', () => {
it('surrogate char with combining', async () => {
// eye of Ra with acute accent - string length of 3
const input = '𓂀\u0301 - the eye hiroglyph with an acute accent.';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// index 0..2 should map to 0
@@ -1222,9 +1236,9 @@ describe('Terminal', () => {
}
});
it('multiline surrogate with combining', () => {
it('multiline surrogate with combining', async () => {
const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
// every buffer cell index contains 3 string indices
@@ -1234,9 +1248,9 @@ describe('Terminal', () => {
}
});
it('fullwidth chars', () => {
it('fullwidth chars', async () => {
const input = 'These 123 are some fat numbers.';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < 6; ++i) {
@@ -1253,9 +1267,9 @@ describe('Terminal', () => {
}
});
it('multiline fullwidth chars', () => {
it('multiline fullwidth chars', async () => {
const input = '12345678901234567890';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 9; i < input.length; ++i) {
@@ -1264,9 +1278,9 @@ describe('Terminal', () => {
}
});
it('fullwidth combining with emoji - match emoji cell', () => {
it('fullwidth combining with emoji - match emoji cell', async () => {
const input = 'Lots of ¥\u0301 make me 😃.';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
const stringIndex = s.match(/😃/)!.index!;
@@ -1274,14 +1288,14 @@ describe('Terminal', () => {
assert(terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars(), '😃');
});
it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', () => {
it('multiline fullwidth chars with offset 1 (currently tests for broken behavior)', async () => {
const input = 'a12345678901234567890';
// the 'a' at the beginning moves all fullwidth chars one to the right
// now the end of the line contains a dangling empty cell since
// the next fullwidth char has to wrap early
// the dangling last cell is wrongly added in the string
// --> fixable after resolving #1685
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 10; i < input.length; ++i) {
@@ -1291,9 +1305,9 @@ describe('Terminal', () => {
}
});
it('test fully wrapped buffer up to last char', () => {
it('test fully wrapped buffer up to last char', async () => {
const input = Array(6).join('1234567890');
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < input.length; ++i) {
@@ -1302,10 +1316,10 @@ describe('Terminal', () => {
}
});
it('test fully wrapped buffer up to last char with full width odd', () => {
it('test fully wrapped buffer up to last char with full width odd', async () => {
const input = 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301'
+ 'a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301a¥\u0301';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(input, s);
for (let i = 0; i < input.length; ++i) {
@@ -1320,16 +1334,16 @@ describe('Terminal', () => {
}
});
it('should handle \t in lines correctly', () => {
it('should handle \t in lines correctly', async () => {
const input = '\thttps://google.de';
terminal.writeSync(input);
await terminal.writeP(input);
const s = terminal.buffer.iterator(true).next().content;
assert.equal(s, Array(terminal.optionsService.options.tabStopWidth + 1).join(' ') + 'https://google.de');
});
});
describe('BufferStringIterator', function(): void {
it('iterator does not overflow buffer limits', function(): void {
it('iterator does not overflow buffer limits', async () => {
const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5});
const data = [
'aaaaaaaaaa',
@@ -1343,7 +1357,7 @@ describe('Terminal', () => {
'aaaaaaaaaa',
'aaaaaaaaaa'
];
terminal.writeSync(data.join(''));
await terminal.writeP(data.join(''));
// brute force test with insane values
assert.doesNotThrow(() => {
for (let overscan = 0; overscan < 20; ++overscan) {
@@ -1361,7 +1375,7 @@ describe('Terminal', () => {
});
describe('Windows Mode', () => {
it('should mark lines as wrapped when the line ends in a non-null character after a LF', () => {
it('should mark lines as wrapped when the line ends in a non-null character after a LF', async () => {
const data = [
'aaaaaaaaaa\n\r', // cannot wrap as it's the first
'aaaaaaaaa\n\r', // wrapped (windows mode only)
@@ -1369,19 +1383,19 @@ describe('Terminal', () => {
];
const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false});
normalTerminal.writeSync(data.join(''));
await normalTerminal.writeP(data.join(''));
assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false);
assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false);
assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false);
const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true});
windowsModeTerminal.writeSync(data.join(''));
await windowsModeTerminal.writeP(data.join(''));
assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false);
assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character');
assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false);
});
it('should mark lines as wrapped when the line ends in a non-null character after a CUP', () => {
it('should mark lines as wrapped when the line ends in a non-null character after a CUP', async () => {
const data = [
'aaaaaaaaaa\x1b[2;1H', // cannot wrap as it's the first
'aaaaaaaaa\x1b[3;1H', // wrapped (windows mode only)
@@ -1389,22 +1403,22 @@ describe('Terminal', () => {
];
const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false});
normalTerminal.writeSync(data.join(''));
await normalTerminal.writeP(data.join(''));
assert.equal(normalTerminal.buffer.lines.get(0)!.isWrapped, false);
assert.equal(normalTerminal.buffer.lines.get(1)!.isWrapped, false);
assert.equal(normalTerminal.buffer.lines.get(2)!.isWrapped, false);
const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true});
windowsModeTerminal.writeSync(data.join(''));
await windowsModeTerminal.writeP(data.join(''));
assert.equal(windowsModeTerminal.buffer.lines.get(0)!.isWrapped, false);
assert.equal(windowsModeTerminal.buffer.lines.get(1)!.isWrapped, true, 'This line should wrap in Windows mode as the previous line ends in a non-null character');
assert.equal(windowsModeTerminal.buffer.lines.get(2)!.isWrapped, false);
});
});
it('convertEol setting', function(): void {
it('convertEol setting', async () => {
// not converting
const termNotConverting = new TestTerminal({cols: 15, rows: 10});
termNotConverting.writeSync('Hello\nWorld');
await termNotConverting.writeP('Hello\nWorld');
assert.equal(termNotConverting.buffer.lines.get(0)!.translateToString(false), 'Hello ');
assert.equal(termNotConverting.buffer.lines.get(1)!.translateToString(false), ' World ');
assert.equal(termNotConverting.buffer.lines.get(0)!.translateToString(true), 'Hello');
@@ -1412,7 +1426,7 @@ describe('Terminal', () => {
// converting
const termConverting = new TestTerminal({cols: 15, rows: 10, convertEol: true});
termConverting.writeSync('Hello\nWorld');
await termConverting.writeP('Hello\nWorld');
assert.equal(termConverting.buffer.lines.get(0)!.translateToString(false), 'Hello ');
assert.equal(termConverting.buffer.lines.get(1)!.translateToString(false), 'World ');
assert.equal(termConverting.buffer.lines.get(0)!.translateToString(true), 'Hello');
@@ -1433,54 +1447,54 @@ describe('Terminal', () => {
beforeEach(() => {
term = new TestTerminal({cols: 5, rows: 5, scrollback: 1});
});
it('SL (scrollLeft)', () => {
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[ @');
it('SL (scrollLeft)', async () => {
await term.writeP('12345'.repeat(6));
await term.writeP('\x1b[ @');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '2345', '2345', '2345', '2345', '2345']);
term.writeSync('\x1b[0 @');
await term.writeP('\x1b[0 @');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '345', '345', '345', '345', '345']);
term.writeSync('\x1b[2 @');
await term.writeP('\x1b[2 @');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '5', '5', '5', '5', '5']);
});
it('SR (scrollRight)', () => {
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[ A');
it('SR (scrollRight)', async () => {
await term.writeP('12345'.repeat(6));
await term.writeP('\x1b[ A');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']);
term.writeSync('\x1b[0 A');
await term.writeP('\x1b[0 A');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']);
term.writeSync('\x1b[2 A');
await term.writeP('\x1b[2 A');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']);
});
it('insertColumns (DECIC)', () => {
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[\'}');
it('insertColumns (DECIC)', async () => {
await term.writeP('12345'.repeat(6));
await term.writeP('\x1b[3;3H');
await term.writeP('\x1b[\'}');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
term.reset();
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[1\'}');
await term.writeP('12345'.repeat(6));
await term.writeP('\x1b[3;3H');
await term.writeP('\x1b[1\'}');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']);
term.reset();
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[2\'}');
await term.writeP('12345'.repeat(6));
await term.writeP('\x1b[3;3H');
await term.writeP('\x1b[2\'}');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']);
});
it('deleteColumns (DECDC)', () => {
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[\'~');
it('deleteColumns (DECDC)', async () => {
await term.writeP('12345'.repeat(6));
await term.writeP('\x1b[3;3H');
await term.writeP('\x1b[\'~');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']);
term.reset();
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[1\'~');
await term.writeP('12345'.repeat(6));
await term.writeP('\x1b[3;3H');
await term.writeP('\x1b[1\'~');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '1245', '1245', '1245', '1245', '1245']);
term.reset();
term.writeSync('12345'.repeat(6));
term.writeSync('\x1b[3;3H');
term.writeSync('\x1b[2\'~');
await term.writeP('12345'.repeat(6));
await term.writeP('\x1b[3;3H');
await term.writeP('\x1b[2\'~');
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '125', '125', '125', '125', '125']);
});
});
@@ -1493,41 +1507,41 @@ describe('Terminal', () => {
});
describe('reverseWraparound set', () => {
it('should not reverse outside of scroll margins', () => {
it('should not reverse outside of scroll margins', async () => {
// prepare buffer content
term.writeSync('#####abcdefghijklmnopqrstuvwxy');
await term.writeP('#####abcdefghijklmnopqrstuvwxy');
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
assert.equal(term.buffer.ydisp, 1);
assert.equal(term.buffer.x, 5);
assert.equal(term.buffer.y, 4);
term.writeSync(ttyBS.repeat(100));
await term.writeP(ttyBS.repeat(100));
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']);
term.writeSync('\x1b[?45h');
term.writeSync('uvwxy');
await term.writeP('\x1b[?45h');
await term.writeP('uvwxy');
// set top/bottom to 1/3 (0-based)
term.writeSync('\x1b[2;4r');
await term.writeP('\x1b[2;4r');
// place cursor below scroll bottom
term.buffer.x = 5;
term.buffer.y = 4;
term.writeSync(ttyBS.repeat(100));
await term.writeP(ttyBS.repeat(100));
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']);
term.writeSync('uvwxy');
await term.writeP('uvwxy');
// place cursor within scroll margins
term.buffer.x = 5;
term.buffer.y = 3;
term.writeSync(ttyBS.repeat(100));
await term.writeP(ttyBS.repeat(100));
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', ' ', ' ', ' ', 'uvwxy']);
assert.equal(term.buffer.x, 0);
assert.equal(term.buffer.y, term.buffer.scrollTop); // stops at 0, scrollTop
term.writeSync('fghijklmnopqrst');
await term.writeP('fghijklmnopqrst');
// place cursor above scroll top
term.buffer.x = 5;
term.buffer.y = 0;
term.writeSync(ttyBS.repeat(100));
await term.writeP(ttyBS.repeat(100));
assert.deepEqual(getLines(term, 6), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
});
});
@@ -1541,22 +1555,22 @@ describe('Terminal', () => {
let markers: IMarker[];
let disposeStack: IMarker[];
let term: TestTerminal;
beforeEach(() => {
beforeEach(async () => {
term = new TestTerminal({});
markers = [];
disposeStack = [];
term.optionsService.setOption('scrollback', 1);
term.resize(10, 5);
markers.push(term.buffers.active.addMarker(term.buffers.active.y));
term.writeSync('\x1b[r0\r\n');
await term.writeP('\x1b[r0\r\n');
markers.push(term.buffers.active.addMarker(term.buffers.active.y));
term.writeSync('1\r\n');
await term.writeP('1\r\n');
markers.push(term.buffers.active.addMarker(term.buffers.active.y));
term.writeSync('2\r\n');
await term.writeP('2\r\n');
markers.push(term.buffers.active.addMarker(term.buffers.active.y));
term.writeSync('3\r\n');
await term.writeP('3\r\n');
markers.push(term.buffers.active.addMarker(term.buffers.active.y));
term.writeSync('4');
await term.writeP('4');
for (let i = 0; i < markers.length; ++i) {
const marker = markers[i];
marker.onDispose(() => disposeStack.push(marker));
@@ -1565,15 +1579,15 @@ describe('Terminal', () => {
it('initial', () => {
assert.deepEqual(markers.map(m => m.line), [0, 1, 2, 3, 4]);
});
it('should dispose on normal trim off the top', () => {
it('should dispose on normal trim off the top', async () => {
// moves top line into scrollback
term.writeSync('\n');
await term.writeP('\n');
assert.deepEqual(disposeStack, []);
// trims first marker
term.writeSync('\n');
await term.writeP('\n');
assert.deepEqual(disposeStack, [markers[0]]);
// trims second marker
term.writeSync('\n');
await term.writeP('\n');
assert.deepEqual(disposeStack, [markers[0], markers[1]]);
// trimmed marker objs should be disposed
assert.deepEqual(disposeStack.map(el => el.isDisposed), [true, true]);
@@ -1581,14 +1595,14 @@ describe('Terminal', () => {
// trimmed markers should contain line -1
assert.deepEqual(disposeStack.map(el => el.line), [-1, -1]);
});
it('should dispose on DL', () => {
term.writeSync('\x1b[3;1H'); // move cursor to 0, 2
term.writeSync('\x1b[2M'); // delete 2 lines
it('should dispose on DL', async () => {
await term.writeP('\x1b[3;1H'); // move cursor to 0, 2
await term.writeP('\x1b[2M'); // delete 2 lines
assert.deepEqual(disposeStack, [markers[2], markers[3]]);
});
it('should dispose on IL', () => {
term.writeSync('\x1b[3;1H'); // move cursor to 0, 2
term.writeSync('\x1b[2L'); // insert 2 lines
it('should dispose on IL', async () => {
await term.writeP('\x1b[3;1H'); // move cursor to 0, 2
await term.writeP('\x1b[2L'); // insert 2 lines
assert.deepEqual(disposeStack, [markers[4], markers[3]]);
assert.deepEqual(markers.map(el => el.line), [0, 1, 4, -1, -1]);
});
+1 -1
View File
@@ -74,7 +74,7 @@ describe('Escape Sequence Files', function(): void {
let content = '';
const OSC_CODE = 12345;
await new Promise(resolve => {
customHandler = term.addOscHandler(OSC_CODE, () => {
customHandler = term.registerOscHandler(OSC_CODE, () => {
// grab terminal viewport content
content = terminalToString(term);
resolve();
+7 -4
View File
@@ -21,6 +21,9 @@ export class TestTerminal extends Terminal {
public get curAttrData(): IAttributeData { return (this as any)._inputHandler._curAttrData; }
public keyDown(ev: any): boolean | undefined { return this._keyDown(ev); }
public keyPress(ev: any): boolean { return this._keyPress(ev); }
public writeP(data: string | Uint8Array): Promise<void> {
return new Promise(r => this.write(data, r));
}
}
export class MockTerminal implements ITerminal {
@@ -75,16 +78,16 @@ export class MockTerminal implements ITerminal {
public attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void {
throw new Error('Method not implemented.');
}
public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
throw new Error('Method not implemented.');
}
public registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => boolean | void, options?: ILinkMatcherOptions): number {
+4 -4
View File
@@ -52,10 +52,10 @@ export interface IPublicTerminal extends IDisposable {
resize(columns: number, rows: number): void;
open(parent: HTMLElement): void;
attachCustomKeyEventHandler(customKeyEventHandler: (event: KeyboardEvent) => boolean): void;
addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable;
addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable;
addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable;
addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable;
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
registerLinkMatcher(regex: RegExp, handler: (event: MouseEvent, uri: string) => void, options?: ILinkMatcherOptions): number;
deregisterLinkMatcher(matcherId: number): void;
registerLinkProvider(linkProvider: ILinkProvider): IDisposable;
+12 -12
View File
@@ -292,28 +292,28 @@ class BufferLineApiView implements IBufferLineApi {
class ParserApi implements IParser {
constructor(private _core: ITerminal) { }
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable {
return this._core.addCsiHandler(id, (params: IParams) => callback(params.toArray()));
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise<boolean>): IDisposable {
return this._core.registerCsiHandler(id, (params: IParams) => callback(params.toArray()));
}
public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable {
public addCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise<boolean>): IDisposable {
return this.registerCsiHandler(id, callback);
}
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable {
return this._core.addDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray()));
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise<boolean>): IDisposable {
return this._core.registerDcsHandler(id, (data: string, params: IParams) => callback(data, params.toArray()));
}
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable {
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise<boolean>): IDisposable {
return this.registerDcsHandler(id, callback);
}
public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
return this._core.addEscHandler(id, handler);
public registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable {
return this._core.registerEscHandler(id, handler);
}
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable {
public addEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable {
return this.registerEscHandler(id, handler);
}
public registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._core.addOscHandler(ident, callback);
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this._core.registerOscHandler(ident, callback);
}
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
public addOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this.registerOscHandler(ident, callback);
}
}
+20 -10
View File
@@ -109,7 +109,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this.register(this.optionsService.onOptionChange(key => this._updateOptions(key)));
// Setup WriteBuffer
this._writeBuffer = new WriteBuffer(data => this._inputHandler.parse(data));
this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult));
}
public dispose(): void {
@@ -125,7 +125,17 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
this._writeBuffer.write(data, callback);
}
/**
* Write data to terminal synchonously.
*
* This method is unreliable with async parser handlers, thus should not
* be used anymore. If you need blocking semantics on data input consider
* `write` with a callback instead.
*
* @deprecated Unreliable, will be removed soon.
*/
public writeSync(data: string | Uint8Array): void {
console.error('writeSync is unreliable and will be removed soon.');
this._writeBuffer.writeSync(data);
}
@@ -268,23 +278,23 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
}
/** Add handler for ESC escape sequence. See xterm.d.ts for details. */
public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable {
return this._inputHandler.addEscHandler(id, callback);
public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable {
return this._inputHandler.registerEscHandler(id, callback);
}
/** Add handler for DCS escape sequence. See xterm.d.ts for details. */
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
return this._inputHandler.addDcsHandler(id, callback);
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable {
return this._inputHandler.registerDcsHandler(id, callback);
}
/** Add handler for CSI escape sequence. See xterm.d.ts for details. */
public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
return this._inputHandler.addCsiHandler(id, callback);
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable {
return this._inputHandler.registerCsiHandler(id, callback);
}
/** Add handler for OSC escape sequence. See xterm.d.ts for details. */
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
return this._inputHandler.addOscHandler(ident, callback);
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this._inputHandler.registerOscHandler(ident, callback);
}
protected _setup(): void {
@@ -322,7 +332,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal {
if (!this._windowsMode) {
const disposables: IDisposable[] = [];
disposables.push(this.onLineFeed(updateWindowsModeWrappedState.bind(null, this._bufferService)));
disposables.push(this.addCsiHandler({ final: 'H' }, () => {
disposables.push(this.registerCsiHandler({ final: 'H' }, () => {
updateWindowsModeWrappedState(this._bufferService);
return false;
}));
File diff suppressed because it is too large Load Diff
+98 -20
View File
@@ -4,7 +4,7 @@
* @license MIT
*/
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IAnsiColorChangeEvent } from 'common/Types';
import { IInputHandler, IAttributeData, IDisposable, IWindowOptions, IAnsiColorChangeEvent, IParseStack } from 'common/Types';
import { C0, C1 } from 'common/data/EscapeSequences';
import { CHARSETS, DEFAULT_CHARSET } from 'common/data/Charsets';
import { EscapeSequenceParser } from 'common/parser/EscapeSequenceParser';
@@ -17,7 +17,7 @@ import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IFunctionId
import { NULL_CELL_CODE, NULL_CELL_WIDTH, Attributes, FgFlags, BgFlags, Content, UnderlineStyle } from 'common/buffer/Constants';
import { CellData } from 'common/buffer/CellData';
import { AttributeData } from 'common/buffer/AttributeData';
import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService } from 'common/services/Services';
import { ICoreService, IBufferService, IOptionsService, ILogService, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, LogLevelEnum } from 'common/services/Services';
import { OscHandler } from 'common/parser/OscParser';
import { DcsHandler } from 'common/parser/DcsParser';
@@ -97,6 +97,9 @@ export enum WindowsOptionsReportType {
GET_CELL_SIZE_PIXELS = 1
}
// create a warning log if an async handler takes longer than the limit (in ms)
const SLOW_ASYNC_LIMIT = 5000;
/**
* DCS subparser implementations
*/
@@ -259,6 +262,14 @@ export class InputHandler extends Disposable implements IInputHandler {
private _onAnsiColorChange = new EventEmitter<IAnsiColorChangeEvent>();
public get onAnsiColorChange(): IEvent<IAnsiColorChangeEvent> { return this._onAnsiColorChange.event; }
private _parseStack: IParseStack = {
paused: false,
cursorStartX: 0,
cursorStartY: 0,
decodedLength: 0,
position: 0
};
constructor(
private readonly _bufferService: IBufferService,
private readonly _charsetService: ICharsetService,
@@ -460,10 +471,64 @@ export class InputHandler extends Disposable implements IInputHandler {
super.dispose();
}
public parse(data: string | Uint8Array): void {
/**
* Async parse support.
*/
private _preserveStack(cursorStartX: number, cursorStartY: number, decodedLength: number, position: number): void {
this._parseStack.paused = true;
this._parseStack.cursorStartX = cursorStartX;
this._parseStack.cursorStartY = cursorStartY;
this._parseStack.decodedLength = decodedLength;
this._parseStack.position = position;
}
private _logSlowResolvingAsync(p: Promise<boolean>): void {
// log a limited warning about an async handler taking too long
if (this._logService.logLevel <= LogLevelEnum.WARN) {
Promise.race([p, new Promise((res, rej) => setTimeout(() => rej('#SLOW_TIMEOUT'), SLOW_ASYNC_LIMIT))])
.catch(err => {
if (err !== '#SLOW_TIMEOUT') {
throw err;
}
console.warn(`async parser handler taking longer than ${SLOW_ASYNC_LIMIT} ms`);
});
}
}
/**
* Parse call with async handler support.
*
* Whether the stack state got preserved for the next call, is indicated by the return value:
* - undefined (void):
* all handlers were sync, no stack save, continue normally with next chunk
* - Promise\<boolean\>:
* execution stopped at async handler, stack saved, continue with
* same chunk and the promise resolve value as `promiseResult` until the method returns `undefined`
*
* Note: This method should only be called by `Terminal.write` to ensure correct execution order and
* proper continuation of async parser handlers.
*/
public parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise<boolean> {
let result: void | Promise<boolean>;
let buffer = this._bufferService.buffer;
const cursorStartX = buffer.x;
const cursorStartY = buffer.y;
let cursorStartX = buffer.x;
let cursorStartY = buffer.y;
let start = 0;
const wasPaused = this._parseStack.paused;
if (wasPaused) {
// assumption: _parseBuffer never mutates between async calls
if (result = this._parser.parse(this._parseBuffer, this._parseStack.decodedLength, promiseResult)) {
this._logSlowResolvingAsync(result);
return result;
}
cursorStartX = this._parseStack.cursorStartX;
cursorStartY = this._parseStack.cursorStartY;
this._parseStack.paused = false;
if (data.length > MAX_PARSEBUFFER_LENGTH) {
start = this._parseStack.position + MAX_PARSEBUFFER_LENGTH;
}
}
this._logService.debug('parsing data', data);
@@ -475,22 +540,35 @@ export class InputHandler extends Disposable implements IInputHandler {
}
// Clear the dirty row service so we know which lines changed as a result of parsing
this._dirtyRowService.clearRange();
// Important: do not clear between async calls, otherwise we lost pending update information.
if (!wasPaused) {
this._dirtyRowService.clearRange();
}
// process big data in smaller chunks
if (data.length > MAX_PARSEBUFFER_LENGTH) {
for (let i = 0; i < data.length; i += MAX_PARSEBUFFER_LENGTH) {
for (let i = start; i < data.length; i += MAX_PARSEBUFFER_LENGTH) {
const end = i + MAX_PARSEBUFFER_LENGTH < data.length ? i + MAX_PARSEBUFFER_LENGTH : data.length;
const len = (typeof data === 'string')
? this._stringDecoder.decode(data.substring(i, end), this._parseBuffer)
: this._utf8Decoder.decode(data.subarray(i, end), this._parseBuffer);
this._parser.parse(this._parseBuffer, len);
if (result = this._parser.parse(this._parseBuffer, len)) {
this._preserveStack(cursorStartX, cursorStartY, len, i);
this._logSlowResolvingAsync(result);
return result;
}
}
} else {
const len = (typeof data === 'string')
? this._stringDecoder.decode(data, this._parseBuffer)
: this._utf8Decoder.decode(data, this._parseBuffer);
this._parser.parse(this._parseBuffer, len);
if (!wasPaused) {
const len = (typeof data === 'string')
? this._stringDecoder.decode(data, this._parseBuffer)
: this._utf8Decoder.decode(data, this._parseBuffer);
if (result = this._parser.parse(this._parseBuffer, len)) {
this._preserveStack(cursorStartX, cursorStartY, len, 0);
this._logSlowResolvingAsync(result);
return result;
}
}
}
buffer = this._bufferService.buffer;
@@ -643,9 +721,9 @@ export class InputHandler extends Disposable implements IInputHandler {
}
/**
* Forward addCsiHandler from parser.
* Forward registerCsiHandler from parser.
*/
public addCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean): IDisposable {
public registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable {
if (id.final === 't' && !id.prefix && !id.intermediates) {
// security: always check whether window option is allowed
return this._parser.registerCsiHandler(id, params => {
@@ -659,23 +737,23 @@ export class InputHandler extends Disposable implements IInputHandler {
}
/**
* Forward addDcsHandler from parser.
* Forward registerDcsHandler from parser.
*/
public addDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean): IDisposable {
public registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable {
return this._parser.registerDcsHandler(id, new DcsHandler(callback));
}
/**
* Forward addEscHandler from parser.
* Forward registerEscHandler from parser.
*/
public addEscHandler(id: IFunctionIdentifier, callback: () => boolean): IDisposable {
public registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable {
return this._parser.registerEscHandler(id, callback);
}
/**
* Forward addOscHandler from parser.
* Forward registerOscHandler from parser.
*/
public addOscHandler(ident: number, callback: (data: string) => boolean): IDisposable {
public registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable {
return this._parser.registerOscHandler(ident, new OscHandler(callback));
}
+2 -1
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider } from 'common/services/Services';
import { IBufferService, ICoreService, ILogService, IOptionsService, ITerminalOptions, IPartialTerminalOptions, IDirtyRowService, ICoreMouseService, ICharsetService, IUnicodeService, IUnicodeVersionProvider, LogLevelEnum } from 'common/services/Services';
import { IEvent, EventEmitter } from 'common/EventEmitter';
import { clone } from 'common/Clone';
import { DEFAULT_OPTIONS } from 'common/services/OptionsService';
@@ -92,6 +92,7 @@ export class MockDirtyRowService implements IDirtyRowService {
export class MockLogService implements ILogService {
public serviceBrand: any;
public logLevel = LogLevelEnum.DEBUG;
public debug(message: any, ...optionalParams: any[]): void {}
public info(message: any, ...optionalParams: any[]): void {}
public warn(message: any, ...optionalParams: any[]): void {}
+14 -2
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ITerminalOptions as IPublicTerminalOptions } from 'xterm';
import { IFunctionIdentifier, ITerminalOptions as IPublicTerminalOptions } from 'xterm';
import { IEvent, IEventEmitter } from 'common/EventEmitter';
import { IDeleteEvent, IInsertEvent } from 'common/CircularList';
import { IParams } from 'common/parser/Types';
@@ -349,8 +349,12 @@ export interface IInputHandler {
onTitleChange: IEvent<string>;
onRequestScroll: IEvent<IAttributeData, boolean | void>;
parse(data: string | Uint8Array): void;
parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise<boolean>;
print(data: Uint32Array, start: number, end: number): void;
registerCsiHandler(id: IFunctionIdentifier, callback: (params: IParams) => boolean | Promise<boolean>): IDisposable;
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: IParams) => boolean | Promise<boolean>): IDisposable;
registerEscHandler(id: IFunctionIdentifier, callback: () => boolean | Promise<boolean>): IDisposable;
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
/** C0 BEL */ bell(): void;
/** C0 LF */ lineFeed(): void;
@@ -427,3 +431,11 @@ export interface IInputHandler {
ESC ~ */ setgLevel(level: number): void;
/** ESC # 8 */ screenAlignmentPattern(): void;
}
interface IParseStack {
paused: boolean;
cursorStartX: number;
cursorStartY: number;
decodedLength: number;
position: number;
}
+98 -8
View File
@@ -31,14 +31,23 @@ const WRITE_TIMEOUT_MS = 12;
*/
const WRITE_BUFFER_LENGTH_THRESHOLD = 50;
// queueMicrotask polyfill for nodejs < v11
const qmt: (cb: () => void) => void = (typeof queueMicrotask === 'undefined')
? (cb: () => void) => { Promise.resolve().then(cb); }
: queueMicrotask;
export class WriteBuffer {
private _writeBuffer: (string | Uint8Array)[] = [];
private _callbacks: ((() => void) | undefined)[] = [];
private _pendingData = 0;
private _bufferOffset = 0;
constructor(private _action: (data: string | Uint8Array) => void) { }
constructor(private _action: (data: string | Uint8Array, promiseResult?: boolean) => void | Promise<boolean>) { }
/**
* @deprecated Unreliable, to be removed soon.
*/
public writeSync(data: string | Uint8Array): void {
// force sync processing on pending data chunks to avoid in-band data scrambling
// does the same as innerWrite but without event loop
@@ -76,16 +85,97 @@ export class WriteBuffer {
this._callbacks.push(callback);
}
protected _innerWrite(): void {
const startTime = Date.now();
/**
* Inner write call, that enters the sliced chunk processing by timing.
*
* `lastTime` indicates, when the last _innerWrite call had started.
* It is used to aggregate async handler execution under a timeout constraint
* effectively lowering the redrawing needs, schematically:
*
* macroTask _innerWrite:
* if (Date.now() - (lastTime | 0) < WRITE_TIMEOUT_MS):
* schedule microTask _innerWrite(lastTime)
* else:
* schedule macroTask _innerWrite(0)
*
* overall execution order on task queues:
*
* macrotasks: [...] --> _innerWrite(0) --> [...] --> screenUpdate --> [...]
* m t: |
* i a: [...]
* c s: |
* r k: while < timeout:
* o s: _innerWrite(timeout)
*
* `promiseResult` depicts the promise resolve value of an async handler.
* This value gets carried forward through all saved stack states of the
* paused parser for proper continuation.
*
* Note, for pure sync code `lastTime` and `promiseResult` have no meaning.
*/
protected _innerWrite(lastTime: number = 0, promiseResult: boolean = true): void {
const startTime = lastTime || Date.now();
while (this._writeBuffer.length > this._bufferOffset) {
const data = this._writeBuffer[this._bufferOffset];
const cb = this._callbacks[this._bufferOffset];
this._bufferOffset++;
const result = this._action(data, promiseResult);
if (result) {
/**
* If we get a promise as return value, we re-schedule the continuation
* as thenable on the promise and exit right away.
*
* The exit here means, that we block input processing at the current active chunk,
* the exact execution position within the chunk is preserved by the saved
* stack content in InputHandler and EscapeSequenceParser.
*
* Resuming happens automatically from that saved stack state.
* Also the resolved promise value is passed along the callstack to
* `EscapeSequenceParser.parse` to correctly resume the stopped handler loop.
*
* Exceptions on async handlers will be logged to console async, but do not interrupt
* the input processing (continues with next handler at the current input position).
*/
this._action(data);
this._pendingData -= data.length;
/**
* If a promise takes long to resolve, we should schedule continuation behind setTimeout.
* This might already be too late, if our .then enters really late (executor + prev thens took very long).
* This cannot be solved here for the handler itself (it is the handlers responsibility to slice hard work),
* but we can at least schedule a screen update as we gain control.
*/
const continuation: (r: boolean) => void = (r: boolean) => Date.now() - startTime >= WRITE_TIMEOUT_MS
? setTimeout(() => this._innerWrite(0, r))
: this._innerWrite(startTime, r);
/**
* Optimization considerations:
* The continuation above favors FPS over throughput by eval'ing `startTime` on resolve.
* This might schedule too many screen updates with bad throughput drops (in case a slow
* resolving handler sliced its work properly behind setTimeout calls). We cannot spot
* this condition here, also the renderer has no way to spot nonsense updates either.
* FIXME: A proper fix for this would track the FPS at the renderer entry level separately.
*
* If favoring of FPS shows bad throughtput impact, use the following instead. It favors
* throughput by eval'ing `startTime` upfront pulling at least one more chunk into the
* current microtask queue (executed before setTimeout).
*/
// const continuation: (r: boolean) => void = Date.now() - startTime >= WRITE_TIMEOUT_MS
// ? r => setTimeout(() => this._innerWrite(0, r))
// : r => this._innerWrite(startTime, r);
// Handle exceptions synchronously to current band position, idea:
// 1. spawn a single microtask which we allow to throw hard
// 2. spawn a promise immediately resolving to `true`
// (executed on the same queue, thus properly aligned before continuation happens)
result.catch(err => {
qmt(() => {throw err;});
return Promise.resolve(false);
}).then(continuation);
return;
}
const cb = this._callbacks[this._bufferOffset];
if (cb) cb();
this._bufferOffset++;
this._pendingData -= data.length;
if (Date.now() - startTime >= WRITE_TIMEOUT_MS) {
break;
@@ -99,7 +189,7 @@ export class WriteBuffer {
this._callbacks = this._callbacks.slice(this._bufferOffset);
this._bufferOffset = 0;
}
setTimeout(() => this._innerWrite(), 0);
setTimeout(() => this._innerWrite());
} else {
this._writeBuffer = [];
this._callbacks = [];
+205
View File
@@ -252,3 +252,208 @@ describe('DcsParser', () => {
});
});
});
class TestHandlerAsync implements IDcsHandler {
constructor(public output: any[], public msg: string, public returnFalse: boolean = false) {}
public hook(params: IParams): void {
this.output.push([this.msg, 'HOOK', params.toArray()]);
}
public put(data: Uint32Array, start: number, end: number): void {
this.output.push([this.msg, 'PUT', utf32ToString(data, start, end)]);
}
public async unhook(success: boolean): Promise<boolean> {
// simple sleep to check in tests whether ordering gets messed up
await new Promise(res => setTimeout(res, 20));
this.output.push([this.msg, 'UNHOOK', success]);
if (this.returnFalse) {
return false;
}
return true;
}
}
async function unhookP(parser: DcsParser, success: boolean): Promise<void> {
let result: void | Promise<boolean>;
let prev: boolean | undefined;
while (result = parser.unhook(success, prev)) {
prev = await result;
}
}
describe('DcsParser - async tests', () => {
let parser: DcsParser;
let reports: any[] = [];
beforeEach(() => {
reports = [];
parser = new DcsParser();
parser.setHandlerFallback((id, action, data) => {
if (action === 'HOOK') {
data = data.toArray();
}
reports.push([id, action, data]);
});
});
describe('sync and async mixed', () => {
describe('sync | async | sync', () => {
it('first should run, cleanup action for others', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', false));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 'HOOK', [1, 2, 3]],
['a1', 'HOOK', [1, 2, 3]],
['s1', 'HOOK', [1, 2, 3]],
['s2', 'PUT', 'Here comes'],
['a1', 'PUT', 'Here comes'],
['s1', 'PUT', 'Here comes'],
['s2', 'PUT', 'the mouse!'],
['a1', 'PUT', 'the mouse!'],
['s1', 'PUT', 'the mouse!'],
['s2', 'UNHOOK', true],
['a1', 'UNHOOK', false], // important: a1 before s1
['s1', 'UNHOOK', false]
]);
});
it('all should run', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's2', true));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 'HOOK', [1, 2, 3]],
['a1', 'HOOK', [1, 2, 3]],
['s1', 'HOOK', [1, 2, 3]],
['s2', 'PUT', 'Here comes'],
['a1', 'PUT', 'Here comes'],
['s1', 'PUT', 'Here comes'],
['s2', 'PUT', 'the mouse!'],
['a1', 'PUT', 'the mouse!'],
['s1', 'PUT', 'the mouse!'],
['s2', 'UNHOOK', true],
['a1', 'UNHOOK', true], // important: a1 before s1
['s1', 'UNHOOK', true]
]);
});
});
describe('async | sync | async', () => {
it('first should run, cleanup action for others', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', false));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', false));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', false));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['a2', 'HOOK', [1, 2, 3]],
['s1', 'HOOK', [1, 2, 3]],
['a1', 'HOOK', [1, 2, 3]],
['a2', 'PUT', 'Here comes'],
['s1', 'PUT', 'Here comes'],
['a1', 'PUT', 'Here comes'],
['a2', 'PUT', 'the mouse!'],
['s1', 'PUT', 'the mouse!'],
['a1', 'PUT', 'the mouse!'],
['a2', 'UNHOOK', true],
['s1', 'UNHOOK', false], // important: s1 between a2 .. a1
['a1', 'UNHOOK', false]
]);
});
it('all should run', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a1', true));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandler(reports, 's1', true));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new TestHandlerAsync(reports, 'a2', true));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['a2', 'HOOK', [1, 2, 3]],
['s1', 'HOOK', [1, 2, 3]],
['a1', 'HOOK', [1, 2, 3]],
['a2', 'PUT', 'Here comes'],
['s1', 'PUT', 'Here comes'],
['a1', 'PUT', 'Here comes'],
['a2', 'PUT', 'the mouse!'],
['s1', 'PUT', 'the mouse!'],
['a1', 'PUT', 'the mouse!'],
['a2', 'UNHOOK', true],
['s1', 'UNHOOK', true], // important: s1 between a2 .. a1
['a1', 'UNHOOK', true]
]);
});
});
describe('DcsHandlerFactory', () => {
it('should be called once on end(true)', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [[[1, 2, 3], 'Here comes the mouse!']]);
});
it('should not be called on end(false)', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push([params.toArray(), data]); return true; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, false);
assert.deepEqual(reports, []);
});
it('should be disposable', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
const dispo = parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return true; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!']]);
dispo.dispose();
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
data = toUtf32('some other');
parser.put(data, 0, data.length);
data = toUtf32(' data');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'some other data']]);
});
it('should respect return false', async () => {
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['one', params.toArray(), data]); return true; }));
parser.registerHandler(identifier({intermediates: '+', final: 'p'}), new DcsHandler(async (data, params) => { reports.push(['two', params.toArray(), data]); return false; }));
parser.hook(identifier({intermediates: '+', final: 'p'}), Params.fromArray([1, 2, 3]));
let data = toUtf32('Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await unhookP(parser, true);
assert.deepEqual(reports, [['two', [1, 2, 3], 'Here comes the mouse!'], ['one', [1, 2, 3], 'Here comes the mouse!']]);
});
});
});
});
+56 -15
View File
@@ -4,7 +4,7 @@
*/
import { IDisposable } from 'common/Types';
import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType } from 'common/parser/Types';
import { IDcsHandler, IParams, IHandlerCollection, IDcsParser, DcsFallbackHandlerType, ISubParserStackState } from 'common/parser/Types';
import { utf32ToString } from 'common/input/TextDecoder';
import { Params } from 'common/parser/Params';
import { PAYLOAD_LIMIT } from 'common/parser/Constants';
@@ -15,11 +15,16 @@ export class DcsParser implements IDcsParser {
private _handlers: IHandlerCollection<IDcsHandler> = Object.create(null);
private _active: IDcsHandler[] = EMPTY_HANDLERS;
private _ident: number = 0;
private _handlerFb: DcsFallbackHandlerType = () => {};
private _handlerFb: DcsFallbackHandlerType = () => { };
private _stack: ISubParserStackState = {
paused: false,
loopPosition: 0,
fallThrough: false
};
public dispose(): void {
this._handlers = Object.create(null);
this._handlerFb = () => {};
this._handlerFb = () => { };
this._active = EMPTY_HANDLERS;
}
@@ -48,9 +53,13 @@ export class DcsParser implements IDcsParser {
}
public reset(): void {
// force cleanup leftover handlers
if (this._active.length) {
this.unhook(false);
for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {
this._active[j].unhook(false);
}
}
this._stack.paused = false;
this._active = EMPTY_HANDLERS;
this._ident = 0;
}
@@ -79,20 +88,42 @@ export class DcsParser implements IDcsParser {
}
}
public unhook(success: boolean): void {
public unhook(success: boolean, promiseResult: boolean = true): void | Promise<boolean> {
if (!this._active.length) {
this._handlerFb(this._ident, 'UNHOOK', success);
} else {
let handlerResult: boolean | Promise<boolean> = false;
let j = this._active.length - 1;
for (; j >= 0; j--) {
if (this._active[j].unhook(success)) {
break;
}
let fallThrough = false;
if (this._stack.paused) {
j = this._stack.loopPosition - 1;
handlerResult = promiseResult;
fallThrough = this._stack.fallThrough;
this._stack.paused = false;
}
j--;
// cleanup left over handlers
if (!fallThrough && handlerResult === false) {
for (; j >= 0; j--) {
handlerResult = this._active[j].unhook(success);
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = false;
return handlerResult;
}
}
j--;
}
// cleanup left over handlers (fallThrough for async)
for (; j >= 0; j--) {
this._active[j].unhook(false);
handlerResult = this._active[j].unhook(false);
if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = true;
return handlerResult;
}
}
}
this._active = EMPTY_HANDLERS;
@@ -113,7 +144,7 @@ export class DcsHandler implements IDcsHandler {
private _params: IParams = EMPTY_PARAMS;
private _hitLimit: boolean = false;
constructor(private _handler: (data: string, params: IParams) => boolean) {}
constructor(private _handler: (data: string, params: IParams) => boolean | Promise<boolean>) { }
public hook(params: IParams): void {
// since we need to preserve params until `unhook`, we have to clone it
@@ -136,12 +167,22 @@ export class DcsHandler implements IDcsHandler {
}
}
public unhook(success: boolean): boolean {
let ret = false;
public unhook(success: boolean): boolean | Promise<boolean> {
let ret: boolean | Promise<boolean> = false;
if (this._hitLimit) {
ret = false;
} else if (success) {
ret = this._handler(this._data, this._params);
if (ret instanceof Promise) {
// need to hold data and params until `ret` got resolved
// dont care for errors, data will be freed anyway on next start
return ret.then(res => {
this._params = EMPTY_PARAMS;
this._data = '';
this._hitLimit = false;
return res;
});
}
}
this._params = EMPTY_PARAMS;
this._data = '';
+639 -99
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType, IFunctionIdentifier } from 'common/parser/Types';
import { IParsingState, IParams, ParamsArray, IOscParser, IOscHandler, OscFallbackHandlerType, IFunctionIdentifier, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types';
import { EscapeSequenceParser, TransitionTable, VT500_TRANSITION_TABLE } from 'common/parser/EscapeSequenceParser';
import { assert } from 'chai';
import { StringToUtf32, stringFromCodePoint, utf32ToString } from 'common/input/TextDecoder';
@@ -24,7 +24,7 @@ function r(a: number, b: number): string[] {
}
class MockOscPutParser implements IOscParser {
private _fallback: OscFallbackHandlerType = () => {};
private _fallback: OscFallbackHandlerType = () => { };
public data = '';
public reset(): void {
this.data = '';
@@ -92,6 +92,21 @@ class TestEscapeSequenceParser extends EscapeSequenceParser {
public identifier(id: IFunctionIdentifier): number {
return this._identifier(id);
}
public get parseStack(): IParserStackState {
return this._parseStack;
}
private _trackStack = false;
public trackStackSavesOnPause(): void {
this._trackStack = true;
}
public trackedStack: IParserStackState[] = [];
public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean> {
const result = super.parse(data, length, promiseResult);
if (result instanceof Promise && this._trackStack) {
this.trackedStack.push({ ...this.parseStack });
}
return result;
}
}
// test object to collect parser actions and compare them with expected values
@@ -1206,71 +1221,71 @@ describe('EscapeSequenceParser', () => {
assert.equal(print, '');
});
it('ESC handler', () => {
parser2.registerEscHandler({intermediates: '%', final: 'G'}, function (): boolean {
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, function (): boolean {
esc.push('%G');
return true;
});
parser2.registerEscHandler({final: 'E'}, function (): boolean {
parser2.registerEscHandler({ final: 'E' }, function (): boolean {
esc.push('E');
return true;
});
parse(parser2, INPUT);
assert.deepEqual(esc, ['%G', 'E']);
parser2.clearEscHandler({intermediates: '%', final: 'G'});
parser2.clearEscHandler({intermediates: '%', final: 'G'}); // should not throw
parser2.clearEscHandler({ intermediates: '%', final: 'G' });
parser2.clearEscHandler({ intermediates: '%', final: 'G' }); // should not throw
clearAccu();
parse(parser2, INPUT);
assert.deepEqual(esc, ['E']);
parser2.clearEscHandler({final: 'E'});
parser2.clearEscHandler({ final: 'E' });
clearAccu();
parse(parser2, INPUT);
assert.deepEqual(esc, []);
});
describe('ESC custom handlers', () => {
it('prevent fallback', () => {
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; });
parse(parser2, INPUT);
assert.deepEqual(esc, ['custom - %G']);
});
it('allow fallback', () => {
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return false; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return false; });
parse(parser2, INPUT);
assert.deepEqual(esc, ['custom - %G', 'default - %G']);
});
it('Multiple custom handlers fallback once', () => {
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return false; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom2 - %G'); return false; });
parse(parser2, INPUT);
assert.deepEqual(esc, ['custom2 - %G', 'custom - %G']);
});
it('Multiple custom handlers no fallback', () => {
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom2 - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom2 - %G'); return true; });
parse(parser2, INPUT);
assert.deepEqual(esc, ['custom2 - %G']);
});
it('Execution order should go from latest handler down to the original', () => {
const order: number[] = [];
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { order.push(1); return true; });
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { order.push(2); return false; });
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { order.push(3); return false; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { order.push(1); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { order.push(2); return false; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { order.push(3); return false; });
parse(parser2, '\x1b%G');
assert.deepEqual(order, [3, 2, 1]);
});
it('Dispose should work', () => {
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
const dispo = parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
const dispo = parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; });
dispo.dispose();
parse(parser2, INPUT);
assert.deepEqual(esc, ['default - %G']);
});
it('Should not corrupt the parser when dispose is called twice', () => {
parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('default - %G'); return true; });
const dispo = parser2.registerEscHandler({intermediates: '%', final: 'G'}, () => { esc.push('custom - %G'); return true; });
parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('default - %G'); return true; });
const dispo = parser2.registerEscHandler({ intermediates: '%', final: 'G' }, () => { esc.push('custom - %G'); return true; });
dispo.dispose();
dispo.dispose();
parse(parser2, INPUT);
@@ -1278,14 +1293,14 @@ describe('EscapeSequenceParser', () => {
});
});
it('CSI handler', () => {
parser2.registerCsiHandler({final: 'm'}, function (params: IParams): boolean {
parser2.registerCsiHandler({ final: 'm' }, function (params: IParams): boolean {
csi.push(['m', params.toArray(), '']);
return true;
});
parse(parser2, INPUT);
assert.deepEqual(csi, [['m', [1, 31], ''], ['m', [0], '']]);
parser2.clearCsiHandler({final: 'm'});
parser2.clearCsiHandler({final: 'm'}); // should not throw
parser2.clearCsiHandler({ final: 'm' });
parser2.clearCsiHandler({ final: 'm' }); // should not throw
clearAccu();
parse(parser2, INPUT);
assert.deepEqual(csi, []);
@@ -1293,16 +1308,16 @@ describe('EscapeSequenceParser', () => {
describe('CSI custom handlers', () => {
it('Prevent fallback', () => {
const csiCustom: [string, ParamsArray, string][] = [];
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parse(parser2, INPUT);
assert.deepEqual(csi, [], 'Should not fallback to original handler');
assert.deepEqual(csiCustom, [['m', [1, 31], ''], ['m', [0], '']]);
});
it('Allow fallback', () => {
const csiCustom: [string, ParamsArray, string][] = [];
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return false; });
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return false; });
parse(parser2, INPUT);
assert.deepEqual(csi, [['m', [1, 31], ''], ['m', [0], '']], 'Should fallback to original handler');
assert.deepEqual(csiCustom, [['m', [1, 31], ''], ['m', [0], '']]);
@@ -1310,9 +1325,9 @@ describe('EscapeSequenceParser', () => {
it('Multiple custom handlers fallback once', () => {
const csiCustom: [string, ParamsArray, string][] = [];
const csiCustom2: [string, ParamsArray, string][] = [];
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return false; });
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom2.push(['m', params.toArray(), '']); return false; });
parse(parser2, INPUT);
assert.deepEqual(csi, [], 'Should not fallback to original handler');
assert.deepEqual(csiCustom, [['m', [1, 31], ''], ['m', [0], '']]);
@@ -1321,9 +1336,9 @@ describe('EscapeSequenceParser', () => {
it('Multiple custom handlers no fallback', () => {
const csiCustom: [string, ParamsArray, string][] = [];
const csiCustom2: [string, ParamsArray, string][] = [];
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({final: 'm'}, params => { csiCustom2.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom2.push(['m', params.toArray(), '']); return true; });
parse(parser2, INPUT);
assert.deepEqual(csi, [], 'Should not fallback to original handler');
assert.deepEqual(csiCustom, [], 'Should not fallback once');
@@ -1331,16 +1346,16 @@ describe('EscapeSequenceParser', () => {
});
it('Execution order should go from latest handler down to the original', () => {
const order: number[] = [];
parser2.registerCsiHandler({final: 'm'}, () => { order.push(1); return true; });
parser2.registerCsiHandler({final: 'm'}, () => { order.push(2); return false; });
parser2.registerCsiHandler({final: 'm'}, () => { order.push(3); return false; });
parser2.registerCsiHandler({ final: 'm' }, () => { order.push(1); return true; });
parser2.registerCsiHandler({ final: 'm' }, () => { order.push(2); return false; });
parser2.registerCsiHandler({ final: 'm' }, () => { order.push(3); return false; });
parse(parser2, '\x1b[0m');
assert.deepEqual(order, [3, 2, 1]);
});
it('Dispose should work', () => {
const csiCustom: [string, ParamsArray, string][] = [];
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
const customHandler = parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
const customHandler = parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
customHandler.dispose();
parse(parser2, INPUT);
assert.deepEqual(csi, [['m', [1, 31], ''], ['m', [0], '']]);
@@ -1348,8 +1363,8 @@ describe('EscapeSequenceParser', () => {
});
it('Should not corrupt the parser when dispose is called twice', () => {
const csiCustom: [string, ParamsArray, string][] = [];
parser2.registerCsiHandler({final: 'm'}, params => { csi.push(['m', params.toArray(), '']); return true; });
const customHandler = parser2.registerCsiHandler({final: 'm'}, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
parser2.registerCsiHandler({ final: 'm' }, params => { csi.push(['m', params.toArray(), '']); return true; });
const customHandler = parser2.registerCsiHandler({ final: 'm' }, params => { csiCustom.push(['m', params.toArray(), '']); return true; });
customHandler.dispose();
customHandler.dispose();
parse(parser2, INPUT);
@@ -1455,7 +1470,7 @@ describe('EscapeSequenceParser', () => {
});
});
it('DCS handler', () => {
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, {
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, {
hook: function (params: IParams): void {
dcs.push(['hook', '', params.toArray(), 0]);
},
@@ -1478,8 +1493,8 @@ describe('EscapeSequenceParser', () => {
['put', 'abc'], ['put', ';de'],
['unhook']
]);
parser2.clearDcsHandler({intermediates: '+', final: 'p'});
parser2.clearDcsHandler({intermediates: '+', final: 'p'}); // should not throw
parser2.clearDcsHandler({ intermediates: '+', final: 'p' });
parser2.clearDcsHandler({ intermediates: '+', final: 'p' }); // should not throw
clearAccu();
parse(parser2, '\x1bP1;2;3+pabc');
parse(parser2, ';de\x9c');
@@ -1489,54 +1504,54 @@ describe('EscapeSequenceParser', () => {
const DCS_INPUT = '\x1bP1;2;3+pabc\x1b\\';
it('Prevent fallback', () => {
const dcsCustom: [string, (number | number[])[], string][] = [];
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
parse(parser2, DCS_INPUT);
assert.deepEqual(dcsCustom, [['B', [1, 2, 3], 'abc']]);
});
it('Allow fallback', () => {
const dcsCustom: [string, (number | number[])[], string][] = [];
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return false; }));
parse(parser2, DCS_INPUT);
assert.deepEqual(dcsCustom, [['B', [1, 2, 3], 'abc'], ['A', [1, 2, 3], 'abc']]);
});
it('Multiple custom handlers fallback once', () => {
const dcsCustom: [string, (number | number[])[], string][] = [];
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return false; }));
parse(parser2, DCS_INPUT);
assert.deepEqual(dcsCustom, [['C', [1, 2, 3], 'abc'], ['B', [1, 2, 3], 'abc']]);
});
it('Multiple custom handlers no fallback', () => {
const dcsCustom: [string, (number | number[])[], string][] = [];
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['C', params.toArray(), data]); return true; }));
parse(parser2, DCS_INPUT);
assert.deepEqual(dcsCustom, [['C', [1, 2, 3], 'abc']]);
});
it('Execution order should go from latest handler down to the original', () => {
const order: number[] = [];
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(1); return true; }));
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(2); return false; }));
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler(() => { order.push(3); return false; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler(() => { order.push(1); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler(() => { order.push(2); return false; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler(() => { order.push(3); return false; }));
parse(parser2, DCS_INPUT);
assert.deepEqual(order, [3, 2, 1]);
});
it('Dispose should work', () => {
const dcsCustom: [string, (number | number[])[], string][] = [];
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
const dispo = parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
const dispo = parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
dispo.dispose();
parse(parser2, DCS_INPUT);
assert.deepEqual(dcsCustom, [['A', [1, 2, 3], 'abc']]);
});
it('Should not corrupt the parser when dispose is called twice', () => {
const dcsCustom: [string, (number | number[])[], string][] = [];
parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
const dispo = parser2.registerDcsHandler({intermediates: '+', final: 'p'}, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['A', params.toArray(), data]); return true; }));
const dispo = parser2.registerDcsHandler({ intermediates: '+', final: 'p' }, new DcsHandler((data, params) => { dcsCustom.push(['B', params.toArray(), data]); return true; }));
dispo.dispose();
dispo.dispose();
parse(parser2, DCS_INPUT);
@@ -1570,54 +1585,54 @@ describe('EscapeSequenceParser', () => {
it('prefix range 0x3c .. 0x3f, one byte', () => {
for (let i = 0x3c; i <= 0x3f; ++i) {
const c = String.fromCharCode(i);
assert.equal(parser.identToString(parser.identifier({prefix: c, final: 'z'})), c + 'z');
assert.equal(parser.identToString(parser.identifier({ prefix: c, final: 'z' })), c + 'z');
}
assert.throws(() => { parser.identifier({prefix: '\x3b', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f');
assert.throws(() => { parser.identifier({prefix: '\x40', final: 'z'}); }, 'prefix must be in range 0x3c .. 0x3f');
assert.throws(() => { parser.identifier({prefix: '??', final: 'z'}); }, 'only one byte as prefix supported');
assert.throws(() => { parser.identifier({ prefix: '\x3b', final: 'z' }); }, 'prefix must be in range 0x3c .. 0x3f');
assert.throws(() => { parser.identifier({ prefix: '\x40', final: 'z' }); }, 'prefix must be in range 0x3c .. 0x3f');
assert.throws(() => { parser.identifier({ prefix: '??', final: 'z' }); }, 'only one byte as prefix supported');
});
it('intermediates range 0x20 .. 0x2f, up to two bytes', () => {
for (let i = 0x20; i <= 0x2f; ++i) {
const c = String.fromCharCode(i);
assert.equal(parser.identToString(parser.identifier({intermediates: c + c, final: 'z'})), c + c + 'z');
assert.equal(parser.identToString(parser.identifier({ intermediates: c + c, final: 'z' })), c + c + 'z');
}
assert.throws(() => { parser.identifier({intermediates: '\x1f', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f');
assert.throws(() => { parser.identifier({intermediates: '\x30', final: 'z'}); }, 'intermediate must be in range 0x20 .. 0x2f');
assert.throws(() => { parser.identifier({intermediates: '!!!', final: 'z'}); }, 'only two bytes as intermediates are supported');
assert.throws(() => { parser.identifier({ intermediates: '\x1f', final: 'z' }); }, 'intermediate must be in range 0x20 .. 0x2f');
assert.throws(() => { parser.identifier({ intermediates: '\x30', final: 'z' }); }, 'intermediate must be in range 0x20 .. 0x2f');
assert.throws(() => { parser.identifier({ intermediates: '!!!', final: 'z' }); }, 'only two bytes as intermediates are supported');
});
it('final CSI/DCS range 0x40 .. 0x7e (default), one byte', () => {
for (let i = 0x40; i <= 0x7e; ++i) {
const c = String.fromCharCode(i);
assert.equal(parser.identToString(parser.identifier({final: c})), c);
assert.equal(parser.identToString(parser.identifier({ final: c })), c);
}
assert.throws(() => { parser.identifier({final: '\x3f'}); }, 'final must be in range 64 .. 126');
assert.throws(() => { parser.identifier({final: '\x7f'}); }, 'final must be in range 64 .. 126');
assert.throws(() => { parser.identifier({final: 'zz'}); }, 'final must be a single byte');
assert.throws(() => { parser.identifier({ final: '\x3f' }); }, 'final must be in range 64 .. 126');
assert.throws(() => { parser.identifier({ final: '\x7f' }); }, 'final must be in range 64 .. 126');
assert.throws(() => { parser.identifier({ final: 'zz' }); }, 'final must be a single byte');
});
it('final ESC range 0x30 .. 0x7e, one byte', () => {
for (let i = 0x30; i <= 0x7e; ++i) {
const final = String.fromCharCode(i);
let handler: IDisposable | undefined;
assert.doesNotThrow(() => { handler = parser.registerEscHandler({final}, () => true); }, 'final must be in range 48 .. 126');
assert.doesNotThrow(() => { handler = parser.registerEscHandler({ final }, () => true); }, 'final must be in range 48 .. 126');
if (handler) handler.dispose();
}
assert.throws(() => { parser.registerEscHandler({final: '\x2f'}, () => true); }, 'final must be in range 48 .. 126');
assert.throws(() => { parser.registerEscHandler({final: '\x7f'}, () => true); }, 'final must be in range 48 .. 126');
assert.throws(() => { parser.registerEscHandler({ final: '\x2f' }, () => true); }, 'final must be in range 48 .. 126');
assert.throws(() => { parser.registerEscHandler({ final: '\x7f' }, () => true); }, 'final must be in range 48 .. 126');
});
it('id calculation - should stacking prefix -> intermediate -> final', () => {
assert.equal(parser.identToString(parser.identifier({final: 'z'})), 'z');
assert.equal(parser.identToString(parser.identifier({prefix: '?', final: 'z'})), '?z');
assert.equal(parser.identToString(parser.identifier({intermediates: '!', final: 'z'})), '!z');
assert.equal(parser.identToString(parser.identifier({prefix: '?', intermediates: '!', final: 'z'})), '?!z');
assert.equal(parser.identToString(parser.identifier({prefix: '?', intermediates: '!!', final: 'z'})), '?!!z');
assert.equal(parser.identToString(parser.identifier({ final: 'z' })), 'z');
assert.equal(parser.identToString(parser.identifier({ prefix: '?', final: 'z' })), '?z');
assert.equal(parser.identToString(parser.identifier({ intermediates: '!', final: 'z' })), '!z');
assert.equal(parser.identToString(parser.identifier({ prefix: '?', intermediates: '!', final: 'z' })), '?!z');
assert.equal(parser.identToString(parser.identifier({ prefix: '?', intermediates: '!!', final: 'z' })), '?!!z');
});
});
describe('identifier invocation', () => {
it('ESC', () => {
const callstack: string[] = [];
const h1 = parser.registerEscHandler({final: 'z'}, () => { callstack.push('z'); return true; });
const h2 = parser.registerEscHandler({intermediates: '!', final: 'z'}, () => { callstack.push('!z'); return true; });
const h3 = parser.registerEscHandler({intermediates: '!!', final: 'z'}, () => { callstack.push('!!z'); return true; });
const h1 = parser.registerEscHandler({ final: 'z' }, () => { callstack.push('z'); return true; });
const h2 = parser.registerEscHandler({ intermediates: '!', final: 'z' }, () => { callstack.push('!z'); return true; });
const h3 = parser.registerEscHandler({ intermediates: '!!', final: 'z' }, () => { callstack.push('!!z'); return true; });
parse(parser, '\x1bz\x1b!z\x1b!!z');
h1.dispose();
h2.dispose();
@@ -1627,12 +1642,12 @@ describe('EscapeSequenceParser', () => {
});
it('CSI', () => {
const callstack: any[] = [];
const h1 = parser.registerCsiHandler({final: 'z'}, params => { callstack.push(['z', params.toArray()]); return true; });
const h2 = parser.registerCsiHandler({intermediates: '!', final: 'z'}, params => { callstack.push(['!z', params.toArray()]); return true; });
const h3 = parser.registerCsiHandler({intermediates: '!!', final: 'z'}, params => { callstack.push(['!!z', params.toArray()]); return true; });
const h4 = parser.registerCsiHandler({prefix: '?', final: 'z'}, params => { callstack.push(['?z', params.toArray()]); return true; });
const h5 = parser.registerCsiHandler({prefix: '?', intermediates: '!', final: 'z'}, params => { callstack.push(['?!z', params.toArray()]); return true; });
const h6 = parser.registerCsiHandler({prefix: '?', intermediates: '!!', final: 'z'}, params => { callstack.push(['?!!z', params.toArray()]); return true; });
const h1 = parser.registerCsiHandler({ final: 'z' }, params => { callstack.push(['z', params.toArray()]); return true; });
const h2 = parser.registerCsiHandler({ intermediates: '!', final: 'z' }, params => { callstack.push(['!z', params.toArray()]); return true; });
const h3 = parser.registerCsiHandler({ intermediates: '!!', final: 'z' }, params => { callstack.push(['!!z', params.toArray()]); return true; });
const h4 = parser.registerCsiHandler({ prefix: '?', final: 'z' }, params => { callstack.push(['?z', params.toArray()]); return true; });
const h5 = parser.registerCsiHandler({ prefix: '?', intermediates: '!', final: 'z' }, params => { callstack.push(['?!z', params.toArray()]); return true; });
const h6 = parser.registerCsiHandler({ prefix: '?', intermediates: '!!', final: 'z' }, params => { callstack.push(['?!!z', params.toArray()]); return true; });
parse(parser, '\x1b[1;z\x1b[1;!z\x1b[1;!!z\x1b[?1;z\x1b[?1;!z\x1b[?1;!!z');
h1.dispose();
h2.dispose();
@@ -1648,12 +1663,12 @@ describe('EscapeSequenceParser', () => {
});
it('DCS', () => {
const callstack: any[] = [];
const h1 = parser.registerDcsHandler({final: 'z'}, new DcsHandler((data, params) => { callstack.push(['z', params.toArray(), data]); return true; }));
const h2 = parser.registerDcsHandler({intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!z', params.toArray(), data]); return true; }));
const h3 = parser.registerDcsHandler({intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['!!z', params.toArray(), data]); return true; }));
const h4 = parser.registerDcsHandler({prefix: '?', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?z', params.toArray(), data]); return true; }));
const h5 = parser.registerDcsHandler({prefix: '?', intermediates: '!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!z', params.toArray(), data]); return true; }));
const h6 = parser.registerDcsHandler({prefix: '?', intermediates: '!!', final: 'z'}, new DcsHandler((data, params) => { callstack.push(['?!!z', params.toArray(), data]); return true; }));
const h1 = parser.registerDcsHandler({ final: 'z' }, new DcsHandler((data, params) => { callstack.push(['z', params.toArray(), data]); return true; }));
const h2 = parser.registerDcsHandler({ intermediates: '!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['!z', params.toArray(), data]); return true; }));
const h3 = parser.registerDcsHandler({ intermediates: '!!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['!!z', params.toArray(), data]); return true; }));
const h4 = parser.registerDcsHandler({ prefix: '?', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['?z', params.toArray(), data]); return true; }));
const h5 = parser.registerDcsHandler({ prefix: '?', intermediates: '!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['?!z', params.toArray(), data]); return true; }));
const h6 = parser.registerDcsHandler({ prefix: '?', intermediates: '!!', final: 'z' }, new DcsHandler((data, params) => { callstack.push(['?!!z', params.toArray(), data]); return true; }));
parse(parser, '\x1bP1;zAB\x1b\\\x1bP1;!zAB\x1b\\\x1bP1;!!zAB\x1b\\\x1bP?1;zAB\x1b\\\x1bP?1;!zAB\x1b\\\x1bP?1;!!zAB\x1b\\');
h1.dispose();
h2.dispose();
@@ -1678,3 +1693,528 @@ describe('EscapeSequenceParser', () => {
});
// TODO: error conditions and error recovery (not implemented yet in parser)
});
/**
* async handler tests.
*/
function parseSync(parser: TestEscapeSequenceParser, data: string): void | Promise<boolean> {
const container = new Uint32Array(data.length);
const decoder = new StringToUtf32();
return parser.parse(container, decoder.decode(data, container));
}
async function parseP(parser: TestEscapeSequenceParser, data: string): Promise<void> {
const container = new Uint32Array(data.length);
const decoder = new StringToUtf32();
const len = decoder.decode(data, container);
let result: void | Promise<boolean>;
let prev: boolean | undefined;
while (result = parser.parse(container, len, prev)) {
prev = await result;
}
}
function evalStackSaves(stackSaves: IParserStackState[], data: [number, ParserStackType, number][]): void {
assert.equal(stackSaves.length, data.length);
for (let i = 0; i < data.length; ++i) {
assert.equal(stackSaves[i].chunkPos, data[i][0]);
assert.equal(stackSaves[i].state, data[i][1]);
assert.equal(stackSaves[i].handlerPos, data[i][2]);
}
}
// helper similiar to assert.throws for async functions
async function throwsAsync(fn: () => Promise<any>, message?: string | undefined): Promise<void> {
let msg: string | undefined;
try {
await fn();
} catch (e) {
if (e instanceof Error) {
msg = e.message;
} else if (typeof e === 'string') {
msg = e;
}
if (typeof message === 'string') {
assert.equal(msg, message);
}
return;
}
assert.throws(fn, message);
}
describe('EscapeSequenceParser - async', () => {
// sequences: SGR 1;31 | hello SP | ESC %G | wor | ESC E | ld! | SGR 0 | EXE \r\n | $> | DCS 1;2 a [xyz] ST | OSC 1;foo=bar ST | FIN
// needed handlers: CSI m, PRINT, ESC %G, ESC E, EXE \r, EXE \n, OSC 1
const INPUT = '\x1b[1;31mhello \x1b%Gwor\x1bEld!\x1b[0m\r\n$>\x1bP1;2axyz\x1b\\\x1b]1;foo=bar\x1b\\FIN';
let RESULT: any[];
let parser: TestEscapeSequenceParser;
const callstack: any[] = [];
function clearAccu(): void {
callstack.length = 0;
parser.trackedStack.length = 0;
}
beforeEach(() => {
RESULT = [
['SGR', [1, 31]],
['PRINT', 'hello '],
['ESC %G'],
['PRINT', 'wor'],
['ESC E'],
['PRINT', 'ld!'],
['SGR', [0]],
['EXE \r'],
['EXE \n'],
['PRINT', '$>'],
['DCS a', ['xyz', [1, 2]]],
['OSC 1', 'foo=bar'],
['PRINT', 'FIN']
];
parser = new TestEscapeSequenceParser();
parser.reset();
parser.trackStackSavesOnPause();
clearAccu();
});
describe('sync handlers should behave as before', () => {
beforeEach(() => {
parser.setPrintHandler((data, start, end) => {
let result = '';
for (let i = start; i < end; ++i) {
result += stringFromCodePoint(data[i]);
}
callstack.push(['PRINT', result]);
});
parser.registerCsiHandler({ final: 'm' }, params => { callstack.push(['SGR', params.toArray()]); return true; });
parser.registerEscHandler({ intermediates: '%', final: 'G' }, () => { callstack.push(['ESC %G']); return true; });
parser.registerEscHandler({ final: 'E' }, () => { callstack.push(['ESC E']); return true; });
parser.setExecuteHandler('\r', () => { callstack.push(['EXE \r']); return true; });
parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; });
parser.registerOscHandler(1, new OscHandler(data => { callstack.push(['OSC 1', data]); return true; }));
parser.registerDcsHandler({final: 'a'}, new DcsHandler((data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true;}));
});
it('sync handlers keep being parsed in sync mode', () => {
// note: if we have only sync handlers, a parse call should never return anything
assert.equal(!parseSync(parser, INPUT), true);
assert.equal(parser.parseStack.state, ParserStackType.NONE); // not paused
assert.equal(parser.trackedStack.length, 0); // never got paused
});
it('correct result on sync parse call', () => {
parseSync(parser, INPUT);
assert.deepEqual(callstack, RESULT);
assert.equal(parser.trackedStack.length, 0);
});
it('correct result on async parse call', async () => {
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT);
assert.equal(parser.trackedStack.length, 0);
});
});
describe('async handlers', () => {
beforeEach(() => {
parser.setPrintHandler((data, start, end) => {
let result = '';
for (let i = start; i < end; ++i) {
result += stringFromCodePoint(data[i]);
}
callstack.push(['PRINT', result]);
});
parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['SGR', params.toArray()]); return true; });
parser.registerEscHandler({ intermediates: '%', final: 'G' }, async () => { callstack.push(['ESC %G']); return true; });
parser.registerEscHandler({ final: 'E' }, async () => { callstack.push(['ESC E']); return true; });
parser.setExecuteHandler('\r', () => { callstack.push(['EXE \r']); return true; });
parser.setExecuteHandler('\n', () => { callstack.push(['EXE \n']); return true; });
parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['OSC 1', data]); return true; }));
parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['DCS a', [data, params.toArray()]]); return true;}));
});
it('sync parse call does not work anymore', () => {
assert.notEqual(!parseSync(parser, INPUT), true);
assert.notDeepEqual(callstack, RESULT);
// due to sync calling we should save exactly one saved stack
// proper continuation is not possible anymore, as we lost the promise resolve value
assert.equal(parser.trackedStack.length, 1);
});
it('improper continuation should throw', async () => {
/**
* Explanation:
* The first sync call will stop at the first promise returned,
* but does not await its resolve value.
* The second sync call to parse will fail due to missing `promiseResult`,
* which is needed for correct continuation.
*/
assert.notEqual(!parseSync(parser, INPUT), true);
assert.notDeepEqual(callstack, RESULT);
assert.throws(() => parseSync(parser, INPUT), 'improper continuation due to previous async handler, giving up parsing');
// keeps being broken for further parse calls (sync and async)
assert.throws(() => parseSync(parser, 'random'), 'improper continuation due to previous async handler, giving up parsing');
await throwsAsync(() => parseP(parser, 'foobar'), 'improper continuation due to previous async handler, giving up parsing');
// reset should lift the error condition
parser.reset();
await parseP(parser, INPUT); // does not throw anymore
});
it('correct result on awaited parse call', async () => {
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT);
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
});
it('correct result on chunked awaited parse calls', async () => {
RESULT = [
['SGR', [1, 31]],
['PRINT', 'h'], // due to single char input PRINT is split
['PRINT', 'e'],
['PRINT', 'l'],
['PRINT', 'l'],
['PRINT', 'o'],
['PRINT', ' '],
['ESC %G'],
['PRINT', 'w'],
['PRINT', 'o'],
['PRINT', 'r'],
['ESC E'],
['PRINT', 'l'],
['PRINT', 'd'],
['PRINT', '!'],
['SGR', [0]],
['EXE \r'],
['EXE \n'],
['PRINT', '$'],
['PRINT', '>'],
['DCS a', ['xyz', [1, 2]]],
['OSC 1', 'foo=bar'],
['PRINT', 'F'],
['PRINT', 'I'],
['PRINT', 'N']
];
// split to single char input
for (let i = 0; i < INPUT.length; ++i) {
// Note: a single fully awaited parse call always ends in sync mode,
// which re-enables faster sync processing in the higher up callstack
await parseP(parser, INPUT[i]);
}
assert.deepEqual(callstack, RESULT);
evalStackSaves(parser.trackedStack, [
[0, ParserStackType.CSI, 0],
[0, ParserStackType.ESC, 0],
[0, ParserStackType.ESC, 0],
[0, ParserStackType.CSI, 0],
[0, ParserStackType.DCS, 0],
[0, ParserStackType.OSC, 0]
]);
});
it('multiple async SGR handlers', async () => {
// register with fallback
const SGR2 = parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['2# SGR', params.toArray()]); return false; });
await parseP(parser, INPUT);
// should contain [2# SGR, SGR] call pairs
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '2# SGR') assert.equal(callstack[i + 1][0], 'SGR', 'Should fallback to original handler');
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 1],
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 1],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// after dispose we should be back to RESULT
SGR2.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// register without fallback
const SGR22 = parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['2# SGR', params.toArray()]); return true; });
await parseP(parser, INPUT);
// should only contain 2# SGR
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '2# SGR') assert.notEqual(callstack[i + 1][0], 'SGR', 'Should not fallback to original handler');
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 1],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 1],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// after dispose we should be back to RESULT
SGR22.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
});
it('multiple async ESC handlers', async () => {
// register with fallback
const ESC2 = parser.registerEscHandler({ final: 'E' }, async () => { callstack.push(['2# ESC E']); return false; });
await parseP(parser, INPUT);
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '2# ESC E') assert.equal(callstack[i + 1][0], 'ESC E', 'Should fallback to original handler');
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 1],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// after dispose we should be back to RESULT
ESC2.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// register without fallback
const ESC22 = parser.registerEscHandler({ final: 'E' }, async () => { callstack.push(['2# ESC E']); return true; });
await parseP(parser, INPUT);
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '2# ESC E') assert.notEqual(callstack[i + 1][0], 'ESC E', 'Should not fallback to original handler');
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 1],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// after dispose we should be back to RESULT
ESC22.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
});
it('sync/async SGR mixed', async () => {
// sync with fallback
const SGR2 = parser.registerCsiHandler({ final: 'm' }, params => { callstack.push(['2# SGR', params.toArray()]); return false; });
// async with fallback
const SGR3 = parser.registerCsiHandler({ final: 'm' }, async params => { callstack.push(['3# SGR', params.toArray()]); return false; });
await parseP(parser, INPUT);
// should contain [3# SGR, 2# SGR, SGR] call triples
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '3# SGR') {
assert.equal(callstack[i + 1][0], '2# SGR', 'Should fallback to next handler');
assert.equal(callstack[i + 2][0], 'SGR', 'Should fallback to original handler');
}
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 2],
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 2],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// dispose SGR2 (sync one)
SGR2.dispose();
await parseP(parser, INPUT);
// should contain [3# SGR, SGR] call pairs
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '3# SGR') {
assert.equal(callstack[i + 1][0], 'SGR', 'Should fallback to original handler');
}
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 1],
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 1],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// dispose SGR3 (async one)
SGR3.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
});
it('multiple async OSC handlers', async () => {
// register with fallback
const OSC2 = parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['2# OSC 1', data]); return false; }));
await parseP(parser, INPUT);
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '2# OSC 1') assert.equal(callstack[i + 1][0], 'OSC 1', 'Should fallback to original handler');
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// after dispose we should be back to RESULT
OSC2.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// register without fallback
const OSC22 = parser.registerOscHandler(1, new OscHandler(async data => { callstack.push(['2# OSC 1', data]); return true; }));
await parseP(parser, INPUT);
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '2# OSC 1') assert.notEqual(callstack[i + 1][0], 'OSC 1', 'Should fallback to original handler');
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// after dispose we should be back to RESULT
OSC22.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
});
it('multiple async DCS handlers', async () => {
// register with fallback
const DCS2 = parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return false;}));
await parseP(parser, INPUT);
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '2# DCS a') assert.equal(callstack[i + 1][0], 'DCS a', 'Should fallback to original handler');
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// after dispose we should be back to RESULT
DCS2.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// register without fallback
const DCS22 = parser.registerDcsHandler({final: 'a'}, new DcsHandler(async (data, params) => { callstack.push(['#2 DCS a', [data, params.toArray()]]); return true;}));
await parseP(parser, INPUT);
for (let i = 0; i < callstack.length; ++i) {
const entry = callstack[i];
if (entry[0] === '2# DCS a') assert.notEqual(callstack[i + 1][0], 'DCS a', 'Should fallback to original handler');
}
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
// after dispose we should be back to RESULT
DCS22.dispose();
await parseP(parser, INPUT);
assert.deepEqual(callstack, RESULT, 'Should not call custom handler');
evalStackSaves(parser.trackedStack, [
[6, ParserStackType.CSI, 0],
[15, ParserStackType.ESC, 0],
[20, ParserStackType.ESC, 0],
[27, ParserStackType.CSI, 0],
[41, ParserStackType.DCS, 0],
[54, ParserStackType.OSC, 0]
]);
clearAccu();
});
});
});
+219 -49
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType } from 'common/parser/Types';
import { IParsingState, IDcsHandler, IEscapeSequenceParser, IParams, IOscHandler, IHandlerCollection, CsiHandlerType, OscFallbackHandlerType, IOscParser, EscHandlerType, IDcsParser, DcsFallbackHandlerType, IFunctionIdentifier, ExecuteFallbackHandlerType, CsiFallbackHandlerType, EscFallbackHandlerType, PrintHandlerType, PrintFallbackHandlerType, ExecuteHandlerType, IParserStackState, ParserStackType, ResumableHandlersType } from 'common/parser/Types';
import { ParserState, ParserAction } from 'common/parser/Constants';
import { Disposable } from 'common/Lifecycle';
import { IDisposable } from 'common/Types';
@@ -239,7 +239,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
// handler lookup containers
protected _printHandler: PrintHandlerType;
protected _executeHandlers: {[flag: number]: ExecuteHandlerType};
protected _executeHandlers: { [flag: number]: ExecuteHandlerType };
protected _csiHandlers: IHandlerCollection<CsiHandlerType>;
protected _escHandlers: IHandlerCollection<EscHandlerType>;
protected _oscParser: IOscParser;
@@ -253,6 +253,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
protected _escHandlerFb: EscFallbackHandlerType;
protected _errorHandlerFb: (state: IParsingState) => IParsingState;
// parser stack save for async handler support
protected _parseStack: IParserStackState = {
state: ParserStackType.NONE,
handlers: [],
handlerPos: 0,
transition: 0,
chunkPos: 0
};
constructor(
protected readonly _transitions: TransitionTable = VT500_TRANSITION_TABLE
) {
@@ -280,7 +289,7 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._errorHandler = this._errorHandlerFb;
// swallow 7bit ST (ESC+\)
this.registerEscHandler({final: '\\'}, () => true);
this.registerEscHandler({ final: '\\' }, () => true);
}
protected _identifier(id: IFunctionIdentifier, finalRange: number[] = [0x40, 0x7e]): number {
@@ -427,6 +436,15 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._errorHandler = this._errorHandlerFb;
}
/**
* Reset parser to initial values.
*
* This can also be used to lift the improper continuation error condition
* when dealing with async handlers. Use this only as a last resort to silence
* that error when the terminal has no pending data to be processed. Note that
* the interrupted async handler might continue its work in the future messing
* up the terminal state even further.
*/
public reset(): void {
this.currentState = this.initialState;
this._oscParser.reset();
@@ -435,9 +453,31 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
this._params.addParam(0); // ZDM
this._collect = 0;
this.precedingCodepoint = 0;
// abort pending continuation from async handler
// Here the RESET type indicates, that the next parse call will
// ignore any saved stack, instead continues sync with next codepoint from GROUND
if (this._parseStack.state !== ParserStackType.NONE) {
this._parseStack.state = ParserStackType.RESET;
this._parseStack.handlers = []; // also release handlers ref
}
}
/**
* Async parse support.
*/
protected _preserveStack(
state: ParserStackType,
handlers: ResumableHandlersType,
handlerPos: number,
transition: number,
chunkPos: number
): void {
this._parseStack.state = state;
this._parseStack.handlers = handlers;
this._parseStack.handlerPos = handlerPos;
this._parseStack.transition = transition;
this._parseStack.chunkPos = chunkPos;
}
/**
* Parse UTF32 codepoints in `data` up to `length`.
@@ -452,23 +492,141 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
* - DCS_PARAM:PARAM
* - OSC_STRING:OSC_PUT
* - DCS_PASSTHROUGH:DCS_PUT
*
* Note on asynchronous handler support:
* Any handler returning a promise will be treated as asynchronous.
* To keep the in-band blocking working for async handlers, `parse` pauses execution,
* creates a stack save and returns the promise to the caller.
* For proper continuation of the paused state it is important
* to await the promise resolving. On resolve the parse must be repeated
* with the same chunk of data and the resolved value in `promiseResult`
* until no promise is returned.
*
* Important: With only sync handlers defined, parsing is completely synchronous as well.
* As soon as an async handler is involved, synchronous parsing is not possible anymore.
*
* Boilerplate for proper parsing of multiple chunks with async handlers:
*
* ```typescript
* async function parseMultipleChunks(chunks: Uint32Array[]): Promise<void> {
* for (const chunk of chunks) {
* let result: void | Promise<boolean>;
* let prev: boolean | undefined;
* while (result = parser.parse(chunk, chunk.length, prev)) {
* prev = await result;
* }
* }
* // finished parsing all chunks...
* }
* ```
*/
public parse(data: Uint32Array, length: number): void {
public parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean> {
let code = 0;
let transition = 0;
let currentState = this.currentState;
const osc = this._oscParser;
const dcs = this._dcsParser;
let collect = this._collect;
const params = this._params;
const table: Uint8Array = this._transitions.table;
let start = 0;
let handlerResult: void | boolean | Promise<boolean>;
// resume from async handler
if (this._parseStack.state) {
// allow sync parser reset even in continuation mode
// Note: can be used to recover parser from improper continuation error below
if (this._parseStack.state === ParserStackType.RESET) {
this._parseStack.state = ParserStackType.NONE;
start = this._parseStack.chunkPos + 1; // continue with next codepoint in GROUND
} else {
if (promiseResult === undefined || this._parseStack.state === ParserStackType.FAIL) {
/**
* Reject further parsing on improper continuation after pausing.
* This is a really bad condition with screwed up execution order and prolly messed up
* terminal state, therefore we exit hard with an exception and reject any further parsing.
*
* Note: With `Terminal.write` usage this exception should never occur, as the top level
* calls are guaranteed to handle async conditions properly. If you ever encounter this
* exception in your terminal integration it indicates, that you injected data chunks to
* `InputHandler.parse` or `EscapeSequenceParser.parse` synchronously without waiting for
* continuation of a running async handler.
*
* It is possible to get rid of this error by calling `reset`. But dont rely on that,
* as the pending async handler still might mess up the terminal later. Instead fix the faulty
* async handling, so this error will not be thrown anymore.
*/
this._parseStack.state = ParserStackType.FAIL;
throw new Error('improper continuation due to previous async handler, giving up parsing');
}
// we have to resume the old handler loop if:
// - return value of the promise was `false`
// - handlers are not exhausted yet
const handlers = this._parseStack.handlers;
let handlerPos = this._parseStack.handlerPos - 1;
switch (this._parseStack.state) {
case ParserStackType.CSI:
if (promiseResult === false && handlerPos > -1) {
for (; handlerPos >= 0; handlerPos--) {
handlerResult = (handlers as CsiHandlerType[])[handlerPos](this._params);
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._parseStack.handlerPos = handlerPos;
return handlerResult;
}
}
}
this._parseStack.handlers = [];
break;
case ParserStackType.ESC:
if (promiseResult === false && handlerPos > -1) {
for (; handlerPos >= 0; handlerPos--) {
handlerResult = (handlers as EscHandlerType[])[handlerPos]();
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._parseStack.handlerPos = handlerPos;
return handlerResult;
}
}
}
this._parseStack.handlers = [];
break;
case ParserStackType.DCS:
code = data[this._parseStack.chunkPos];
handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a, promiseResult);
if (handlerResult) {
return handlerResult;
}
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
this._params.reset();
this._params.addParam(0); // ZDM
this._collect = 0;
break;
case ParserStackType.OSC:
code = data[this._parseStack.chunkPos];
handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a, promiseResult);
if (handlerResult) {
return handlerResult;
}
if (code === 0x1b) this._parseStack.transition |= ParserState.ESCAPE;
this._params.reset();
this._params.addParam(0); // ZDM
this._collect = 0;
break;
}
// cleanup before continuing with the main sync loop
this._parseStack.state = ParserStackType.NONE;
start = this._parseStack.chunkPos + 1;
this.precedingCodepoint = 0;
this.currentState = this._parseStack.transition & TableAccess.TRANSITION_STATE_MASK;
}
}
// continue with main sync loop
// process input string
for (let i = 0; i < length; ++i) {
for (let i = start; i < length; ++i) {
code = data[i];
// normal transition & action lookup
transition = table[currentState << TableAccess.INDEX_STATE_SHIFT | (code < 0xa0 ? code : NON_ASCII_PRINTABLE)];
transition = this._transitions.table[this.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
@@ -508,9 +666,9 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
{
position: i,
code,
currentState,
collect,
params,
currentState: this.currentState,
collect: this._collect,
params: this._params,
abort: false
});
if (inject.abort) return;
@@ -518,16 +676,21 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
break;
case ParserAction.CSI_DISPATCH:
// Trigger CSI Handler
const handlers = this._csiHandlers[collect << 8 | code];
const handlers = this._csiHandlers[this._collect << 8 | code];
let j = handlers ? handlers.length - 1 : -1;
for (; j >= 0; j--) {
// true means success and to stop bubbling
if (handlers[j](params)) {
// a promise indicates an async handler that needs to finish before progressing
handlerResult = handlers[j](this._params);
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._preserveStack(ParserStackType.CSI, handlers, j, transition, i);
return handlerResult;
}
}
if (j < 0) {
this._csiHandlerFb(collect << 8 | code, params);
this._csiHandlerFb(this._collect << 8 | code, this._params);
}
this.precedingCodepoint = 0;
break;
@@ -536,91 +699,98 @@ export class EscapeSequenceParser extends Disposable implements IEscapeSequenceP
do {
switch (code) {
case 0x3b:
params.addParam(0); // ZDM
this._params.addParam(0); // ZDM
break;
case 0x3a:
params.addSubParam(-1);
this._params.addSubParam(-1);
break;
default: // 0x30 - 0x39
params.addDigit(code - 48);
this._params.addDigit(code - 48);
}
} while (++i < length && (code = data[i]) > 0x2f && code < 0x3c);
i--;
break;
case ParserAction.COLLECT:
collect <<= 8;
collect |= code;
this._collect <<= 8;
this._collect |= code;
break;
case ParserAction.ESC_DISPATCH:
const handlersEsc = this._escHandlers[collect << 8 | code];
const handlersEsc = this._escHandlers[this._collect << 8 | code];
let jj = handlersEsc ? handlersEsc.length - 1 : -1;
for (; jj >= 0; jj--) {
// true means success and to stop bubbling
if (handlersEsc[jj]()) {
// a promise indicates an async handler that needs to finish before progressing
handlerResult = handlersEsc[jj]();
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._preserveStack(ParserStackType.ESC, handlersEsc, jj, transition, i);
return handlerResult;
}
}
if (jj < 0) {
this._escHandlerFb(collect << 8 | code);
this._escHandlerFb(this._collect << 8 | code);
}
this.precedingCodepoint = 0;
break;
case ParserAction.CLEAR:
params.reset();
params.addParam(0); // ZDM
collect = 0;
this._params.reset();
this._params.addParam(0); // ZDM
this._collect = 0;
break;
case ParserAction.DCS_HOOK:
dcs.hook(collect << 8 | code, params);
this._dcsParser.hook(this._collect << 8 | code, this._params);
break;
case ParserAction.DCS_PUT:
// inner loop - exit DCS_PUT: 0x18, 0x1a, 0x1b, 0x7f, 0x80 - 0x9f
// unhook triggered by: 0x1b, 0x9c (success) and 0x18, 0x1a (abort)
for (let j = i + 1; ; ++j) {
if (j >= length || (code = data[j]) === 0x18 || code === 0x1a || code === 0x1b || (code > 0x7f && code < NON_ASCII_PRINTABLE)) {
dcs.put(data, i, j);
this._dcsParser.put(data, i, j);
i = j - 1;
break;
}
}
break;
case ParserAction.DCS_UNHOOK:
dcs.unhook(code !== 0x18 && code !== 0x1a);
handlerResult = this._dcsParser.unhook(code !== 0x18 && code !== 0x1a);
if (handlerResult) {
this._preserveStack(ParserStackType.DCS, [], 0, transition, i);
return handlerResult;
}
if (code === 0x1b) transition |= ParserState.ESCAPE;
params.reset();
params.addParam(0); // ZDM
collect = 0;
this._params.reset();
this._params.addParam(0); // ZDM
this._collect = 0;
this.precedingCodepoint = 0;
break;
case ParserAction.OSC_START:
osc.start();
this._oscParser.start();
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 < NON_ASCII_PRINTABLE)) {
osc.put(data, i, j);
this._oscParser.put(data, i, j);
i = j - 1;
break;
}
}
break;
case ParserAction.OSC_END:
osc.end(code !== 0x18 && code !== 0x1a);
handlerResult = this._oscParser.end(code !== 0x18 && code !== 0x1a);
if (handlerResult) {
this._preserveStack(ParserStackType.OSC, [], 0, transition, i);
return handlerResult;
}
if (code === 0x1b) transition |= ParserState.ESCAPE;
params.reset();
params.addParam(0); // ZDM
collect = 0;
this._params.reset();
this._params.addParam(0); // ZDM
this._collect = 0;
this.precedingCodepoint = 0;
break;
}
currentState = transition & TableAccess.TRANSITION_STATE_MASK;
this.currentState = transition & TableAccess.TRANSITION_STATE_MASK;
}
// save collected intermediates
this._collect = collect;
// save state
this.currentState = currentState;
}
}
+201
View File
@@ -250,3 +250,204 @@ describe('OscParser', () => {
});
});
});
class TestHandlerAsync implements IOscHandler {
constructor(public id: number, public output: any[], public msg: string, public returnFalse: boolean = false) {}
public start(): void {
this.output.push([this.msg, this.id, 'START']);
}
public put(data: Uint32Array, start: number, end: number): void {
this.output.push([this.msg, this.id, 'PUT', utf32ToString(data, start, end)]);
}
public async end(success: boolean): Promise<boolean> {
await new Promise(res => setTimeout(res, 20));
this.output.push([this.msg, this.id, 'END', success]);
if (this.returnFalse) {
return false;
}
return true;
}
}
async function endP(parser: OscParser, success: boolean): Promise<void> {
let result: void | Promise<boolean>;
let prev: boolean | undefined;
while (result = parser.end(success, prev)) {
prev = await result;
}
}
describe('OscParser - async tests', () => {
let parser: OscParser;
let reports: any[] = [];
beforeEach(() => {
reports = [];
parser = new OscParser();
parser.setHandlerFallback((id, action, data) => {
reports.push([id, action, data]);
});
});
describe('sync and async mixed', () => {
describe('sync | async | sync', () => {
it('first should run, cleanup action for others', async () => {
parser.registerHandler(1234, new TestHandler(1234, reports, 's1'));
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 'a1'));
parser.registerHandler(1234, new TestHandler(1234, reports, 's2'));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 1234, 'START'],
['a1', 1234, 'START'],
['s1', 1234, 'START'],
['s2', 1234, 'PUT', 'Here comes'],
['a1', 1234, 'PUT', 'Here comes'],
['s1', 1234, 'PUT', 'Here comes'],
['s2', 1234, 'PUT', 'the mouse!'],
['a1', 1234, 'PUT', 'the mouse!'],
['s1', 1234, 'PUT', 'the mouse!'],
['s2', 1234, 'END', true],
['a1', 1234, 'END', false],
['s1', 1234, 'END', false]
]);
});
it('all should run', async () => {
parser.registerHandler(1234, new TestHandler(1234, reports, 's1', true));
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 'a1', true));
parser.registerHandler(1234, new TestHandler(1234, reports, 's2', true));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 1234, 'START'],
['a1', 1234, 'START'],
['s1', 1234, 'START'],
['s2', 1234, 'PUT', 'Here comes'],
['a1', 1234, 'PUT', 'Here comes'],
['s1', 1234, 'PUT', 'Here comes'],
['s2', 1234, 'PUT', 'the mouse!'],
['a1', 1234, 'PUT', 'the mouse!'],
['s1', 1234, 'PUT', 'the mouse!'],
['s2', 1234, 'END', true],
['a1', 1234, 'END', true],
['s1', 1234, 'END', true]
]);
});
});
describe('async | sync | async', () => {
it('first should run, cleanup action for others', async () => {
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's1'));
parser.registerHandler(1234, new TestHandler(1234, reports, 'a1'));
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's2'));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 1234, 'START'],
['a1', 1234, 'START'],
['s1', 1234, 'START'],
['s2', 1234, 'PUT', 'Here comes'],
['a1', 1234, 'PUT', 'Here comes'],
['s1', 1234, 'PUT', 'Here comes'],
['s2', 1234, 'PUT', 'the mouse!'],
['a1', 1234, 'PUT', 'the mouse!'],
['s1', 1234, 'PUT', 'the mouse!'],
['s2', 1234, 'END', true],
['a1', 1234, 'END', false],
['s1', 1234, 'END', false]
]);
});
it('all should run', async () => {
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's1', true));
parser.registerHandler(1234, new TestHandler(1234, reports, 'a1', true));
parser.registerHandler(1234, new TestHandlerAsync(1234, reports, 's2', true));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32('the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [
// messages from TestHandler
['s2', 1234, 'START'],
['a1', 1234, 'START'],
['s1', 1234, 'START'],
['s2', 1234, 'PUT', 'Here comes'],
['a1', 1234, 'PUT', 'Here comes'],
['s1', 1234, 'PUT', 'Here comes'],
['s2', 1234, 'PUT', 'the mouse!'],
['a1', 1234, 'PUT', 'the mouse!'],
['s1', 1234, 'PUT', 'the mouse!'],
['s2', 1234, 'END', true],
['a1', 1234, 'END', true],
['s1', 1234, 'END', true]
]);
});
});
describe('OscHandlerFactory', () => {
it('should be called once on end(true)', async () => {
parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
parser.end(true);
await endP(parser, true);
assert.deepEqual(reports, [[1234, 'Here comes the mouse!']]);
});
it('should not be called on end(false)', async () => {
parser.registerHandler(1234, new OscHandler(async data => { reports.push([1234, data]); return true; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await endP(parser, false);
assert.deepEqual(reports, []);
});
it('should be disposable', async () => {
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; }));
const dispo = parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return true; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!']]);
dispo.dispose();
parser.start();
data = toUtf32('1234;some other');
parser.put(data, 0, data.length);
data = toUtf32(' data');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'some other data']]);
});
it('should respect return false', async () => {
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['one', data]); return true; }));
parser.registerHandler(1234, new OscHandler(async data => { reports.push(['two', data]); return false; }));
parser.start();
let data = toUtf32('1234;Here comes');
parser.put(data, 0, data.length);
data = toUtf32(' the mouse!');
parser.put(data, 0, data.length);
await endP(parser, true);
assert.deepEqual(reports, [['two', 'Here comes the mouse!'], ['one', 'Here comes the mouse!']]);
});
});
});
});
+66 -30
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser } from 'common/parser/Types';
import { IOscHandler, IHandlerCollection, OscFallbackHandlerType, IOscParser, ISubParserStackState } from 'common/parser/Types';
import { OscState, PAYLOAD_LIMIT } from 'common/parser/Constants';
import { utf32ToString } from 'common/input/TextDecoder';
import { IDisposable } from 'common/Types';
@@ -16,6 +16,11 @@ export class OscParser implements IOscParser {
private _id = -1;
private _handlers: IHandlerCollection<IOscHandler> = Object.create(null);
private _handlerFb: OscFallbackHandlerType = () => { };
private _stack: ISubParserStackState = {
paused: false,
loopPosition: 0,
fallThrough: false
};
public registerHandler(ident: number, handler: IOscHandler): IDisposable {
if (this._handlers[ident] === undefined) {
@@ -41,15 +46,18 @@ export class OscParser implements IOscParser {
public dispose(): void {
this._handlers = Object.create(null);
this._handlerFb = () => {};
this._handlerFb = () => { };
this._active = EMPTY_HANDLERS;
}
public reset(): void {
// cleanup handlers if payload was already sent
// force cleanup handlers if payload was already sent
if (this._state === OscState.PAYLOAD) {
this.end(false);
for (let j = this._stack.paused ? this._stack.loopPosition - 1 : this._active.length - 1; j >= 0; --j) {
this._active[j].end(false);
}
}
this._stack.paused = false;
this._active = EMPTY_HANDLERS;
this._id = -1;
this._state = OscState.START;
@@ -76,27 +84,6 @@ export class OscParser implements IOscParser {
}
}
private _end(success: boolean): void {
// other than the old code we always have to call .end
// to keep the bubbling we use `success` to indicate
// whether a handler should execute
if (!this._active.length) {
this._handlerFb(this._id, 'END', success);
} else {
let j = this._active.length - 1;
for (; j >= 0; j--) {
if (this._active[j].end(success)) {
break;
}
}
j--;
// cleanup left over handlers
for (; j >= 0; j--) {
this._active[j].end(false);
}
}
}
public start(): void {
// always reset leftover handlers
this.reset();
@@ -142,7 +129,7 @@ export class OscParser implements IOscParser {
* Whether the OSC got aborted or finished normally
* is indicated by `success`.
*/
public end(success: boolean): void {
public end(success: boolean, promiseResult: boolean = true): void | Promise<boolean> {
if (this._state === OscState.START) {
return;
}
@@ -154,7 +141,47 @@ export class OscParser implements IOscParser {
if (this._state === OscState.ID) {
this._start();
}
this._end(success);
if (!this._active.length) {
this._handlerFb(this._id, 'END', success);
} else {
let handlerResult: boolean | Promise<boolean> = false;
let j = this._active.length - 1;
let fallThrough = false;
if (this._stack.paused) {
j = this._stack.loopPosition - 1;
handlerResult = promiseResult;
fallThrough = this._stack.fallThrough;
this._stack.paused = false;
}
if (!fallThrough && handlerResult === false) {
for (; j >= 0; j--) {
handlerResult = this._active[j].end(success);
if (handlerResult === true) {
break;
} else if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = false;
return handlerResult;
}
}
j--;
}
// cleanup left over handlers
// we always have to call .end for proper cleanup,
// here we use `success` to indicate whether a handler should execute
for (; j >= 0; j--) {
handlerResult = this._active[j].end(false);
if (handlerResult instanceof Promise) {
this._stack.paused = true;
this._stack.loopPosition = j;
this._stack.fallThrough = true;
return handlerResult;
}
}
}
}
this._active = EMPTY_HANDLERS;
this._id = -1;
@@ -170,7 +197,7 @@ export class OscHandler implements IOscHandler {
private _data = '';
private _hitLimit: boolean = false;
constructor(private _handler: (data: string) => boolean) {}
constructor(private _handler: (data: string) => boolean | Promise<boolean>) { }
public start(): void {
this._data = '';
@@ -188,12 +215,21 @@ export class OscHandler implements IOscHandler {
}
}
public end(success: boolean): boolean {
let ret = false;
public end(success: boolean): boolean | Promise<boolean> {
let ret: boolean | Promise<boolean> = false;
if (this._hitLimit) {
ret = false;
} else if (success) {
ret = this._handler(this._data);
if (ret instanceof Promise) {
// need to hold data until `ret` got resolved
// dont care for errors, data will be freed anyway on next start
return ret.then(res => {
this._data = '';
this._hitLimit = false;
return res;
});
}
}
this._data = '';
this._hitLimit = false;
+42 -7
View File
@@ -6,6 +6,7 @@
import { IDisposable } from 'common/Types';
import { ParserState } from 'common/parser/Constants';
/** sequence params serialized to js arrays */
export type ParamsArray = (number | number[])[];
@@ -69,7 +70,7 @@ export interface IParsingState {
* CSI handler types.
* Note: `params` is borrowed.
*/
export type CsiHandlerType = (params: IParams) => boolean;
export type CsiHandlerType = (params: IParams) => boolean | Promise<boolean>;
export type CsiFallbackHandlerType = (ident: number, params: IParams) => void;
/**
@@ -93,14 +94,14 @@ export interface IDcsHandler {
* execution of the command should depend on `success`.
* To save memory also cleanup data structures here.
*/
unhook(success: boolean): boolean;
unhook(success: boolean): boolean | Promise<boolean>;
}
export type DcsFallbackHandlerType = (ident: number, action: 'HOOK' | 'PUT' | 'UNHOOK', payload?: any) => void;
/**
* ESC handler types.
*/
export type EscHandlerType = () => boolean;
export type EscHandlerType = () => boolean | Promise<boolean>;
export type EscFallbackHandlerType = (identifier: number) => void;
/**
@@ -129,7 +130,7 @@ export interface IOscHandler {
* execution of the command should depend on `success`.
* To save memory also cleanup data structures here.
*/
end(success: boolean): boolean;
end(success: boolean): boolean | Promise<boolean>;
}
export type OscFallbackHandlerType = (ident: number, action: 'START' | 'PUT' | 'END', payload?: any) => void;
@@ -160,7 +161,7 @@ export interface IEscapeSequenceParser extends IDisposable {
* Parse UTF32 codepoints in `data` up to `length`.
* @param data The data to parse.
*/
parse(data: Uint32Array, length: number): void;
parse(data: Uint32Array, length: number, promiseResult?: boolean): void | Promise<boolean>;
/**
* Get string from numercial function identifier `ident`.
@@ -213,12 +214,12 @@ export interface ISubParser<T, U> extends IDisposable {
export interface IOscParser extends ISubParser<IOscHandler, OscFallbackHandlerType> {
start(): void;
end(success: boolean): void;
end(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
}
export interface IDcsParser extends ISubParser<IDcsHandler, DcsFallbackHandlerType> {
hook(ident: number, params: IParams): void;
unhook(success: boolean): void;
unhook(success: boolean, promiseResult?: boolean): void | Promise<boolean>;
}
/**
@@ -237,3 +238,37 @@ export interface IFunctionIdentifier {
export interface IHandlerCollection<T> {
[key: string]: T[];
}
/**
* Types for async parser support.
*/
// type of saved stack state in parser
export const enum ParserStackType {
NONE = 0,
FAIL,
RESET,
CSI,
ESC,
OSC,
DCS
}
// aggregate of resumable handler lists
export type ResumableHandlersType = CsiHandlerType[] | EscHandlerType[];
// saved stack state of the parser
export interface IParserStackState {
state: ParserStackType;
handlers: ResumableHandlersType;
handlerPos: number;
transition: number;
chunkPos: number;
}
// saved stack state of subparser (OSC and DCS)
export interface ISubParserStackState {
paused: boolean;
loopPosition: number;
fallThrough: boolean;
}
+13 -22
View File
@@ -3,7 +3,7 @@
* @license MIT
*/
import { ILogService, IOptionsService } from 'common/services/Services';
import { ILogService, IOptionsService, LogLevelEnum } from 'common/services/Services';
type LogType = (message?: any, ...optionalParams: any[]) => void;
@@ -19,21 +19,12 @@ interface IConsole {
// module doesn't depend on them so we need to explicitly declare it.
declare const console: IConsole;
export enum LogLevel {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
OFF = 4
}
const optionsKeyToLogLevel: { [key: string]: LogLevel } = {
debug: LogLevel.DEBUG,
info: LogLevel.INFO,
warn: LogLevel.WARN,
error: LogLevel.ERROR,
off: LogLevel.OFF
const optionsKeyToLogLevel: { [key: string]: LogLevelEnum } = {
debug: LogLevelEnum.DEBUG,
info: LogLevelEnum.INFO,
warn: LogLevelEnum.WARN,
error: LogLevelEnum.ERROR,
off: LogLevelEnum.OFF
};
const LOG_PREFIX = 'xterm.js: ';
@@ -41,7 +32,7 @@ const LOG_PREFIX = 'xterm.js: ';
export class LogService implements ILogService {
public serviceBrand: any;
private _logLevel!: LogLevel;
public logLevel: LogLevelEnum = LogLevelEnum.OFF;
constructor(
@IOptionsService private readonly _optionsService: IOptionsService
@@ -55,7 +46,7 @@ export class LogService implements ILogService {
}
private _updateLogLevel(): void {
this._logLevel = optionsKeyToLogLevel[this._optionsService.options.logLevel];
this.logLevel = optionsKeyToLogLevel[this._optionsService.options.logLevel];
}
private _evalLazyOptionalParams(optionalParams: any[]): void {
@@ -72,25 +63,25 @@ export class LogService implements ILogService {
}
public debug(message: string, ...optionalParams: any[]): void {
if (this._logLevel <= LogLevel.DEBUG) {
if (this.logLevel <= LogLevelEnum.DEBUG) {
this._log(console.log, message, optionalParams);
}
}
public info(message: string, ...optionalParams: any[]): void {
if (this._logLevel <= LogLevel.INFO) {
if (this.logLevel <= LogLevelEnum.INFO) {
this._log(console.info, message, optionalParams);
}
}
public warn(message: string, ...optionalParams: any[]): void {
if (this._logLevel <= LogLevel.WARN) {
if (this.logLevel <= LogLevelEnum.WARN) {
this._log(console.warn, message, optionalParams);
}
}
public error(message: string, ...optionalParams: any[]): void {
if (this._logLevel <= LogLevel.ERROR) {
if (this.logLevel <= LogLevelEnum.ERROR) {
this._log(console.error, message, optionalParams);
}
}
+9
View File
@@ -161,6 +161,8 @@ export const ILogService = createDecorator<ILogService>('LogService');
export interface ILogService {
serviceBrand: undefined;
logLevel: LogLevelEnum;
debug(message: any, ...optionalParams: any[]): void;
info(message: any, ...optionalParams: any[]): void;
warn(message: any, ...optionalParams: any[]): void;
@@ -181,6 +183,13 @@ export interface IOptionsService {
export type FontWeight = 'normal' | 'bold' | '100' | '200' | '300' | '400' | '500' | '600' | '700' | '800' | '900' | number;
export type LogLevel = 'debug' | 'info' | 'warn' | 'error' | 'off';
export enum LogLevelEnum {
DEBUG = 0,
INFO = 1,
WARN = 2,
ERROR = 3,
OFF = 4
}
export type RendererType = 'dom' | 'canvas';
export interface IPartialTerminalOptions {
+125 -20
View File
@@ -14,8 +14,8 @@ let page: Page;
const width = 800;
const height = 600;
describe('Parser Integration Tests', function(): void {
before(async function(): Promise<any> {
describe('Parser Integration Tests', function (): void {
before(async function (): Promise<any> {
const browserType = getBrowserType();
browser = await browserType.launch({
headless: process.argv.indexOf('--headless') !== -1
@@ -28,84 +28,189 @@ describe('Parser Integration Tests', function(): void {
after(async () => browser.close());
describe('addCsiHandler', () => {
describe('registerCsiHandler', () => {
it('should call custom CSI handler with js array params', async () => {
await page.evaluate(`
window.term.reset();
window._customCsiHandlerParams = [];
const _customCsiHandler = window.term.parser.addCsiHandler({final: 'm'}, (params, collect) => {
const _customCsiHandler = window.term.parser.registerCsiHandler({final: 'm'}, (params, collect) => {
window._customCsiHandlerParams.push(params);
return false;
}, '');
`);
await writeSync(page, '\x1b[38;5;123mparams\x1b[38:2::50:100:150msubparams');
assert.deepEqual(await page.evaluate(`(() => window._customCsiHandlerParams)();`), [[38, 5, 123], [38, [2, -1, 50, 100, 150]]]);
assert.deepEqual(await page.evaluate(`window._customCsiHandlerParams`), [
[38, 5, 123],
[38, [2, -1, 50, 100, 150]]
]);
});
it('async', async () => {
await page.evaluate(`
window.term.reset();
window._customCsiHandlerCallStack = [];
const _customCsiHandlerA = window.term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, params => {
window._customCsiHandlerCallStack.push(['A', params]);
return false;
});
const _customCsiHandlerB = window.term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, async params => {
await new Promise(res => setTimeout(res, 50));
window._customCsiHandlerCallStack.push(['B', params]);
return false;
});
const _customCsiHandlerC = window.term.parser.registerCsiHandler({intermediates:'+', final: 'Z'}, params => {
window._customCsiHandlerCallStack.push(['C', params]);
return false;
});
`);
await writeSync(page, '\x1b[1;2+Z');
assert.deepEqual(await page.evaluate(`window._customCsiHandlerCallStack`), [
['C', [1, 2]],
['B', [1, 2]],
['A', [1, 2]]
]);
});
});
describe('addDcsHandler', () => {
describe('registerDcsHandler', () => {
it('should respects return value', async () => {
await page.evaluate(`
window.term.reset();
window._customDcsHandlerCallStack = [];
const _customDcsHandlerA = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => {
const _customDcsHandlerA = window.term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => {
window._customDcsHandlerCallStack.push(['A', params, data]);
return false;
});
const _customDcsHandlerB = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => {
const _customDcsHandlerB = window.term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => {
window._customDcsHandlerCallStack.push(['B', params, data]);
return true;
});
const _customDcsHandlerC = window.term.parser.addDcsHandler({intermediates:'+', final: 'p'}, (data, params) => {
const _customDcsHandlerC = window.term.parser.registerDcsHandler({intermediates:'+', final: 'p'}, (data, params) => {
window._customDcsHandlerCallStack.push(['C', params, data]);
return false;
});
`);
await writeSync(page, '\x1bP1;2+psome data\x1b\\\\');
assert.deepEqual(await page.evaluate(`(() => window._customDcsHandlerCallStack)();`), [['C', [1, 2], 'some data'], ['B', [1, 2], 'some data']]);
assert.deepEqual(await page.evaluate(`window._customDcsHandlerCallStack`), [
['C', [1, 2], 'some data'],
['B', [1, 2], 'some data']
]);
});
it('async', async () => {
await page.evaluate(`
window.term.reset();
window._customDcsHandlerCallStack = [];
const _customDcsHandlerA = window.term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, (data, params) => {
window._customDcsHandlerCallStack.push(['A', params, data]);
return false;
});
const _customDcsHandlerB = window.term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, async (data, params) => {
await new Promise(res => setTimeout(res, 50));
window._customDcsHandlerCallStack.push(['B', params, data]);
return false;
});
const _customDcsHandlerC = window.term.parser.registerDcsHandler({intermediates:'+', final: 'q'}, (data, params) => {
window._customDcsHandlerCallStack.push(['C', params, data]);
return false;
});
`);
await writeSync(page, '\x1bP1;2+qsome data\x1b\\\\');
assert.deepEqual(await page.evaluate(`window._customDcsHandlerCallStack`), [
['C', [1, 2], 'some data'],
['B', [1, 2], 'some data'],
['A', [1, 2], 'some data']
]);
});
});
describe('addEscHandler', () => {
describe('registerEscHandler', () => {
it('should respects return value', async () => {
await page.evaluate(`
window.term.reset();
window._customEscHandlerCallStack = [];
const _customEscHandlerA = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => {
const _customEscHandlerA = window.term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => {
window._customEscHandlerCallStack.push('A');
return false;
});
const _customEscHandlerB = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => {
const _customEscHandlerB = window.term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => {
window._customEscHandlerCallStack.push('B');
return true;
});
const _customEscHandlerC = window.term.parser.addEscHandler({intermediates:'(', final: 'B'}, () => {
const _customEscHandlerC = window.term.parser.registerEscHandler({intermediates:'(', final: 'B'}, () => {
window._customEscHandlerCallStack.push('C');
return false;
});
`);
await writeSync(page, '\x1b(B');
assert.deepEqual(await page.evaluate(`(() => window._customEscHandlerCallStack)();`), ['C', 'B']);
assert.deepEqual(await page.evaluate(`window._customEscHandlerCallStack`), ['C', 'B']);
});
it('async', async () => {
await page.evaluate(`
window.term.reset();
window._customEscHandlerCallStack = [];
const _customEscHandlerA = window.term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, () => {
window._customEscHandlerCallStack.push('A');
return false;
});
const _customEscHandlerB = window.term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, async () => {
await new Promise(res => setTimeout(res, 50));
window._customEscHandlerCallStack.push('B');
return false;
});
const _customEscHandlerC = window.term.parser.registerEscHandler({intermediates:'(', final: 'Z'}, () => {
window._customEscHandlerCallStack.push('C');
return false;
});
`);
await writeSync(page, '\x1b(Z');
assert.deepEqual(await page.evaluate(`window._customEscHandlerCallStack`), ['C', 'B', 'A']);
});
});
describe('addOscHandler', () => {
describe('registerOscHandler', () => {
it('should respects return value', async () => {
await page.evaluate(`
window.term.reset();
window._customOscHandlerCallStack = [];
const _customOscHandlerA = window.term.parser.addOscHandler(1234, data => {
const _customOscHandlerA = window.term.parser.registerOscHandler(1234, data => {
window._customOscHandlerCallStack.push(['A', data]);
return false;
});
const _customOscHandlerB = window.term.parser.addOscHandler(1234, data => {
const _customOscHandlerB = window.term.parser.registerOscHandler(1234, data => {
window._customOscHandlerCallStack.push(['B', data]);
return true;
});
const _customOscHandlerC = window.term.parser.addOscHandler(1234, data => {
const _customOscHandlerC = window.term.parser.registerOscHandler(1234, data => {
window._customOscHandlerCallStack.push(['C', data]);
return false;
});
`);
await writeSync(page, '\x1b]1234;some data\x07');
assert.deepEqual(await page.evaluate(`(() => window._customOscHandlerCallStack)();`), [['C', 'some data'], ['B', 'some data']]);
assert.deepEqual(await page.evaluate(`window._customOscHandlerCallStack`), [
['C', 'some data'],
['B', 'some data']
]);
});
it('async', async () => {
await page.evaluate(`
window.term.reset();
window._customOscHandlerCallStack = [];
const _customOscHandlerA = window.term.parser.registerOscHandler(666, data => {
window._customOscHandlerCallStack.push(['A', data]);
return false;
});
const _customOscHandlerB = window.term.parser.registerOscHandler(666, async data => {
await new Promise(res => setTimeout(res, 50));
window._customOscHandlerCallStack.push(['B', data]);
return false;
});
const _customOscHandlerC = window.term.parser.registerOscHandler(666, data => {
window._customOscHandlerCallStack.push(['C', data]);
return false;
});
`);
await writeSync(page, '\x1b]666;some data\x07');
assert.deepEqual(await page.evaluate(`window._customOscHandlerCallStack`), [
['C', 'some data'],
['B', 'some data'],
['A', 'some data']
]);
});
});
});
+7 -7
View File
@@ -29,7 +29,7 @@ perfContext('Terminal: ls -lR /usr/lib', () => {
chunks.push(data as unknown as Buffer);
length += data.length;
});
await new Promise(resolve => p.on('exit', () => resolve()));
await new Promise<void>(resolve => p.on('exit', () => resolve()));
contentUtf8 = Buffer.concat(chunks, length);
// translate to content string
const buffer = new Uint32Array(contentUtf8.length);
@@ -44,24 +44,24 @@ perfContext('Terminal: ls -lR /usr/lib', () => {
}
});
perfContext('write', () => {
perfContext('write/string/async', () => {
let terminal: Terminal;
before(() => {
terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000});
});
new ThroughputRuntimeCase('', () => {
terminal.writeSync(content);
new ThroughputRuntimeCase('', async () => {
await new Promise<void>(res => terminal.write(content, res));
return {payloadSize: contentUtf8.length};
}, {fork: false}).showAverageThroughput();
});
perfContext('writeUtf8', () => {
perfContext('write/Utf8/async', () => {
let terminal: Terminal;
before(() => {
terminal = new Terminal({cols: 80, rows: 25, scrollback: 1000});
});
new ThroughputRuntimeCase('', () => {
terminal.writeSync(content);
new ThroughputRuntimeCase('', async () => {
await new Promise<void>(res => terminal.write(content, res));
return {payloadSize: contentUtf8.length};
}, {fork: false}).showAverageThroughput();
});
+29 -16
View File
@@ -1501,6 +1501,23 @@ declare module 'xterm' {
/**
* Allows hooking into the parser for custom handling of escape sequences.
*
* Note on sync vs. async handlers:
* xterm.js implements all parser actions with synchronous handlers.
* In general custom handlers should also operate in sync mode wherever
* possible to keep the parser fast.
* Still the exposed interfaces allow to register async handlers by returning
* a `Promise<boolean>`. Here the parser will pause input processing until
* the promise got resolved or rejected (in-band blocking). This "full stop"
* on the input chain allows to implement backpressure from a certain async
* action while the terminal state will not progress any further from input.
* It does not mean that the terminal state will not change at all in between,
* as user actions like resize or reset are still processed immediately.
* It is an error to assume a stable terminal state while giving back control
* in between, e.g. by multiple chained `then` calls.
* Downside of an async handler is a rather bad throughput performance,
* thus use async handlers only as a last resort or for actions that have
* to rely on async interfaces itself.
*/
export interface IParser {
/**
@@ -1510,12 +1527,11 @@ declare module 'xterm' {
* @param callback The function to handle the sequence. The callback is
* called with the numerical params. If the sequence has subparams the
* array will contain subarrays with their numercial values.
* Return true if the sequence was handled; false if we should try
* a previous handler (set by addCsiHandler or setCsiHandler).
* The most recently added handler is tried first.
* Return `true` if the sequence was handled, `false` if the parser should try
* a previous handler. The most recently added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean): IDisposable;
registerCsiHandler(id: IFunctionIdentifier, callback: (params: (number | number[])[]) => boolean | Promise<boolean>): IDisposable;
/**
* Adds a handler for DCS escape sequences.
@@ -1529,12 +1545,11 @@ declare module 'xterm' {
* big payloads. Currently xterm.js limits DCS payload to 10 MB
* which should give enough room for most use cases.
* The function gets the payload and numerical parameters as arguments.
* Return true if the sequence was handled; false if we should try
* a previous handler (set by addDcsHandler or setDcsHandler).
* The most recently added handler is tried first.
* Return `true` if the sequence was handled, `false` if the parser should try
* a previous handler. The most recently added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean): IDisposable;
registerDcsHandler(id: IFunctionIdentifier, callback: (data: string, param: (number | number[])[]) => boolean | Promise<boolean>): IDisposable;
/**
* Adds a handler for ESC escape sequences.
@@ -1542,12 +1557,11 @@ declare module 'xterm' {
* gets registered, e.g. {intermediates: '%' final: 'G'} for
* default charset selection.
* @param callback The function to handle the sequence.
* Return true if the sequence was handled; false if we should try
* a previous handler (set by addEscHandler or setEscHandler).
* The most recently added handler is tried first.
* Return `true` if the sequence was handled, `false` if the parser should try
* a previous handler. The most recently added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
registerEscHandler(id: IFunctionIdentifier, handler: () => boolean): IDisposable;
registerEscHandler(id: IFunctionIdentifier, handler: () => boolean | Promise<boolean>): IDisposable;
/**
* Adds a handler for OSC escape sequences.
@@ -1560,12 +1574,11 @@ declare module 'xterm' {
* big payloads. Currently xterm.js limits OSC payload to 10 MB
* which should give enough room for most use cases.
* The callback is called with OSC data string.
* Return true if the sequence was handled; false if we should try
* a previous handler (set by addOscHandler or setOscHandler).
* The most recently added handler is tried first.
* Return `true` if the sequence was handled, `false` if the parser should try
* a previous handler. The most recently added handler is tried first.
* @return An IDisposable you can call to remove this handler.
*/
registerOscHandler(ident: number, callback: (data: string) => boolean): IDisposable;
registerOscHandler(ident: number, callback: (data: string) => boolean | Promise<boolean>): IDisposable;
}
/**