Merge branch 'master' into Fix-bug-of-some-IMEs-cannot-input-in-terminal
This commit is contained in:
+244
-230
@@ -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]);
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Vendored
+4
-4
@@ -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;
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}));
|
||||
|
||||
+580
-507
File diff suppressed because it is too large
Load Diff
+98
-20
@@ -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));
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
Vendored
+14
-2
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = [];
|
||||
|
||||
@@ -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!']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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!']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
Vendored
+42
-7
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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']
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
Vendored
+29
-16
@@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user