Merge pull request #2724 from jerch/reverse_wraparound

Reverse wraparound
This commit is contained in:
Daniel Imms
2020-05-03 07:11:25 -07:00
committed by GitHub
7 changed files with 213 additions and 7 deletions
+48
View File
@@ -1484,6 +1484,54 @@ describe('Terminal', () => {
assert.deepEqual(getLines(term, term.rows + 1), ['12345', '125', '125', '125', '125', '125']);
});
});
describe('BS with reverseWraparound set/unset', () => {
const ttyBS = '\x08 \x08'; // tty ICANON sends <BS SP BS> on pressing BS
beforeEach(() => {
term = new TestTerminal({cols: 5, rows: 5, scrollback: 1});
});
describe('reverseWraparound set', () => {
it('should not reverse outside of scroll margins', () => {
// prepare buffer content
term.writeSync('#####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));
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']);
term.writeSync('\x1b[?45h');
term.writeSync('uvwxy');
// set top/bottom to 1/3 (0-based)
term.writeSync('\x1b[2;4r');
// place cursor below scroll bottom
term.buffer.x = 5;
term.buffer.y = 4;
term.writeSync(ttyBS.repeat(100));
assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']);
term.writeSync('uvwxy');
// place cursor within scroll margins
term.buffer.x = 5;
term.buffer.y = 3;
term.writeSync(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');
// place cursor above scroll top
term.buffer.x = 5;
term.buffer.y = 0;
term.writeSync(ttyBS.repeat(100));
assert.deepEqual(getLines(term, 6), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']);
});
});
});
});
});
+1 -1
View File
@@ -19,7 +19,7 @@ const TESTFILES = glob.sync('**/escape_sequence_files/*.in', { cwd: path.join(__
const SKIP_FILES = [
't0084-CBT.in',
't0101-NLM.in',
't0103-reverse_wrap.in',
't0103-reverse_wrap.in', // not comparable, we deviate from xterm reverse wrap on purpose
't0504-vim.in'
];
if (os.platform() === 'darwin') {
+94 -1
View File
@@ -28,7 +28,10 @@ function getCursor(bufferService: IBufferService): number[] {
function getLines(bufferService: IBufferService, limit: number = bufferService.rows): string[] {
const res: string[] = [];
for (let i = 0; i < limit; ++i) {
res.push(bufferService.buffer.lines.get(i)!.translateToString(true));
const line = bufferService.buffer.lines.get(i);
if (line) {
res.push(line.translateToString(true));
}
}
return res;
}
@@ -359,6 +362,14 @@ describe('InputHandler', () => {
container[0] = 0x200B;
inputHandler.print(container, 0, 1);
});
it('should clear cells to the right on early wrap-around', () => {
bufferService.resize(5, 5);
optionsService.options.scrollback = 1;
inputHandler.parse('12345');
bufferService.buffer.x = 0;
inputHandler.parse('¥¥¥');
assert.deepEqual(getLines(bufferService, 2), ['¥¥', '¥']);
});
});
describe('alt screen', () => {
@@ -1375,6 +1386,88 @@ describe('InputHandler', () => {
assert.deepEqual(getLines(bufferService), ['¥¥ ¥¥', '¥¥ ¥', '¥¥ ¥', '¥¥¥¥¥', '']);
});
});
describe('BS with reverseWraparound set/unset', () => {
const ttyBS = '\x08 \x08'; // tty ICANON sends <BS SP BS> on pressing BS
beforeEach(() => {
bufferService.resize(5, 5);
optionsService.options.scrollback = 1;
});
describe('reverseWraparound unset (default)', () => {
it('cannot delete last cell', () => {
inputHandler.parse('12345');
inputHandler.parse(ttyBS);
assert.deepEqual(getLines(bufferService, 1), ['123 5']);
inputHandler.parse(ttyBS.repeat(10));
assert.deepEqual(getLines(bufferService, 1), [' 5']);
});
it('cannot access prev line', () => {
inputHandler.parse('12345'.repeat(2));
inputHandler.parse(ttyBS);
assert.deepEqual(getLines(bufferService, 2), ['12345', '123 5']);
inputHandler.parse(ttyBS.repeat(10));
assert.deepEqual(getLines(bufferService, 2), ['12345', ' 5']);
});
});
describe('reverseWraparound set', () => {
it('can delete last cell', () => {
inputHandler.parse('\x1b[?45h');
inputHandler.parse('12345');
inputHandler.parse(ttyBS);
assert.deepEqual(getLines(bufferService, 1), ['1234 ']);
inputHandler.parse(ttyBS.repeat(7));
assert.deepEqual(getLines(bufferService, 1), [' ']);
});
it('can access prev line if wrapped', () => {
inputHandler.parse('\x1b[?45h');
inputHandler.parse('12345'.repeat(2));
inputHandler.parse(ttyBS);
assert.deepEqual(getLines(bufferService, 2), ['12345', '1234 ']);
inputHandler.parse(ttyBS.repeat(7));
assert.deepEqual(getLines(bufferService, 2), ['12 ', ' ']);
});
it('should lift isWrapped', () => {
inputHandler.parse('\x1b[?45h');
inputHandler.parse('12345'.repeat(2));
assert.equal(bufferService.buffer.lines.get(1)?.isWrapped, true);
inputHandler.parse(ttyBS.repeat(7));
assert.equal(bufferService.buffer.lines.get(1)?.isWrapped, false);
});
it('stops at hard NLs', () => {
inputHandler.parse('\x1b[?45h');
inputHandler.parse('12345\r\n');
inputHandler.parse('12345'.repeat(2));
inputHandler.parse(ttyBS.repeat(50));
assert.deepEqual(getLines(bufferService, 3), ['12345', ' ', ' ']);
assert.equal(bufferService.buffer.x, 0);
assert.equal(bufferService.buffer.y, 1);
});
it('handles wide chars correctly', () => {
inputHandler.parse('\x1b[?45h');
inputHandler.parse('¥¥¥');
assert.deepEqual(getLines(bufferService, 2), ['¥¥', '¥']);
inputHandler.parse(ttyBS);
assert.deepEqual(getLines(bufferService, 2), ['¥¥', ' ']);
assert.equal(bufferService.buffer.x, 1);
inputHandler.parse(ttyBS);
assert.deepEqual(getLines(bufferService, 2), ['¥¥', ' ']);
assert.equal(bufferService.buffer.x, 0);
inputHandler.parse(ttyBS);
assert.deepEqual(getLines(bufferService, 2), ['¥ ', ' ']);
assert.equal(bufferService.buffer.x, 3); // x=4 skipped due to early wrap-around
inputHandler.parse(ttyBS);
assert.deepEqual(getLines(bufferService, 2), ['¥ ', ' ']);
assert.equal(bufferService.buffer.x, 2);
inputHandler.parse(ttyBS);
assert.deepEqual(getLines(bufferService, 2), [' ', ' ']);
assert.equal(bufferService.buffer.x, 1);
inputHandler.parse(ttyBS);
assert.deepEqual(getLines(bufferService, 2), [' ', ' ']);
assert.equal(bufferService.buffer.x, 0);
});
});
});
describe('DECSTR', () => {
beforeEach(() => {
bufferService.resize(10, 5);
+67 -5
View File
@@ -556,6 +556,10 @@ export class InputHandler extends Disposable implements IInputHandler {
// autowrap - DECAWM
// automatically wraps to the beginning of the next line
if (wraparoundMode) {
// clear left over cells to the right
while (buffer.x < cols) {
bufferRow.setCellFromCodePoint(buffer.x++, 0, 1, curAttr.fg, curAttr.bg);
}
buffer.x = 0;
buffer.y++;
if (buffer.y === buffer.scrollBottom + 1) {
@@ -727,12 +731,62 @@ export class InputHandler extends Disposable implements IInputHandler {
* Backspace (Ctrl-H).
*
* @vt: #Y C0 BS "Backspace" "\b, \x08" "Move the cursor one position to the left."
* By default it is not possible to move the cursor past the leftmost position.
* If `reverse wrap-around` (`CSI ? 45 h`) is set, a previous soft line wrap (DECAWM)
* can be undone with BS within the scroll margins. In that case the cursor will wrap back
* to the end of the previous row. Note that it is not possible to peek back into the scrollbuffer
* with the cursor, thus at the home position (top-leftmost cell) this has no effect.
*/
public backspace(): void {
this._restrictCursor();
if (this._bufferService.buffer.x > 0) {
this._bufferService.buffer.x--;
const buffer = this._bufferService.buffer;
// reverse wrap-around is disabled
if (!this._coreService.decPrivateModes.reverseWraparound) {
this._restrictCursor();
if (buffer.x > 0) {
buffer.x--;
}
return;
}
// reverse wrap-around is enabled
// other than for normal operation mode, reverse wrap-around allows the cursor
// to be at x=cols to be able to address the last cell of a row by BS
this._restrictCursor(this._bufferService.cols);
if (buffer.x > 0) {
buffer.x--;
} else {
/**
* reverse wrap-around handling:
* Our implementation deviates from xterm on purpose. Details:
* - only previous soft NLs can be reversed (isWrapped=true)
* - only works within scrollborders (top/bottom, left/right not yet supported)
* - cannot peek into scrollbuffer
* - any cursor movement sequence keeps working as expected
*/
if (buffer.x === 0
&& buffer.y > buffer.scrollTop
&& buffer.y <= buffer.scrollBottom
&& buffer.lines.get(buffer.ybase + buffer.y)?.isWrapped)
{
buffer.lines.get(buffer.ybase + buffer.y)!.isWrapped = false;
buffer.y--;
buffer.x = this._bufferService.cols - 1;
// find last taken cell - last cell can have 3 different states:
// - hasContent(true) + hasWidth(1): narrow char - we are done
// - hasWidth(0): second part of wide char - we are done
// - hasContent(false) + hasWidth(1): empty cell due to early wrapping wide char, go one cell further back
const line = buffer.lines.get(buffer.ybase + buffer.y)!;
if (line.hasWidth(buffer.x) && !line.hasContent(buffer.x)) {
buffer.x--;
// We do this only once, since width=1 + hasContent=false currently happens only once before
// early wrapping of a wide char.
// This needs to be fixed once we support graphemes taking more than 2 cells.
}
}
}
this._restrictCursor();
}
/**
@@ -777,8 +831,8 @@ export class InputHandler extends Disposable implements IInputHandler {
/**
* Restrict cursor to viewport size / scroll margin (origin mode).
*/
private _restrictCursor(): void {
this._bufferService.buffer.x = Math.min(this._bufferService.cols - 1, Math.max(0, this._bufferService.buffer.x));
private _restrictCursor(maxCol: number = this._bufferService.cols - 1): void {
this._bufferService.buffer.x = Math.min(maxCol, Math.max(0, this._bufferService.buffer.x));
this._bufferService.buffer.y = this._coreService.decPrivateModes.origin
? Math.min(this._bufferService.buffer.scrollBottom, Math.max(this._bufferService.buffer.scrollTop, this._bufferService.buffer.y))
: Math.min(this._bufferService.rows - 1, Math.max(0, this._bufferService.buffer.y));
@@ -1716,6 +1770,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* | 9 | X10 xterm mouse protocol. | #Y |
* | 12 | Start Blinking Cursor. | #Y |
* | 25 | Show Cursor (DECTCEM). | #Y |
* | 45 | Reverse wrap-around. | #Y |
* | 47 | Use Alternate Screen Buffer. | #Y |
* | 66 | Application keypad (DECNKM). | #Y |
* | 1000 | X11 xterm mouse protocol. | #Y |
@@ -1767,6 +1822,9 @@ export class InputHandler extends Disposable implements IInputHandler {
case 12:
// this.cursorBlink = true;
break;
case 45:
this._coreService.decPrivateModes.reverseWraparound = true;
break;
case 66:
this._logService.debug('Serial port requested application keypad.');
this._coreService.decPrivateModes.applicationKeypad = true;
@@ -1950,6 +2008,7 @@ export class InputHandler extends Disposable implements IInputHandler {
* | 9 | Don't send Mouse X & Y on button press. | #Y |
* | 12 | Stop Blinking Cursor. | #Y |
* | 25 | Hide Cursor (DECTCEM). | #Y |
* | 45 | No reverse wrap-around. | #Y |
* | 47 | Use Normal Screen Buffer. | #Y |
* | 66 | Numeric keypad (DECNKM). | #Y |
* | 1000 | Don't send Mouse reports. | #Y |
@@ -1994,6 +2053,9 @@ export class InputHandler extends Disposable implements IInputHandler {
case 12:
// this.cursorBlink = false;
break;
case 45:
this._coreService.decPrivateModes.reverseWraparound = false;
break;
case 66:
this._logService.debug('Switching back to normal keypad.');
this._coreService.decPrivateModes.applicationKeypad = false;
+1
View File
@@ -68,6 +68,7 @@ export class MockCoreService implements ICoreService {
applicationKeypad: false,
bracketedPasteMode: false,
origin: false,
reverseWraparound: false,
sendFocus: false,
wraparound: true
};
+1
View File
@@ -175,6 +175,7 @@ export interface IDecPrivateModes {
applicationKeypad: boolean;
bracketedPasteMode: boolean;
origin: boolean;
reverseWraparound: boolean;
sendFocus: boolean;
wraparound: boolean; // defaults: xterm - true, vt100 - false
}
+1
View File
@@ -17,6 +17,7 @@ const DEFAULT_DEC_PRIVATE_MODES: IDecPrivateModes = Object.freeze({
applicationKeypad: false,
bracketedPasteMode: false,
origin: false,
reverseWraparound: false,
sendFocus: false,
wraparound: true // defaults: xterm - true, vt100 - false
});