From fa7889cdd02436f40e1c581f135900d5fedb9f21 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 1 Apr 2021 12:17:34 -0700 Subject: [PATCH 01/15] add onRecovercontext event --- addons/xterm-addon-webgl/src/WebglAddon.ts | 6 +++++- addons/xterm-addon-webgl/src/WebglRenderer.ts | 12 ++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index aef1301e5..8eeda832e 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -3,14 +3,17 @@ * @license MIT */ -import { Terminal, ITerminalAddon } from 'xterm'; +import { Terminal, ITerminalAddon, IEvent } from 'xterm'; import { WebglRenderer } from './WebglRenderer'; import { IRenderService } from 'browser/services/Services'; import { IColorSet } from 'browser/Types'; +import { EventEmitter } from 'common/EventEmitter'; export class WebglAddon implements ITerminalAddon { private _terminal?: Terminal; private _renderer?: WebglRenderer; + private _onRecoverContext = new EventEmitter(); + public get onRecoverContext(): IEvent { return this._onRecoverContext.event; } constructor( private _preserveDrawingBuffer?: boolean @@ -24,6 +27,7 @@ export class WebglAddon implements ITerminalAddon { const renderService: IRenderService = (terminal)._core._renderService; const colors: IColorSet = (terminal)._core._colorManager.colors; this._renderer = new WebglRenderer(terminal, colors, this._preserveDrawingBuffer); + this._renderer.onRecoverContext(() => this._onRecoverContext.fire()); renderService.setRenderer(this._renderer); } diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index fecd57429..86e2900d3 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -19,6 +19,7 @@ import { IRenderDimensions, IRenderer, IRequestRedrawEvent } from 'browser/rende import { ITerminal, IColorSet } from 'browser/Types'; import { EventEmitter } from 'common/EventEmitter'; import { CellData } from 'common/buffer/CellData'; +import { addDisposableDomListener } from 'browser/Lifecycle'; export class WebglRenderer extends Disposable implements IRenderer { private _renderLayers: IRenderLayer[]; @@ -41,6 +42,9 @@ export class WebglRenderer extends Disposable implements IRenderer { private _onRequestRedraw = new EventEmitter(); public get onRequestRedraw(): IEvent { return this._onRequestRedraw.event; } + private _onRecoverContext = new EventEmitter(); + public get onRecoverContext(): IEvent { return this._onRecoverContext.event; } + constructor( private _terminal: Terminal, private _colors: IColorSet, @@ -82,6 +86,9 @@ export class WebglRenderer extends Disposable implements IRenderer { if (!this._gl) { throw new Error('WebGL2 not supported ' + this._gl); } + + this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLost(e); })); + this._core.screenElement!.appendChild(this._canvas); this._rectangleRenderer = new RectangleRenderer(this._terminal, this._colors, this._gl, this.dimensions); @@ -93,6 +100,11 @@ export class WebglRenderer extends Disposable implements IRenderer { this._isAttached = document.body.contains(this._core.screenElement!); } + private _onContextLost(e: Event): void { + e.preventDefault(); + this._onRecoverContext.fire(); + } + public dispose(): void { for (const l of this._renderLayers) { l.dispose(); From e2885f3a3877c36564195dd3fe0d9c24d924dc3d Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 31 Mar 2021 20:07:54 -0700 Subject: [PATCH 02/15] move stuff to inputHandler and move scroll to bufferService --- src/browser/Terminal.test.ts | 2245 +++++++++++--------------- src/browser/Terminal.ts | 4 +- src/common/CoreTerminal.ts | 37 +- src/common/InputHandler.test.ts | 184 ++- src/common/TestUtils.test.ts | 59 +- src/common/services/BufferService.ts | 141 +- src/common/services/Services.ts | 11 +- 7 files changed, 1328 insertions(+), 1353 deletions(-) diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts index 5724715f4..f3bcdfda7 100644 --- a/src/browser/Terminal.test.ts +++ b/src/browser/Terminal.test.ts @@ -117,13 +117,6 @@ describe('Terminal', () => { }); term.resize(1, 1); }); - it('should fire the onScroll event', (done) => { - term.onScroll(e => { - assert.equal(typeof e, 'number'); - done(); - }); - term.scroll(DEFAULT_ATTR_DATA.clone()); - }); it('should fire the onTitleChange event', (done) => { term.onTitleChange(e => { assert.equal(e, 'title'); @@ -280,1359 +273,991 @@ describe('Terminal', () => { }); }); - describe('scrollPages', () => { - let startYDisp: number; - beforeEach(async () => { - for (let i = 0; i < term.rows * 3; i++) { - await term.writeP('test\r\n'); - } - startYDisp = (term.rows * 2) + 1; - }); - it('should scroll a single page', () => { - assert.equal(term.buffer.ydisp, startYDisp); - term.scrollPages(-1); - assert.equal(term.buffer.ydisp, startYDisp - (term.rows - 1)); - term.scrollPages(1); - assert.equal(term.buffer.ydisp, startYDisp); - }); - it('should scroll a multiple pages', () => { - assert.equal(term.buffer.ydisp, startYDisp); - term.scrollPages(-2); - assert.equal(term.buffer.ydisp, startYDisp - (term.rows - 1) * 2); - term.scrollPages(2); - assert.equal(term.buffer.ydisp, startYDisp); - }); - }); + describe('Third level shift', () => { + let evKeyDown: any; + let evKeyPress: any; - describe('scrollToTop', () => { - beforeEach(async () => { - for (let i = 0; i < term.rows * 3; i++) { - await term.writeP('test\r\n'); - } - }); - it('should scroll to the top', () => { - assert.notEqual(term.buffer.ydisp, 0); - term.scrollToTop(); - assert.equal(term.buffer.ydisp, 0); - }); - }); - - describe('scrollToBottom', () => { - let startYDisp: number; - beforeEach(async () => { - for (let i = 0; i < term.rows * 3; i++) { - await term.writeP('test\r\n'); - } - startYDisp = (term.rows * 2) + 1; - }); - it('should scroll to the bottom', () => { - term.scrollLines(-1); - term.scrollToBottom(); - assert.equal(term.buffer.ydisp, startYDisp); - term.scrollPages(-1); - term.scrollToBottom(); - assert.equal(term.buffer.ydisp, startYDisp); - term.scrollToTop(); - term.scrollToBottom(); - assert.equal(term.buffer.ydisp, startYDisp); - }); - }); - - describe('scrollToLine', () => { - let startYDisp: number; - beforeEach(async () => { - for (let i = 0; i < term.rows * 3; i++) { - await term.writeP('test\r\n'); - } - startYDisp = (term.rows * 2) + 1; - }); - it('should scroll to requested line', () => { - assert.equal(term.buffer.ydisp, startYDisp); - term.scrollToLine(0); - assert.equal(term.buffer.ydisp, 0); - term.scrollToLine(10); - assert.equal(term.buffer.ydisp, 10); - term.scrollToLine(startYDisp); - assert.equal(term.buffer.ydisp, startYDisp); - term.scrollToLine(20); - assert.equal(term.buffer.ydisp, 20); - }); - it('should not scroll beyond boundary lines', () => { - assert.equal(term.buffer.ydisp, startYDisp); - term.scrollToLine(-1); - assert.equal(term.buffer.ydisp, 0); - term.scrollToLine(startYDisp + 1); - assert.equal(term.buffer.ydisp, startYDisp); - }); - }); - - describe('keyPress', () => { - it('should scroll down, when a key is pressed and terminal is scrolled up', () => { - const event = { - type: 'keydown', - key: 'a', - keyCode: 65, + beforeEach(() => { + term.clearSelection = () => { }; + // term.compositionHelper = { + // isComposing: false, + // keydown: { + // bind: () => { + // return () => { return true; }; + // } + // } + // }; + evKeyDown = { preventDefault: () => { }, - stopPropagation: () => { } + stopPropagation: () => { }, + type: 'keydown', + altKey: null, + keyCode: null + }; + evKeyPress = { + preventDefault: () => { }, + stopPropagation: () => { }, + type: 'keypress', + altKey: null, + charCode: null, + keyCode: null }; - - term.buffer.ydisp = 0; - term.buffer.ybase = 40; - term.keyPress(event); - - // Ensure that now the terminal is scrolled to bottom - assert.equal(term.buffer.ydisp, term.buffer.ybase); }); - 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++) { - await term.writeP('test\r\n'); - } - const startYDisp = (term.rows * 2) + 1; - term.attachCustomKeyEventHandler(() => { - return false; - }); - - assert.equal(term.buffer.ydisp, startYDisp); - term.scrollLines(-1); - assert.equal(term.buffer.ydisp, startYDisp - 1); - term.keyPress({ keyCode: 0 }); - assert.equal(term.buffer.ydisp, startYDisp - 1); - }); - }); - - describe('scroll() function', () => { - describe('when scrollback > 0', () => { - it('should create a new line and scroll', () => { - term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); - term.buffer.lines.get(INIT_ROWS - 1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); - term.buffer.y = INIT_ROWS - 1; // Move cursor to last line - term.scroll(DEFAULT_ATTR_DATA.clone()); - assert.equal(term.buffer.lines.length, INIT_ROWS + 1); - assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a'); - assert.equal(term.buffer.lines.get(INIT_ROWS - 1)!.loadCell(0, new CellData()).getChars(), 'b'); - assert.equal(term.buffer.lines.get(INIT_ROWS)!.loadCell(0, new CellData()).getChars(), ''); - }); - - it('should properly scroll inside a scroll region (scrollTop set)', () => { - term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); - term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); - term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); - term.buffer.y = INIT_ROWS - 1; // Move cursor to last line - term.buffer.scrollTop = 1; - term.scroll(DEFAULT_ATTR_DATA.clone()); - assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a'); - assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'c'); - }); - - it('should properly scroll inside a scroll region (scrollBottom set)', () => { - term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); - term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); - term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); - term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); - term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); - term.buffer.y = 3; - term.buffer.scrollBottom = 3; - term.scroll(DEFAULT_ATTR_DATA.clone()); - assert.equal(term.buffer.lines.length, INIT_ROWS + 1); - assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a', '\'a\' should be pushed to the scrollback'); - assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'b'); - assert.equal(term.buffer.lines.get(2)!.loadCell(0, new CellData()).getChars(), 'c'); - assert.equal(term.buffer.lines.get(3)!.loadCell(0, new CellData()).getChars(), 'd'); - assert.equal(term.buffer.lines.get(4)!.loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(5)!.loadCell(0, new CellData()).getChars(), 'e'); - }); - - it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { - term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); - term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); - term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); - term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); - term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); - term.buffer.y = INIT_ROWS - 1; // Move cursor to last line - term.buffer.scrollTop = 1; - term.buffer.scrollBottom = 3; - term.scroll(DEFAULT_ATTR_DATA.clone()); - assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a'); - assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer'); - assert.equal(term.buffer.lines.get(2)!.loadCell(0, new CellData()).getChars(), 'd'); - assert.equal(term.buffer.lines.get(3)!.loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4)!.loadCell(0, new CellData()).getChars(), 'e'); - }); - }); - - describe('when scrollback === 0', () => { + describe('with macOptionIsMeta', () => { + let originalIsMac: boolean; beforeEach(() => { - term.optionsService.setOption('scrollback', 0); - assert.equal(term.buffer.lines.maxLength, INIT_ROWS); + originalIsMac = term.browser.isMac; + term.options.macOptionIsMeta = true; + }); + afterEach(() => term.browser.isMac = originalIsMac); + + it('should interfere with the alt key on keyDown', () => { + evKeyDown.altKey = true; + evKeyDown.keyCode = 81; + assert.equal(term.keyDown(evKeyDown), false); + evKeyDown.altKey = true; + evKeyDown.keyCode = 192; + assert.equal(term.keyDown(evKeyDown), false); + }); + }); + + describe('On Mac OS', () => { + let originalIsMac: boolean; + beforeEach(() => { + originalIsMac = term.browser.isMac; + term.browser.isMac = true; + }); + afterEach(() => term.browser.isMac = originalIsMac); + + it('should not interfere with the alt key on keyDown', () => { + evKeyDown.altKey = true; + evKeyDown.keyCode = 81; + assert.equal(term.keyDown(evKeyDown), true); + evKeyDown.altKey = true; + evKeyDown.keyCode = 192; + term.keyDown(evKeyDown); + assert.equal(term.keyDown(evKeyDown), true); }); - it('should create a new line and shift everything up', () => { - term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); - term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); - term.buffer.lines.get(INIT_ROWS - 1)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); - term.buffer.y = INIT_ROWS - 1; // Move cursor to last line - assert.equal(term.buffer.lines.length, INIT_ROWS); - term.scroll(DEFAULT_ATTR_DATA.clone()); - assert.equal(term.buffer.lines.length, INIT_ROWS); - // 'a' gets pushed out of buffer - assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'b'); - assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), ''); - assert.equal(term.buffer.lines.get(INIT_ROWS - 2)!.loadCell(0, new CellData()).getChars(), 'c'); - assert.equal(term.buffer.lines.get(INIT_ROWS - 1)!.loadCell(0, new CellData()).getChars(), ''); + it('should interfere with the alt + arrow keys', () => { + evKeyDown.altKey = true; + evKeyDown.keyCode = 37; + assert.equal(term.keyDown(evKeyDown), false); + evKeyDown.altKey = true; + evKeyDown.keyCode = 39; + assert.equal(term.keyDown(evKeyDown), false); }); - it('should properly scroll inside a scroll region (scrollTop set)', () => { - term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); - term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); - term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); - term.buffer.y = INIT_ROWS - 1; // Move cursor to last line - term.buffer.scrollTop = 1; - term.scroll(DEFAULT_ATTR_DATA.clone()); - assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a'); - assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'c'); + it('should emit key with alt + key on keyPress', (done) => { + const keys = ['@', '@', '\\', '\\', '|', '|']; + + term.onKey(e => { + if (e.key) { + const index = keys.indexOf(e.key); + assert(index !== -1, 'Emitted wrong key: ' + e.key); + keys.splice(index, 1); + } + if (keys.length === 0) done(); + }); + + evKeyPress.altKey = true; + // @ + evKeyPress.charCode = null; + evKeyPress.keyCode = 64; + term.keyPress(evKeyPress); + // Firefox @ + evKeyPress.charCode = 64; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + // \ + evKeyPress.charCode = null; + evKeyPress.keyCode = 92; + term.keyPress(evKeyPress); + // Firefox \ + evKeyPress.charCode = 92; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + // | + evKeyPress.charCode = null; + evKeyPress.keyCode = 124; + term.keyPress(evKeyPress); + // Firefox | + evKeyPress.charCode = 124; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + }); + }); + + describe('On MS Windows', () => { + let originalIsWindows: boolean; + beforeEach(() => { + originalIsWindows = term.browser.isWindows; + term.browser.isWindows = true; + }); + afterEach(() => term.browser.isWindows = originalIsWindows); + + it('should not interfere with the alt + ctrl key on keyDown', () => { + evKeyPress.altKey = true; + evKeyPress.ctrlKey = true; + evKeyPress.keyCode = 81; + assert.equal(term.keyDown(evKeyPress), true); + evKeyDown.altKey = true; + evKeyDown.ctrlKey = true; + evKeyDown.keyCode = 81; + term.keyDown(evKeyDown); + assert.equal(term.keyDown(evKeyPress), true); }); - it('should properly scroll inside a scroll region (scrollBottom set)', () => { - term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); - term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); - term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); - term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); - term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); - term.buffer.y = 3; - term.buffer.scrollBottom = 3; - term.scroll(DEFAULT_ATTR_DATA.clone()); - assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'b'); - assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'c'); - assert.equal(term.buffer.lines.get(2)!.loadCell(0, new CellData()).getChars(), 'd'); - assert.equal(term.buffer.lines.get(3)!.loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4)!.loadCell(0, new CellData()).getChars(), 'e'); + it('should interfere with the alt + ctrl + arrow keys', () => { + evKeyDown.altKey = true; + evKeyDown.ctrlKey = true; + + evKeyDown.keyCode = 37; + assert.equal(term.keyDown(evKeyDown), false); + evKeyDown.keyCode = 39; + term.keyDown(evKeyDown); + assert.equal(term.keyDown(evKeyDown), false); }); - it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { - term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); - term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); - term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); - term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); - term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); - term.buffer.y = INIT_ROWS - 1; // Move cursor to last line - term.buffer.scrollTop = 1; - term.buffer.scrollBottom = 3; - term.scroll(DEFAULT_ATTR_DATA.clone()); - assert.equal(term.buffer.lines.length, INIT_ROWS); - assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a'); - assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer'); - assert.equal(term.buffer.lines.get(2)!.loadCell(0, new CellData()).getChars(), 'd'); - assert.equal(term.buffer.lines.get(3)!.loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); - assert.equal(term.buffer.lines.get(4)!.loadCell(0, new CellData()).getChars(), 'e'); + it('should emit key with alt + ctrl + key on keyPress', (done) => { + const keys = ['@', '@', '\\', '\\', '|', '|']; + + term.onKey(e => { + if (e.key) { + const index = keys.indexOf(e.key); + assert(index !== -1, 'Emitted wrong key: ' + e.key); + keys.splice(index, 1); + } + if (keys.length === 0) done(); + }); + + evKeyPress.altKey = true; + evKeyPress.ctrlKey = true; + + // @ + evKeyPress.charCode = null; + evKeyPress.keyCode = 64; + term.keyPress(evKeyPress); + // Firefox @ + evKeyPress.charCode = 64; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + // \ + evKeyPress.charCode = null; + evKeyPress.keyCode = 92; + term.keyPress(evKeyPress); + // Firefox \ + evKeyPress.charCode = 92; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + // | + evKeyPress.charCode = null; + evKeyPress.keyCode = 124; + term.keyPress(evKeyPress); + // Firefox | + evKeyPress.charCode = 124; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); }); }); }); - }); - describe('Third level shift', () => { - let evKeyDown: any; - let evKeyPress: any; - - beforeEach(() => { - term.clearSelection = () => { }; - // term.compositionHelper = { - // isComposing: false, - // keydown: { - // bind: () => { - // return () => { return true; }; - // } - // } - // }; - evKeyDown = { - preventDefault: () => { }, - stopPropagation: () => { }, - type: 'keydown', - altKey: null, - keyCode: null - }; - evKeyPress = { - preventDefault: () => { }, - stopPropagation: () => { }, - type: 'keypress', - altKey: null, - charCode: null, - keyCode: null - }; - }); - - describe('with macOptionIsMeta', () => { - let originalIsMac: boolean; - beforeEach(() => { - originalIsMac = term.browser.isMac; - term.options.macOptionIsMeta = true; - }); - afterEach(() => term.browser.isMac = originalIsMac); - - it('should interfere with the alt key on keyDown', () => { - evKeyDown.altKey = true; - evKeyDown.keyCode = 81; - assert.equal(term.keyDown(evKeyDown), false); - evKeyDown.altKey = true; - evKeyDown.keyCode = 192; - assert.equal(term.keyDown(evKeyDown), false); - }); - }); - - describe('On Mac OS', () => { - let originalIsMac: boolean; - beforeEach(() => { - originalIsMac = term.browser.isMac; - term.browser.isMac = true; - }); - afterEach(() => term.browser.isMac = originalIsMac); - - it('should not interfere with the alt key on keyDown', () => { - evKeyDown.altKey = true; - evKeyDown.keyCode = 81; - assert.equal(term.keyDown(evKeyDown), true); - evKeyDown.altKey = true; - evKeyDown.keyCode = 192; - term.keyDown(evKeyDown); - assert.equal(term.keyDown(evKeyDown), true); - }); - - it('should interfere with the alt + arrow keys', () => { - evKeyDown.altKey = true; - evKeyDown.keyCode = 37; - assert.equal(term.keyDown(evKeyDown), false); - evKeyDown.altKey = true; - evKeyDown.keyCode = 39; - assert.equal(term.keyDown(evKeyDown), false); - }); - - it('should emit key with alt + key on keyPress', (done) => { - const keys = ['@', '@', '\\', '\\', '|', '|']; - - term.onKey(e => { - if (e.key) { - const index = keys.indexOf(e.key); - assert(index !== -1, 'Emitted wrong key: ' + e.key); - keys.splice(index, 1); - } - if (keys.length === 0) done(); - }); - - evKeyPress.altKey = true; - // @ - evKeyPress.charCode = null; - evKeyPress.keyCode = 64; - term.keyPress(evKeyPress); - // Firefox @ - evKeyPress.charCode = 64; - evKeyPress.keyCode = 0; - term.keyPress(evKeyPress); - // \ - evKeyPress.charCode = null; - evKeyPress.keyCode = 92; - term.keyPress(evKeyPress); - // Firefox \ - evKeyPress.charCode = 92; - evKeyPress.keyCode = 0; - term.keyPress(evKeyPress); - // | - evKeyPress.charCode = null; - evKeyPress.keyCode = 124; - term.keyPress(evKeyPress); - // Firefox | - evKeyPress.charCode = 124; - evKeyPress.keyCode = 0; - term.keyPress(evKeyPress); - }); - }); - - describe('On MS Windows', () => { - let originalIsWindows: boolean; - beforeEach(() => { - originalIsWindows = term.browser.isWindows; - term.browser.isWindows = true; - }); - afterEach(() => term.browser.isWindows = originalIsWindows); - - it('should not interfere with the alt + ctrl key on keyDown', () => { - evKeyPress.altKey = true; - evKeyPress.ctrlKey = true; - evKeyPress.keyCode = 81; - assert.equal(term.keyDown(evKeyPress), true); - evKeyDown.altKey = true; - evKeyDown.ctrlKey = true; - evKeyDown.keyCode = 81; - term.keyDown(evKeyDown); - assert.equal(term.keyDown(evKeyPress), true); - }); - - it('should interfere with the alt + ctrl + arrow keys', () => { - evKeyDown.altKey = true; - evKeyDown.ctrlKey = true; - - evKeyDown.keyCode = 37; - assert.equal(term.keyDown(evKeyDown), false); - evKeyDown.keyCode = 39; - term.keyDown(evKeyDown); - assert.equal(term.keyDown(evKeyDown), false); - }); - - it('should emit key with alt + ctrl + key on keyPress', (done) => { - const keys = ['@', '@', '\\', '\\', '|', '|']; - - term.onKey(e => { - if (e.key) { - const index = keys.indexOf(e.key); - assert(index !== -1, 'Emitted wrong key: ' + e.key); - keys.splice(index, 1); - } - if (keys.length === 0) done(); - }); - - evKeyPress.altKey = true; - evKeyPress.ctrlKey = true; - - // @ - evKeyPress.charCode = null; - evKeyPress.keyCode = 64; - term.keyPress(evKeyPress); - // Firefox @ - evKeyPress.charCode = 64; - evKeyPress.keyCode = 0; - term.keyPress(evKeyPress); - // \ - evKeyPress.charCode = null; - evKeyPress.keyCode = 92; - term.keyPress(evKeyPress); - // Firefox \ - evKeyPress.charCode = 92; - evKeyPress.keyCode = 0; - term.keyPress(evKeyPress); - // | - evKeyPress.charCode = null; - evKeyPress.keyCode = 124; - term.keyPress(evKeyPress); - // Firefox | - evKeyPress.charCode = 124; - evKeyPress.keyCode = 0; - term.keyPress(evKeyPress); - }); - }); - }); - - describe('unicode - surrogates', () => { - it('2 characters per cell', async function (): Promise { - 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) { - 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); - assert.equal(tchar.getWidth(), 1); - assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), ''); - term.reset(); - } - }); - 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; - 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', async function (): Promise { - 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; - - 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); - assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), ''); - term.reset(); - } - }); - it('2 characters per cell over line end without autowrap', async function (): Promise { - 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; - await term.writeP('\x1b[?7l'); // Disable wraparound mode - const width = wcwidth((0xD800 - 0xD800) * 0x400 + i - 0xDC00 + 0x10000); - if (width !== 1) { - continue; + describe('unicode - surrogates', () => { + it('2 characters per cell', async function (): Promise { + 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) { + 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); + assert.equal(tchar.getWidth(), 1); + assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), ''); + term.reset(); } - 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); - assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), ''); - term.reset(); - } - }); - it('splitted surrogates', async function (): Promise { - this.timeout(10000); - const high = String.fromCharCode(0xD800); - const cell = new CellData(); - for (let i = 0xDC00; i <= 0xDCFF; ++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); - assert.equal(tchar.getWidth(), 1); - assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), ''); - term.reset(); - } - }); - }); + }); + 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; + 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', async function (): Promise { + 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; - describe('unicode - combining characters', () => { - const cell = new CellData(); - 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); + 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); + assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), ''); + term.reset(); + } + }); + it('2 characters per cell over line end without autowrap', async function (): Promise { + 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; + await term.writeP('\x1b[?7l'); // Disable wraparound mode + const width = wcwidth((0xD800 - 0xD800) * 0x400 + i - 0xDC00 + 0x10000); + if (width !== 1) { + continue; + } + 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); + assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), ''); + term.reset(); + } + }); + it('splitted surrogates', async function (): Promise { + this.timeout(10000); + const high = String.fromCharCode(0xD800); + const cell = new CellData(); + for (let i = 0xDC00; i <= 0xDCFF; ++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); + assert.equal(tchar.getWidth(), 1); + assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), ''); + term.reset(); + } + }); }); - it('café - end of line', async () => { - term.buffer.x = term.cols - 1 - 3; - 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); - assert.equal(cell.getWidth(), 1); - term.buffer.lines.get(0)!.loadCell(1, cell); - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 1); - }); - 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); + + describe('unicode - combining characters', () => { + const cell = new CellData(); + 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); - } - term.buffer.lines.get(1)!.loadCell(0, cell); - assert.equal(cell.getChars(), 'e\u0301'); - assert.equal(cell.getChars().length, 2); - assert.equal(cell.getWidth(), 1); - }); - 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); + }); + it('café - end of line', async () => { + term.buffer.x = term.cols - 1 - 3; + 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); + assert.equal(cell.getWidth(), 1); + term.buffer.lines.get(0)!.loadCell(1, cell); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 1); + }); + 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'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 1); + } + term.buffer.lines.get(1)!.loadCell(0, cell); + assert.equal(cell.getChars(), 'e\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 1); + }); + 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'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 1); + } + term.buffer.lines.get(1)!.loadCell(0, cell); assert.equal(cell.getChars(), '\uD800\uDC00\u0301'); assert.equal(cell.getChars().length, 3); assert.equal(cell.getWidth(), 1); - } - term.buffer.lines.get(1)!.loadCell(0, cell); - assert.equal(cell.getChars(), '\uD800\uDC00\u0301'); - assert.equal(cell.getChars().length, 3); - assert.equal(cell.getWidth(), 1); - }); - }); - - describe('unicode - fullwidth characters', () => { - const cell = new CellData(); - it('cursor movement even', async () => { - assert.equal(term.buffer.x, 0); - await term.writeP('¥'); - assert.equal(term.buffer.x, 2); - }); - it('cursor movement odd', async () => { - term.buffer.x = 1; - assert.equal(term.buffer.x, 1); - await term.writeP('¥'); - assert.equal(term.buffer.x, 3); - }); - 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) { - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 0); - } else { - assert.equal(cell.getChars(), '¥'); - assert.equal(cell.getChars().length, 1); - assert.equal(cell.getWidth(), 2); - } - } - term.buffer.lines.get(1)!.loadCell(0, cell); - assert.equal(cell.getChars(), '¥'); - assert.equal(cell.getChars().length, 1); - assert.equal(cell.getWidth(), 2); - }); - it('line of ¥ odd', async () => { - term.buffer.x = 1; - 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)) { - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 0); - } else { - assert.equal(cell.getChars(), '¥'); - assert.equal(cell.getChars().length, 1); - assert.equal(cell.getWidth(), 2); - } - } - term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 1); - term.buffer.lines.get(1)!.loadCell(0, cell); - assert.equal(cell.getChars(), '¥'); - assert.equal(cell.getChars().length, 1); - assert.equal(cell.getWidth(), 2); - }); - it('line of ¥ with combining odd', async () => { - term.buffer.x = 1; - 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)) { - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 0); - } else { - assert.equal(cell.getChars(), '¥\u0301'); - assert.equal(cell.getChars().length, 2); - assert.equal(cell.getWidth(), 2); - } - } - term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 1); - term.buffer.lines.get(1)!.loadCell(0, cell); - assert.equal(cell.getChars(), '¥\u0301'); - assert.equal(cell.getChars().length, 2); - assert.equal(cell.getWidth(), 2); - }); - 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) { - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 0); - } else { - assert.equal(cell.getChars(), '¥\u0301'); - assert.equal(cell.getChars().length, 2); - assert.equal(cell.getWidth(), 2); - } - } - term.buffer.lines.get(1)!.loadCell(0, cell); - assert.equal(cell.getChars(), '¥\u0301'); - assert.equal(cell.getChars().length, 2); - assert.equal(cell.getWidth(), 2); - }); - it('line of surrogate fullwidth with combining odd', async () => { - term.buffer.x = 1; - 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)) { - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 0); - } else { - assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); - assert.equal(cell.getChars().length, 3); - assert.equal(cell.getWidth(), 2); - } - } - term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 1); - term.buffer.lines.get(1)!.loadCell(0, cell); - assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); - assert.equal(cell.getChars().length, 3); - assert.equal(cell.getWidth(), 2); - }); - 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) { - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 0); - } else { - assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); - assert.equal(cell.getChars().length, 3); - assert.equal(cell.getWidth(), 2); - } - } - term.buffer.lines.get(1)!.loadCell(0, cell); - assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); - assert.equal(cell.getChars().length, 3); - assert.equal(cell.getWidth(), 2); - }); - }); - - describe('insert mode', () => { - const cell = new CellData(); - 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'); - 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', async () => { - await term.writeP(Array(9).join('0123456789').slice(-80)); - term.buffer.x = 10; - term.buffer.y = 0; - term.write('\x1b[4h'); - 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(), ''); - assert.equal(term.buffer.lines.get(0)!.loadCell(14, cell).getChars(), '¥'); - 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', async () => { - await term.writeP(Array(41).join('¥')); - term.buffer.x = 10; - term.buffer.y = 0; - term.write('\x1b[4h'); - 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 - 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(), '¥'); - assert.equal(term.buffer.lines.get(0)!.loadCell(79, cell).getChars(), ''); // empty cell after fullwidth - }); - }); - - describe('Linkifier unicode handling', () => { - let terminal: TestTerminal; - let linkifier: TestLinkifier; - let mouseZoneManager: TestMouseZoneManager; - - // other than the tests above unicode testing needs the full terminal instance - // to get the special handling of fullwidth, surrogate and combining chars in the input handler - beforeEach(() => { - terminal = new TestTerminal({ cols: 10, rows: 5 }); - linkifier = new TestLinkifier((terminal as any)._bufferService, terminal.unicodeService); - mouseZoneManager = new TestMouseZoneManager(); - linkifier.attachToDom({} as any, mouseZoneManager); - }); - - function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: {x1: number, y1: number, x2: number, y2: number}[]): Promise { - 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', () => { - return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}]); - }); - 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', () => { - return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}]); - }); - 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', () => { - return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{x1: 4, x2: 7, y1: 0, y2: 0}]); - }); - 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', () => { - return assertLinkifiesInTerminal('12 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}]); - }); - 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', () => { - return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{x1: 5, x2: 8, y1: 0, y2: 0}]); - }); - 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', () => { - return assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{x1: 5, x2: 9, y1: 0, y2: 0}]); + + describe('unicode - fullwidth characters', () => { + const cell = new CellData(); + it('cursor movement even', async () => { + assert.equal(term.buffer.x, 0); + await term.writeP('¥'); + assert.equal(term.buffer.x, 2); }); - it('combining - match over two lines', () => { - return assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{x1: 9, x2: 3, y1: 0, y2: 1}]); + it('cursor movement odd', async () => { + term.buffer.x = 1; + assert.equal(term.buffer.x, 1); + await term.writeP('¥'); + assert.equal(term.buffer.x, 3); }); - 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', () => { - return assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{x1: 9, x2: 2, y1: 0, y2: 1}]); - }); - 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', () => { - return assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{x1: 9, x2: 2, y1: 0, y2: 1}]); - }); - 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', () => { - return assertLinkifiesInTerminal('testtest a1b', /a1b/, [{x1: 9, x2: 3, y1: 0, y2: 1}]); - }); - 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', () => { - return assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{x1: 9, x2: 3, y1: 0, y2: 1}]); - }); - }); - }); - - describe('Buffer.stringIndexToBufferIndex', () => { - let terminal: TestTerminal; - - beforeEach(() => { - terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); - }); - - it('multiline ascii', async () => { - const input = 'This is ASCII text spanning multiple lines.'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - } - }); - - it('combining e\u0301 in a sentence', async () => { - const input = 'Sitting in the cafe\u0301 drinking coffee.'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < 19; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - } - // string index 18 & 19 point to combining char e\u0301 ---> same buffer Index - assert.deepEqual( - terminal.buffer.stringIndexToBufferIndex(0, 18), - terminal.buffer.stringIndexToBufferIndex(0, 19)); - // after the combining char every string index has an offset of -1 - for (let i = 19; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); - } - }); - - it('multiline combining e\u0301', async () => { - const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - // every buffer cell index contains 2 string indices - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); - } - }); - - it('surrogate char in a sentence', async () => { - const input = 'The 𝄞 is a clef widely used in modern notation.'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < 5; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - } - // string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index - assert.deepEqual( - terminal.buffer.stringIndexToBufferIndex(0, 4), - terminal.buffer.stringIndexToBufferIndex(0, 5)); - // after the combining char every string index has an offset of -1 - for (let i = 5; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); - } - }); - - it('multiline surrogate char', async () => { - const 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 - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); - } - }); - - 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.'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - // index 0..2 should map to 0 - assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1)); - assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2)); - for (let i = 2; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex); - } - }); - - it('multiline surrogate with combining', async () => { - const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - // every buffer cell index contains 3 string indices - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex); - } - }); - - it('fullwidth chars', async () => { - const input = 'These 123 are some fat numbers.'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < 6; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - } - // string index 6, 7, 8 take 2 cells - assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7)); - assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8)); - // rest of the string has offset of +3 - for (let i = 9; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex); - } - }); - - it('multiline fullwidth chars', async () => { - const input = '12345678901234567890'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 9; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex); - } - }); - - it('fullwidth combining with emoji - match emoji cell', async () => { - const input = 'Lots of ¥\u0301 make me 😃.'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - const stringIndex = s.match(/😃/)!.index!; - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex); - 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)', 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 - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 10; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - const j = (i - 0) << 1; - assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); - } - }); - - it('test fully wrapped buffer up to last char', async () => { - const input = Array(6).join('1234567890'); - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars()); - } - }); - - 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'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - assert.equal( - (!(i % 3)) - ? input[i] - : (i % 3 === 1) - ? input.substr(i, 2) - : input.substr(i - 1, 2), - terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars()); - } - }); - - it('should handle \t in lines correctly', async () => { - const input = '\thttps://google.de'; - 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', async () => { - const terminal = new TestTerminal({rows: 5, cols: 10, scrollback: 5}); - const data = [ - 'aaaaaaaaaa', - 'aaaaaaaaa\n', - 'aaaaaaaaaa', - 'aaaaaaaaa\n', - 'aaaaaaaaaa', - 'aaaaaaaaaa', - 'aaaaaaaaaa', - 'aaaaaaaaa\n', - 'aaaaaaaaaa', - 'aaaaaaaaaa' - ]; - await terminal.writeP(data.join('')); - // brute force test with insane values - assert.doesNotThrow(() => { - for (let overscan = 0; overscan < 20; ++overscan) { - for (let start = -10; start < 20; ++start) { - for (let end = -10; end < 20; ++end) { - const it = terminal.buffer.iterator(false, start, end, overscan, overscan); - while (it.hasNext()) { - it.next(); - } - } + 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) { + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); + } else { + assert.equal(cell.getChars(), '¥'); + assert.equal(cell.getChars().length, 1); + assert.equal(cell.getWidth(), 2); } } + term.buffer.lines.get(1)!.loadCell(0, cell); + assert.equal(cell.getChars(), '¥'); + assert.equal(cell.getChars().length, 1); + assert.equal(cell.getWidth(), 2); + }); + it('line of ¥ odd', async () => { + term.buffer.x = 1; + 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)) { + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); + } else { + assert.equal(cell.getChars(), '¥'); + assert.equal(cell.getChars().length, 1); + assert.equal(cell.getWidth(), 2); + } + } + term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 1); + term.buffer.lines.get(1)!.loadCell(0, cell); + assert.equal(cell.getChars(), '¥'); + assert.equal(cell.getChars().length, 1); + assert.equal(cell.getWidth(), 2); + }); + it('line of ¥ with combining odd', async () => { + term.buffer.x = 1; + 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)) { + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); + } else { + assert.equal(cell.getChars(), '¥\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 2); + } + } + term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 1); + term.buffer.lines.get(1)!.loadCell(0, cell); + assert.equal(cell.getChars(), '¥\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 2); + }); + 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) { + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); + } else { + assert.equal(cell.getChars(), '¥\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 2); + } + } + term.buffer.lines.get(1)!.loadCell(0, cell); + assert.equal(cell.getChars(), '¥\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 2); + }); + it('line of surrogate fullwidth with combining odd', async () => { + term.buffer.x = 1; + 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)) { + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); + } else { + assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 2); + } + } + term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 1); + term.buffer.lines.get(1)!.loadCell(0, cell); + assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 2); + }); + 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) { + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); + } else { + assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 2); + } + } + term.buffer.lines.get(1)!.loadCell(0, cell); + assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 2); }); }); - }); - describe('Windows Mode', () => { - 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) - 'aaaaaaaaa' // not wrapped - ]; - - const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false}); - 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}); - 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); + describe('insert mode', () => { + const cell = new CellData(); + 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'); + 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', async () => { + await term.writeP(Array(9).join('0123456789').slice(-80)); + term.buffer.x = 10; + term.buffer.y = 0; + term.write('\x1b[4h'); + 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(), ''); + assert.equal(term.buffer.lines.get(0)!.loadCell(14, cell).getChars(), '¥'); + 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', async () => { + await term.writeP(Array(41).join('¥')); + term.buffer.x = 10; + term.buffer.y = 0; + term.write('\x1b[4h'); + 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 + 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(), '¥'); + assert.equal(term.buffer.lines.get(0)!.loadCell(79, cell).getChars(), ''); // empty cell after fullwidth + }); }); - 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) - 'aaaaaaaaa' // not wrapped - ]; + describe('Linkifier unicode handling', () => { + let terminal: TestTerminal; + let linkifier: TestLinkifier; + let mouseZoneManager: TestMouseZoneManager; - const normalTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: false}); - 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); + // other than the tests above unicode testing needs the full terminal instance + // to get the special handling of fullwidth, surrogate and combining chars in the input handler + beforeEach(() => { + terminal = new TestTerminal({ cols: 10, rows: 5 }); + linkifier = new TestLinkifier((terminal as any)._bufferService, terminal.unicodeService); + mouseZoneManager = new TestMouseZoneManager(); + linkifier.attachToDom({} as any, mouseZoneManager); + }); - const windowsModeTerminal = new TestTerminal({rows: 5, cols: 10, windowsMode: true}); - 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', async () => { - // not converting - const termNotConverting = new TestTerminal({cols: 15, rows: 10}); - 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'); - assert.equal(termNotConverting.buffer.lines.get(1)!.translateToString(true), ' World'); - - // converting - const termConverting = new TestTerminal({cols: 15, rows: 10, convertEol: true}); - 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'); - assert.equal(termConverting.buffer.lines.get(1)!.translateToString(true), 'World'); - }); - describe('Terminal InputHandler integration', () => { - function getLines(term: TestTerminal, limit: number = term.rows): string[] { - const res: string[] = []; - for (let i = 0; i < limit; ++i) { - res.push(term.buffer.lines.get(i)!.translateToString(true)); + function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: { x1: number, y1: number, x2: number, y2: number }[]): Promise { + 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); + }); } - return res; - } - // This suite cannot live in InputHandler unless Terminal.scroll moved into IBufferService - describe('SL/SR/DECIC/DECDC', () => { - let term: TestTerminal; - beforeEach(() => { - term = new TestTerminal({cols: 5, rows: 5, scrollback: 1}); + describe('unicode before the match', () => { + 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', () => { + return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); + }); + it('surrogate - match within one line', () => { + return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]); + }); + 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', () => { + return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]); + }); + 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', () => { + return assertLinkifiesInTerminal('12 foo', /foo/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); + }); + 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', () => { + return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); + }); + it('combining fullwidth - match over two lines', () => { + return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); + }); }); - 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']); - await term.writeP('\x1b[0 @'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '345', '345', '345', '345', '345']); - await term.writeP('\x1b[2 @'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', '5', '5', '5', '5', '5']); - }); - 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']); - await term.writeP('\x1b[0 A'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']); - await term.writeP('\x1b[2 A'); - assert.deepEqual(getLines(term, term.rows + 1), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']); - }); - 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(); - 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(); - 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)', 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(); - 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(); - 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']); - }); - }); - - describe('BS with reverseWraparound set/unset', () => { - const ttyBS = '\x08 \x08'; // tty ICANON sends on pressing BS - - beforeEach(() => { - term = new TestTerminal({cols: 5, rows: 5, scrollback: 1}); - }); - - describe('reverseWraparound set', () => { - it('should not reverse outside of scroll margins', async () => { - // prepare buffer content - 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); - await term.writeP(ttyBS.repeat(100)); - assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']); - - await term.writeP('\x1b[?45h'); - await term.writeP('uvwxy'); - - // set top/bottom to 1/3 (0-based) - await term.writeP('\x1b[2;4r'); - // place cursor below scroll bottom - term.buffer.x = 5; - term.buffer.y = 4; - await term.writeP(ttyBS.repeat(100)); - assert.deepEqual(getLines(term, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']); - - await term.writeP('uvwxy'); - // place cursor within scroll margins - term.buffer.x = 5; - term.buffer.y = 3; - 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 - - await term.writeP('fghijklmnopqrst'); - // place cursor above scroll top - term.buffer.x = 5; - term.buffer.y = 0; - await term.writeP(ttyBS.repeat(100)); - assert.deepEqual(getLines(term, 6), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']); + describe('unicode within the match', () => { + 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', () => { + return assertLinkifiesInTerminal('testtest cafe\u0301', /cafe\u0301/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]); + }); + 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', () => { + return assertLinkifiesInTerminal('testtest a𝄞b', /a𝄞b/, [{ x1: 9, x2: 2, y1: 0, y2: 1 }]); + }); + 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', () => { + return assertLinkifiesInTerminal('testtest a𓂀\u0301b', /a𓂀\u0301b/, [{ x1: 9, x2: 2, y1: 0, y2: 1 }]); + }); + 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', () => { + return assertLinkifiesInTerminal('testtest a1b', /a1b/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]); + }); + 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', () => { + return assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]); }); }); }); + + describe('Buffer.stringIndexToBufferIndex', () => { + let terminal: TestTerminal; + + beforeEach(() => { + terminal = new TestTerminal({ rows: 5, cols: 10, scrollback: 5 }); + }); + + it('multiline ascii', async () => { + const input = 'This is ASCII text spanning multiple lines.'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + } + }); + + it('combining e\u0301 in a sentence', async () => { + const input = 'Sitting in the cafe\u0301 drinking coffee.'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < 19; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + } + // string index 18 & 19 point to combining char e\u0301 ---> same buffer Index + assert.deepEqual( + terminal.buffer.stringIndexToBufferIndex(0, 18), + terminal.buffer.stringIndexToBufferIndex(0, 19)); + // after the combining char every string index has an offset of -1 + for (let i = 19; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); + } + }); + + it('multiline combining e\u0301', async () => { + const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + // every buffer cell index contains 2 string indices + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); + } + }); + + it('surrogate char in a sentence', async () => { + const input = 'The 𝄞 is a clef widely used in modern notation.'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < 5; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + } + // string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index + assert.deepEqual( + terminal.buffer.stringIndexToBufferIndex(0, 4), + terminal.buffer.stringIndexToBufferIndex(0, 5)); + // after the combining char every string index has an offset of -1 + for (let i = 5; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); + } + }); + + it('multiline surrogate char', async () => { + const 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 + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); + } + }); + + 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.'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + // index 0..2 should map to 0 + assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1)); + assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2)); + for (let i = 2; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex); + } + }); + + it('multiline surrogate with combining', async () => { + const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + // every buffer cell index contains 3 string indices + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex); + } + }); + + it('fullwidth chars', async () => { + const input = 'These 123 are some fat numbers.'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < 6; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + } + // string index 6, 7, 8 take 2 cells + assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7)); + assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8)); + // rest of the string has offset of +3 + for (let i = 9; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex); + } + }); + + it('multiline fullwidth chars', async () => { + const input = '12345678901234567890'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 9; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex); + } + }); + + it('fullwidth combining with emoji - match emoji cell', async () => { + const input = 'Lots of ¥\u0301 make me 😃.'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + const stringIndex = s.match(/😃/)!.index!; + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex); + 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)', 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 + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 10; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); + const j = (i - 0) << 1; + assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); + } + }); + + it('test fully wrapped buffer up to last char', async () => { + const input = Array(6).join('1234567890'); + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); + assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars()); + } + }); + + 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'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); + assert.equal( + (!(i % 3)) + ? input[i] + : (i % 3 === 1) + ? input.substr(i, 2) + : input.substr(i - 1, 2), + terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars()); + } + }); + + it('should handle \t in lines correctly', async () => { + const input = '\thttps://google.de'; + 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', async () => { + const terminal = new TestTerminal({ rows: 5, cols: 10, scrollback: 5 }); + const data = [ + 'aaaaaaaaaa', + 'aaaaaaaaa\n', + 'aaaaaaaaaa', + 'aaaaaaaaa\n', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaa\n', + 'aaaaaaaaaa', + 'aaaaaaaaaa' + ]; + await terminal.writeP(data.join('')); + // brute force test with insane values + assert.doesNotThrow(() => { + for (let overscan = 0; overscan < 20; ++overscan) { + for (let start = -10; start < 20; ++start) { + for (let end = -10; end < 20; ++end) { + const it = terminal.buffer.iterator(false, start, end, overscan, overscan); + while (it.hasNext()) { + it.next(); + } + } + } + } + }); + }); + }); + + describe('Windows Mode', () => { + 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) + 'aaaaaaaaa' // not wrapped + ]; + + const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: false }); + 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 }); + 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', async () => { + const data = [ + 'aaaaaaaaaa\x1b[2;1H', // cannot wrap as it's the first + 'aaaaaaaaa\x1b[3;1H', // wrapped (windows mode only) + 'aaaaaaaaa' // not wrapped + ]; + + const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: false }); + 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 }); + 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', async () => { + // not converting + const termNotConverting = new TestTerminal({ cols: 15, rows: 10 }); + 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'); + assert.equal(termNotConverting.buffer.lines.get(1)!.translateToString(true), ' World'); + + // converting + const termConverting = new TestTerminal({ cols: 15, rows: 10, convertEol: true }); + 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'); + assert.equal(termConverting.buffer.lines.get(1)!.translateToString(true), 'World'); + }); + + // FIXME: move to common/CoreTerminal.test once the trimming is moved over + describe('marker lifecycle', () => { + // create a 10x5 terminal with markers on every line + // to test marker lifecycle under various terminal actions + let markers: IMarker[]; + let disposeStack: IMarker[]; + let term: TestTerminal; + 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)); + await term.writeP('\x1b[r0\r\n'); + markers.push(term.buffers.active.addMarker(term.buffers.active.y)); + await term.writeP('1\r\n'); + markers.push(term.buffers.active.addMarker(term.buffers.active.y)); + await term.writeP('2\r\n'); + markers.push(term.buffers.active.addMarker(term.buffers.active.y)); + await term.writeP('3\r\n'); + markers.push(term.buffers.active.addMarker(term.buffers.active.y)); + await term.writeP('4'); + for (let i = 0; i < markers.length; ++i) { + const marker = markers[i]; + marker.onDispose(() => disposeStack.push(marker)); + } + }); + it('initial', () => { + assert.deepEqual(markers.map(m => m.line), [0, 1, 2, 3, 4]); + }); + it('should dispose on normal trim off the top', async () => { + // moves top line into scrollback + await term.writeP('\n'); + assert.deepEqual(disposeStack, []); + // trims first marker + await term.writeP('\n'); + assert.deepEqual(disposeStack, [markers[0]]); + // trims second marker + 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]); + assert.deepEqual(disposeStack.map(el => (el as any)._isDisposed), [true, true]); + // trimmed markers should contain line -1 + assert.deepEqual(disposeStack.map(el => el.line), [-1, -1]); + }); + 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', 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]); + }); + it('should dispose on resize', () => { + term.resize(10, 2); + assert.deepEqual(disposeStack, [markers[0], markers[1]]); + assert.deepEqual(markers.map(el => el.line), [-1, -1, 0, 1, 2]); + }); + }); }); - // FIXME: move to common/CoreTerminal.test once the trimming is moved over - describe('marker lifecycle', () => { - // create a 10x5 terminal with markers on every line - // to test marker lifecycle under various terminal actions - let markers: IMarker[]; - let disposeStack: IMarker[]; - let term: TestTerminal; - 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)); - await term.writeP('\x1b[r0\r\n'); - markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - await term.writeP('1\r\n'); - markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - await term.writeP('2\r\n'); - markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - await term.writeP('3\r\n'); - markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - await term.writeP('4'); - for (let i = 0; i < markers.length; ++i) { - const marker = markers[i]; - marker.onDispose(() => disposeStack.push(marker)); - } - }); - it('initial', () => { - assert.deepEqual(markers.map(m => m.line), [0, 1, 2, 3, 4]); - }); - it('should dispose on normal trim off the top', async () => { - // moves top line into scrollback - await term.writeP('\n'); - assert.deepEqual(disposeStack, []); - // trims first marker - await term.writeP('\n'); - assert.deepEqual(disposeStack, [markers[0]]); - // trims second marker - 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]); - assert.deepEqual(disposeStack.map(el => (el as any)._isDisposed), [true, true]); - // trimmed markers should contain line -1 - assert.deepEqual(disposeStack.map(el => el.line), [-1, -1]); - }); - 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', 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]); - }); - it('should dispose on resize', () => { - term.resize(10, 2); - assert.deepEqual(disposeStack, [markers[0], markers[1]]); - assert.deepEqual(markers.map(el => el.line), [-1, -1, 0, 1, 2]); - }); - }); -}); + class TestLinkifier extends Linkifier { + constructor(bufferService: IBufferService, unicodeService: IUnicodeService) { + super(bufferService, new MockLogService(), unicodeService); + Linkifier._timeBeforeLatency = 0; + } -class TestLinkifier extends Linkifier { - constructor(bufferService: IBufferService, unicodeService: IUnicodeService) { - super(bufferService, new MockLogService(), unicodeService); - Linkifier._timeBeforeLatency = 0; + public get linkMatchers(): IRegisteredLinkMatcher[] { return this._linkMatchers; } + public linkifyRows(): void { super.linkifyRows(0, this._bufferService.buffer.lines.length - 1); } } - public get linkMatchers(): IRegisteredLinkMatcher[] { return this._linkMatchers; } - public linkifyRows(): void { super.linkifyRows(0, this._bufferService.buffer.lines.length - 1); } -} - -class TestMouseZoneManager implements IMouseZoneManager { - public dispose(): void { - } - public clears: number = 0; - public zones: IMouseZone[] = []; - public add(zone: IMouseZone): void { - this.zones.push(zone); - } - public clearAll(): void { - this.clears++; + class TestMouseZoneManager implements IMouseZoneManager { + public dispose(): void { + } + public clears: number = 0; + public zones: IMouseZone[] = []; + public add(zone: IMouseZone): void { + this.zones.push(zone); + } + public clearAll(): void { + this.clears++; + } } } +); diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 1ab207c7f..9d9246e38 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -147,7 +147,7 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(this._inputHandler.onRequestBell(() => this.bell())); this.register(this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end))); this.register(this._inputHandler.onRequestReset(() => this.reset())); - this.register(this._inputHandler.onRequestScroll((eraseAttr, isWrapped) => this.scroll(eraseAttr, isWrapped || undefined))); + this.register(this._inputHandler.onRequestScroll((eraseAttr, isWrapped) => this._bufferService.scroll(eraseAttr, isWrapped || undefined))); this.register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type))); this.register(this._inputHandler.onAnsiColorChange((event) => this._changeAnsiColor(event))); this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove)); @@ -1010,7 +1010,7 @@ export class Terminal extends CoreTerminal implements ITerminal { if (!this._compositionHelper!.keydown(event)) { if (this.buffer.ybase !== this.buffer.ydisp) { - this.scrollToBottom(); + this._bufferService.scrollToBottom(); } return false; } diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 936aa0c8d..9f855845f 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -58,8 +58,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { protected _inputHandler: InputHandler; private _writeBuffer: WriteBuffer; private _windowsMode: IDisposable | undefined; - /** An IBufferline to clone/copy from for new blank lines */ - private _cachedBlankLine: IBufferLine | undefined; + private _onBinary = new EventEmitter(); public get onBinary(): IEvent { return this._onBinary.event; } @@ -98,21 +97,20 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._instantiationService = new InstantiationService(); this.optionsService = new OptionsService(options); this._instantiationService.setService(IOptionsService, this.optionsService); - this._bufferService = this.register(this._instantiationService.createInstance(BufferService)); - this._instantiationService.setService(IBufferService, this._bufferService); this._logService = this._instantiationService.createInstance(LogService); this._instantiationService.setService(ILogService, this._logService); - this._coreService = this.register(this._instantiationService.createInstance(CoreService, () => this.scrollToBottom())); - this._instantiationService.setService(ICoreService, this._coreService); - this._coreMouseService = this._instantiationService.createInstance(CoreMouseService); - this._instantiationService.setService(ICoreMouseService, this._coreMouseService); - this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); - this._instantiationService.setService(IDirtyRowService, this._dirtyRowService); this.unicodeService = this._instantiationService.createInstance(UnicodeService); this._instantiationService.setService(IUnicodeService, this.unicodeService); this._charsetService = this._instantiationService.createInstance(CharsetService); this._instantiationService.setService(ICharsetService, this._charsetService); - + this._bufferService = this.register(this._instantiationService.createInstance(BufferService)); + this._instantiationService.setService(IBufferService, this._bufferService); + this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); + this._instantiationService.setService(IDirtyRowService, this._dirtyRowService); + this._coreService = this.register(this._instantiationService.createInstance(CoreService, () => this._bufferService.scrollToBottom())); + this._instantiationService.setService(ICoreService, this._coreService); + this._coreMouseService = this._instantiationService.createInstance(CoreMouseService); + this._instantiationService.setService(ICoreMouseService, this._coreMouseService); // Register input handler and handle/forward events this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService, this.unicodeService); this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed)); @@ -137,6 +135,23 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._windowsMode = undefined; } + public scrollLines(disp: number, suppressScrollEvent?: boolean): void { + this._bufferService.scrollLines(disp, suppressScrollEvent); + } + + public scrollPages(pageCount: number): void { + this._bufferService.scrollPages(pageCount); + } + public scrollToTop(): void { + this._bufferService.scrollToTop(); + } + public scrollToBottom(): void { + this._bufferService.scrollToBottom(); + } + public scrollToLine(line: number): void { + this._bufferService.scrollToLine(line); + } + public write(data: string | Uint8Array, callback?: () => void): void { this._writeBuffer.write(data, callback); } diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index b5067193d..93de6562a 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -18,6 +18,7 @@ import { clone } from 'common/Clone'; import { BufferService } from 'common/services/BufferService'; import { CoreService } from 'common/services/CoreService'; import { OscHandler } from 'common/parser/OscParser'; +import { DirtyRowService } from 'common/services/DirtyRowService'; function getCursor(bufferService: IBufferService): number[] { return [ @@ -66,11 +67,119 @@ describe('InputHandler', () => { optionsService = new MockOptionsService(); bufferService = new BufferService(optionsService); bufferService.resize(80, 30); - coreService = new CoreService(() => {}, bufferService, new MockLogService(), optionsService); + coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService); inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService()); }); + describe('Terminal InputHandler integration', () => { + function getLines(limit: number): string[] { + const res: string[] = []; + for (let i = 0; i < limit; ++i) { + res.push(bufferService.buffers.active.lines.get(i)!.translateToString(true)); + } + return res; + } + + // This suite cannot live in InputHandler unless Terminal.scroll moved into IBufferService + describe('SL/SR/DECIC/DECDC', () => { + + it('SL (scrollLeft)', async () => { + inputHandler.parseP('12345'.repeat(6)); + assert.deepEqual(getLines(5), ['12345', '2345', '2345', '2345', '2345', '2345']); + inputHandler.parseP('\x1b[0 @'); + assert.deepEqual(getLines(5), ['12345', '345', '345', '345', '345', '345']); + inputHandler.parseP('\x1b[2 @'); + assert.deepEqual(getLines(5), ['12345', '5', '5', '5', '5', '5']); + }); + it('SR (scrollRight)', async () => { + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[ A'); + assert.deepEqual(getLines(5), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']); + inputHandler.parseP('\x1b[0 A'); + assert.deepEqual(getLines(5), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']); + inputHandler.parseP('\x1b[2 A'); + assert.deepEqual(getLines(5), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']); + }); + it('insertColumns (DECIC)', async () => { + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[\'}'); + assert.deepEqual(getLines(5), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); + inputHandler.reset(); + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[1\'}'); + assert.deepEqual(getLines(5), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); + inputHandler.reset(); + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[2\'}'); + assert.deepEqual(getLines(5), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']); + }); + it('deleteColumns (DECDC)', async () => { + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[\'~'); + assert.deepEqual(getLines(5), ['12345', '1245', '1245', '1245', '1245', '1245']); + inputHandler.reset(); + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[1\'~'); + assert.deepEqual(getLines(5), ['12345', '1245', '1245', '1245', '1245', '1245']); + inputHandler.reset(); + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[2\'~'); + assert.deepEqual(getLines(5), ['12345', '125', '125', '125', '125', '125']); + }); + }); + + describe('BS with reverseWraparound set/unset', () => { + const ttyBS = '\x08 \x08'; // tty ICANON sends on pressing BS + + describe('reverseWraparound set', () => { + it('should not reverse outside of scroll margins', async () => { + // prepare buffer content + inputHandler.parseP('#####abcdefghijklmnopqrstuvwxy'); + assert.deepEqual(getLines(5), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', 'uvwxy']); + assert.equal(bufferService.buffers.active.ydisp, 1); + assert.equal(bufferService.buffers.active.x, 5); + assert.equal(bufferService.buffers.active.y, 4); + inputHandler.parseP(ttyBS.repeat(100)); + assert.deepEqual(getLines(5), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']); + + inputHandler.parseP('\x1b[?45h'); + inputHandler.parseP('uvwxy'); + + // set top/bottom to 1/3 (0-based) + inputHandler.parseP('\x1b[2;4r'); + // place cursor below scroll bottom + bufferService.buffers.active.x = 5; + bufferService.buffers.active.y = 4; + inputHandler.parseP(ttyBS.repeat(100)); + assert.deepEqual(getLines(5), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']); + + inputHandler.parseP('uvwxy'); + // place cursor within scroll margins + bufferService.buffers.active.x = 5; + bufferService.buffers.active.y = 3; + inputHandler.parseP(ttyBS.repeat(100)); + assert.deepEqual(getLines(5), ['#####', 'abcde', ' ', ' ', ' ', 'uvwxy']); + assert.equal(bufferService.buffers.active.x, 0); + assert.equal(bufferService.buffers.active.y, bufferService.buffers.active.scrollTop); // stops at 0, scrollTop + + inputHandler.parseP('fghijklmnopqrst'); + // place cursor above scroll top + bufferService.buffers.active.x = 5; + bufferService.buffers.active.y = 0; + inputHandler.parseP(ttyBS.repeat(100)); + assert.deepEqual(getLines(5), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']); + }); + }); + }); + }); + it('save and restore cursor', () => { bufferService.buffer.x = 1; bufferService.buffer.y = 2; @@ -140,7 +249,7 @@ describe('InputHandler', () => { assert.equal(coreService.decPrivateModes.bracketedPasteMode, false); }); }); - describe('regression tests', function(): void { + describe('regression tests', function (): void { function termContent(bufferService: IBufferService, trim: boolean): string[] { const result = []; for (let i = 0; i < bufferService.rows; ++i) result.push(bufferService.buffer.lines.get(i)!.translateToString(trim)); @@ -430,6 +539,68 @@ describe('InputHandler', () => { await inputHandler.parseP('¥¥¥'); assert.deepEqual(getLines(bufferService, 2), ['¥¥', '¥']); }); + + + // This suite cannot live in InputHandler unless Terminal.scroll moved into IBufferService + describe('SL/SR/DECIC/DECDC', () => { + it('SL (scrollLeft)', async () => { + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[ @'); + assert.deepEqual(getLines(bufferService, 5), ['12345', '2345', '2345', '2345', '2345', '2345']); + inputHandler.parseP('\x1b[0 @'); + assert.deepEqual(getLines(bufferService, 5), ['12345', '345', '345', '345', '345', '345']); + inputHandler.parseP('\x1b[2 @'); + assert.deepEqual(getLines(bufferService, 5), ['12345', '5', '5', '5', '5', '5']); + }); + it('SR (scrollRight)', async () => { + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[ A'); + assert.deepEqual(getLines(bufferService, 5), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']); + inputHandler.parseP('\x1b[0 A'); + assert.deepEqual(getLines(bufferService, 5), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']); + inputHandler.parseP('\x1b[2 A'); + assert.deepEqual(getLines(bufferService, 5), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']); + }); + it('insertColumns (DECIC)', async () => { + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[\'}'); + assert.deepEqual(getLines(bufferService, 5), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); + inputHandler.reset(); + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[1\'}'); + assert.deepEqual(getLines(bufferService, 5), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); + inputHandler.reset(); + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[2\'}'); + assert.deepEqual(getLines(bufferService, 5), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']); + }); + it('deleteColumns (DECDC)', async () => { + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[\'~'); + assert.deepEqual(getLines(bufferService, 5), ['12345', '1245', '1245', '1245', '1245', '1245']); + inputHandler.reset(); + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[1\'~'); + assert.deepEqual(getLines(bufferService, 5), ['12345', '1245', '1245', '1245', '1245', '1245']); + inputHandler.reset(); + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[2\'~'); + assert.deepEqual(getLines(bufferService, 5), ['12345', '125', '125', '125', '125', '125']); + }); + }); + it('should fire the onScroll event', (done) => { + bufferService.onScroll(e => { + assert.equal(typeof e, 'number'); + done(); + }); + bufferService.scroll(DEFAULT_ATTR_DATA.clone()); + }); }); describe('alt screen', () => { @@ -1240,7 +1411,7 @@ describe('InputHandler', () => { await inputHandler.parseP('\x1b[6H\x1b[2Mm'); assert.deepEqual(getLines(bufferService), ['0', '1', '2', '3', '4', 'm', '6', '7', '8', '9']); await inputHandler.parseP('\x1b[3H\x1b[2Mn'); - assert.deepEqual(getLines(bufferService), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']); + assert.deepEqual(getLines(bufferService), ['0', '1', 'n', 'm', '', '', '6', '7', '8', '9']); }); }); it('should parse big chunks in smaller subchunks', async () => { @@ -1814,13 +1985,14 @@ describe('InputHandler - async handlers', () => { let bufferService: IBufferService; let coreService: ICoreService; let optionsService: MockOptionsService; + let dirtyRowService: MockDirtyRowService; let inputHandler: TestInputHandler; beforeEach(() => { optionsService = new MockOptionsService(); bufferService = new BufferService(optionsService); bufferService.resize(80, 30); - coreService = new CoreService(() => {}, bufferService, new MockLogService(), optionsService); + coreService = new CoreService(() => { }, bufferService, new MockLogService(), optionsService); coreService.onData(data => { console.log(data); }); inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService()); @@ -1829,7 +2001,7 @@ describe('InputHandler - async handlers', () => { it('async CUP with CPR check', async () => { const cup: number[][] = []; const cpr: number[][] = []; - inputHandler.registerCsiHandler({final: 'H'}, async params => { + inputHandler.registerCsiHandler({ final: 'H' }, async params => { cup.push(params.toArray() as number[]); await new Promise(res => setTimeout(res, 50)); // late call of real repositioning @@ -1855,7 +2027,7 @@ describe('InputHandler - async handlers', () => { assert.deepEqual(getLines(bufferService, 2), ['hello world!', 'second line']); }); it('async DCS between', async () => { - inputHandler.registerDcsHandler({final: 'a'}, async (data, params) => { + inputHandler.registerDcsHandler({ final: 'a' }, async (data, params) => { await new Promise(res => setTimeout(res, 50)); assert.deepEqual(getLines(bufferService, 2), ['hello world!', '']); assert.equal(data, 'some data'); diff --git a/src/common/TestUtils.test.ts b/src/common/TestUtils.test.ts index 01ceacbd2..dce0f570a 100644 --- a/src/common/TestUtils.test.ts +++ b/src/common/TestUtils.test.ts @@ -9,7 +9,7 @@ import { clone } from 'common/Clone'; import { DEFAULT_OPTIONS } from 'common/services/OptionsService'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { BufferSet } from 'common/buffer/BufferSet'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEventType, ICharset, IModes, IAttributeData } from 'common/Types'; import { UnicodeV6 } from 'common/input/UnicodeV6'; export class MockBufferService implements IBufferService { @@ -17,6 +17,7 @@ export class MockBufferService implements IBufferService { public get buffer(): IBuffer { return this.buffers.active; } public buffers: IBufferSet = {} as any; public onResize: IEvent<{ cols: number, rows: number }> = new EventEmitter<{ cols: number, rows: number }>().event; + public onScroll: IEvent = new EventEmitter().event; public isUserScrolling: boolean = false; constructor( public cols: number, @@ -25,23 +26,41 @@ export class MockBufferService implements IBufferService { ) { this.buffers = new BufferSet(optionsService, this); } + public scrollPages(pageCount: number): void { + throw new Error('Method not implemented.'); + } + public scrollToTop(): void { + throw new Error('Method not implemented.'); + } + public scrollToLine(line: number): void { + throw new Error('Method not implemented.'); + } + public scroll(eraseAttr: IAttributeData, isWrapped: boolean): void { + throw new Error('Method not implemented.'); + } + public scrollToBottom(): void { + throw new Error('Method not implemented.'); + } + public scrollLines(disp: number, suppressScrollEvent?: boolean): void { + throw new Error('Method not implemented.'); + } public resize(cols: number, rows: number): void { this.cols = cols; this.rows = rows; } - public reset(): void {} + public reset(): void { } } export class MockCoreMouseService implements ICoreMouseService { public areMouseEventsActive: boolean = false; public activeEncoding: string = ''; public activeProtocol: string = ''; - public addEncoding(name: string): void {} - public addProtocol(name: string): void {} - public reset(): void {} + public addEncoding(name: string): void { } + public addProtocol(name: string): void { } + public reset(): void { } public triggerMouseEvent(event: ICoreMouseEvent): boolean { return false; } public onProtocolChange: IEvent = new EventEmitter().event; - public explainEvents(events: CoreMouseEventType): {[event: string]: boolean} { + public explainEvents(events: CoreMouseEventType): { [event: string]: boolean } { throw new Error('Method not implemented.'); } } @@ -50,9 +69,9 @@ export class MockCharsetService implements ICharsetService { public serviceBrand: any; public charset: ICharset | undefined; public glevel: number = 0; - public reset(): void {} - public setgLevel(g: number): void {} - public setgCharset(g: number, charset: ICharset): void {} + public reset(): void { } + public setgLevel(g: number): void { } + public setgCharset(g: number, charset: ICharset): void { } } export class MockCoreService implements ICoreService { @@ -75,28 +94,28 @@ export class MockCoreService implements ICoreService { public onData: IEvent = new EventEmitter().event; public onUserInput: IEvent = new EventEmitter().event; public onBinary: IEvent = new EventEmitter().event; - public reset(): void {} - public triggerDataEvent(data: string, wasUserInput?: boolean): void {} - public triggerBinaryEvent(data: string): void {} + public reset(): void { } + public triggerDataEvent(data: string, wasUserInput?: boolean): void { } + public triggerBinaryEvent(data: string): void { } } export class MockDirtyRowService implements IDirtyRowService { public serviceBrand: any; public start: number = 0; public end: number = 0; - public clearRange(): void {} - public markDirty(y: number): void {} - public markRangeDirty(y1: number, y2: number): void {} - public markAllDirty(): void {} + public clearRange(): void { } + public markDirty(y: number): void { } + public markRangeDirty(y1: number, y2: number): void { } + public markAllDirty(): void { } } 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 {} - public error(message: any, ...optionalParams: any[]): void {} + public debug(message: any, ...optionalParams: any[]): void { } + public info(message: any, ...optionalParams: any[]): void { } + public warn(message: any, ...optionalParams: any[]): void { } + public error(message: any, ...optionalParams: any[]): void { } } export class MockOptionsService implements IOptionsService { diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 47e547290..7fe0cdc38 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -3,11 +3,13 @@ * @license MIT */ -import { IBufferService, IOptionsService } from 'common/services/Services'; +import { IBufferService, IDirtyRowService, IInstantiationService, IOptionsService } from 'common/services/Services'; import { BufferSet } from 'common/buffer/BufferSet'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; +import { IAttributeData, IBufferLine } from 'common/Types'; +import { DirtyRowService } from 'common/services/DirtyRowService'; export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars export const MINIMUM_ROWS = 1; @@ -23,9 +25,16 @@ export class BufferService extends Disposable implements IBufferService { private _onResize = new EventEmitter<{ cols: number, rows: number }>(); public get onResize(): IEvent<{ cols: number, rows: number }> { return this._onResize.event; } + private _onScroll = new EventEmitter(); + public get onScroll(): IEvent { return this._onScroll.event; } public get buffer(): IBuffer { return this.buffers.active; } + /** An IBufferline to clone/copy from for new blank lines */ + private _cachedBlankLine: IBufferLine | undefined; + + private _dirtyRowService: IDirtyRowService | undefined; + constructor( @IOptionsService private _optionsService: IOptionsService ) { @@ -52,4 +61,134 @@ export class BufferService extends Disposable implements IBufferService { this.buffers.reset(); this.isUserScrolling = false; } + + /** + * Scroll the terminal down 1 row, creating a blank line. + * @param isWrapped Whether the new line is wrapped from the previous line. + */ + public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void { + const buffer = this.buffer; + + let newLine: IBufferLine | undefined; + newLine = this._cachedBlankLine; + if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) { + newLine = buffer.getBlankLine(eraseAttr, isWrapped); + this._cachedBlankLine = newLine; + } + newLine.isWrapped = isWrapped; + + const topRow = buffer.ybase + buffer.scrollTop; + const bottomRow = buffer.ybase + buffer.scrollBottom; + + if (buffer.scrollTop === 0) { + // Determine whether the buffer is going to be trimmed after insertion. + const willBufferBeTrimmed = buffer.lines.isFull; + + // Insert the line using the fastest method + if (bottomRow === buffer.lines.length - 1) { + if (willBufferBeTrimmed) { + buffer.lines.recycle().copyFrom(newLine); + } else { + buffer.lines.push(newLine.clone()); + } + } else { + buffer.lines.splice(bottomRow + 1, 0, newLine.clone()); + } + + // Only adjust ybase and ydisp when the buffer is not trimmed + if (!willBufferBeTrimmed) { + buffer.ybase++; + // Only scroll the ydisp with ybase if the user has not scrolled up + if (!this.isUserScrolling) { + buffer.ydisp++; + } + } else { + // When the buffer is full and the user has scrolled up, keep the text + // stable unless ydisp is right at the top + if (this.isUserScrolling) { + buffer.ydisp = Math.max(buffer.ydisp - 1, 0); + } + } + } else { + // scrollTop is non-zero which means no line will be going to the + // scrollback, instead we can just shift them in-place. + const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */; + buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); + buffer.lines.set(bottomRow, newLine.clone()); + } + + // Move the viewport to the bottom of the buffer unless the user is + // scrolling. + if (!this.isUserScrolling) { + buffer.ydisp = buffer.ybase; + } + + // Flag rows that need updating + if (!this._dirtyRowService) { + this._dirtyRowService = new DirtyRowService(this); + } + this._dirtyRowService?.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); + + this._onScroll.fire(buffer.ydisp); + } + + /** + * Scroll the display of the terminal + * @param disp The number of lines to scroll down (negative scroll up). + * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used + * to avoid unwanted events being handled by the viewport when the event was triggered from the + * viewport originally. + */ + public scrollLines(disp: number, suppressScrollEvent?: boolean): void { + const buffer = this.buffer; + if (disp < 0) { + if (buffer.ydisp === 0) { + return; + } + this.isUserScrolling = true; + } else if (disp + buffer.ydisp >= buffer.ybase) { + this.isUserScrolling = false; + } + + const oldYdisp = buffer.ydisp; + buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0); + + // No change occurred, don't trigger scroll/refresh + if (oldYdisp === buffer.ydisp) { + return; + } + + if (!suppressScrollEvent) { + this._onScroll.fire(buffer.ydisp); + } + } + + /** + * Scroll the display of the terminal by a number of pages. + * @param pageCount The number of pages to scroll (negative scrolls up). + */ + public scrollPages(pageCount: number): void { + this.scrollLines(pageCount * (this.rows - 1)); + } + + /** + * Scrolls the display of the terminal to the top. + */ + public scrollToTop(): void { + this.scrollLines(-this.buffer.ydisp); + } + + /** + * Scrolls the display of the terminal to the bottom. + */ + public scrollToBottom(): void { + this.scrollLines(this.buffer.ybase - this.buffer.ydisp); + } + + public scrollToLine(line: number): void { + const scrollAmount = line - this.buffer.ydisp; + if (scrollAmount !== 0) { + this.scrollLines(scrollAmount); + } + } } diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 1fbf57fbe..40c0c1b2d 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; export const IBufferService = createDecorator('BufferService'); @@ -17,9 +17,14 @@ export interface IBufferService { readonly buffer: IBuffer; readonly buffers: IBufferSet; isUserScrolling: boolean; - onResize: IEvent<{ cols: number, rows: number }>; - + onScroll: IEvent; + scroll(eraseAttr: IAttributeData, isWrapped?: boolean): void; + scrollToBottom(): void; + scrollToTop(): void; + scrollToLine(line: number): void; + scrollLines(disp: number, suppressScrollEvent?: boolean): void; + scrollPages(pageCount: number): void; resize(cols: number, rows: number): void; reset(): void; } From b074c2552a2cb71a2bc161c3c760043512418a98 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 1 Apr 2021 09:49:01 -0700 Subject: [PATCH 03/15] use bufferService in terminal to reduce code a bunch --- src/browser/Terminal.test.ts | 2202 +++++++++++++++++-------------- src/common/CoreTerminal.ts | 137 +- src/common/InputHandler.test.ts | 103 +- 3 files changed, 1275 insertions(+), 1167 deletions(-) diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts index f3bcdfda7..8f10e6224 100644 --- a/src/browser/Terminal.test.ts +++ b/src/browser/Terminal.test.ts @@ -117,6 +117,13 @@ describe('Terminal', () => { }); term.resize(1, 1); }); + it('should fire the onScroll event', (done) => { + term.onScroll(e => { + assert.equal(typeof e, 'number'); + done(); + }); + term.scroll(DEFAULT_ATTR_DATA.clone()); + }); it('should fire the onTitleChange event', (done) => { term.onTitleChange(e => { assert.equal(e, 'title'); @@ -273,991 +280,1244 @@ describe('Terminal', () => { }); }); - describe('Third level shift', () => { - let evKeyDown: any; - let evKeyPress: any; - - beforeEach(() => { - term.clearSelection = () => { }; - // term.compositionHelper = { - // isComposing: false, - // keydown: { - // bind: () => { - // return () => { return true; }; - // } - // } - // }; - evKeyDown = { - preventDefault: () => { }, - stopPropagation: () => { }, - type: 'keydown', - altKey: null, - keyCode: null - }; - evKeyPress = { - preventDefault: () => { }, - stopPropagation: () => { }, - type: 'keypress', - altKey: null, - charCode: null, - keyCode: null - }; - }); - - describe('with macOptionIsMeta', () => { - let originalIsMac: boolean; - beforeEach(() => { - originalIsMac = term.browser.isMac; - term.options.macOptionIsMeta = true; - }); - afterEach(() => term.browser.isMac = originalIsMac); - - it('should interfere with the alt key on keyDown', () => { - evKeyDown.altKey = true; - evKeyDown.keyCode = 81; - assert.equal(term.keyDown(evKeyDown), false); - evKeyDown.altKey = true; - evKeyDown.keyCode = 192; - assert.equal(term.keyDown(evKeyDown), false); - }); - }); - - describe('On Mac OS', () => { - let originalIsMac: boolean; - beforeEach(() => { - originalIsMac = term.browser.isMac; - term.browser.isMac = true; - }); - afterEach(() => term.browser.isMac = originalIsMac); - - it('should not interfere with the alt key on keyDown', () => { - evKeyDown.altKey = true; - evKeyDown.keyCode = 81; - assert.equal(term.keyDown(evKeyDown), true); - evKeyDown.altKey = true; - evKeyDown.keyCode = 192; - term.keyDown(evKeyDown); - assert.equal(term.keyDown(evKeyDown), true); - }); - - it('should interfere with the alt + arrow keys', () => { - evKeyDown.altKey = true; - evKeyDown.keyCode = 37; - assert.equal(term.keyDown(evKeyDown), false); - evKeyDown.altKey = true; - evKeyDown.keyCode = 39; - assert.equal(term.keyDown(evKeyDown), false); - }); - - it('should emit key with alt + key on keyPress', (done) => { - const keys = ['@', '@', '\\', '\\', '|', '|']; - - term.onKey(e => { - if (e.key) { - const index = keys.indexOf(e.key); - assert(index !== -1, 'Emitted wrong key: ' + e.key); - keys.splice(index, 1); - } - if (keys.length === 0) done(); - }); - - evKeyPress.altKey = true; - // @ - evKeyPress.charCode = null; - evKeyPress.keyCode = 64; - term.keyPress(evKeyPress); - // Firefox @ - evKeyPress.charCode = 64; - evKeyPress.keyCode = 0; - term.keyPress(evKeyPress); - // \ - evKeyPress.charCode = null; - evKeyPress.keyCode = 92; - term.keyPress(evKeyPress); - // Firefox \ - evKeyPress.charCode = 92; - evKeyPress.keyCode = 0; - term.keyPress(evKeyPress); - // | - evKeyPress.charCode = null; - evKeyPress.keyCode = 124; - term.keyPress(evKeyPress); - // Firefox | - evKeyPress.charCode = 124; - evKeyPress.keyCode = 0; - term.keyPress(evKeyPress); - }); - }); - - describe('On MS Windows', () => { - let originalIsWindows: boolean; - beforeEach(() => { - originalIsWindows = term.browser.isWindows; - term.browser.isWindows = true; - }); - afterEach(() => term.browser.isWindows = originalIsWindows); - - it('should not interfere with the alt + ctrl key on keyDown', () => { - evKeyPress.altKey = true; - evKeyPress.ctrlKey = true; - evKeyPress.keyCode = 81; - assert.equal(term.keyDown(evKeyPress), true); - evKeyDown.altKey = true; - evKeyDown.ctrlKey = true; - evKeyDown.keyCode = 81; - term.keyDown(evKeyDown); - assert.equal(term.keyDown(evKeyPress), true); - }); - - it('should interfere with the alt + ctrl + arrow keys', () => { - evKeyDown.altKey = true; - evKeyDown.ctrlKey = true; - - evKeyDown.keyCode = 37; - assert.equal(term.keyDown(evKeyDown), false); - evKeyDown.keyCode = 39; - term.keyDown(evKeyDown); - assert.equal(term.keyDown(evKeyDown), false); - }); - - it('should emit key with alt + ctrl + key on keyPress', (done) => { - const keys = ['@', '@', '\\', '\\', '|', '|']; - - term.onKey(e => { - if (e.key) { - const index = keys.indexOf(e.key); - assert(index !== -1, 'Emitted wrong key: ' + e.key); - keys.splice(index, 1); - } - if (keys.length === 0) done(); - }); - - evKeyPress.altKey = true; - evKeyPress.ctrlKey = true; - - // @ - evKeyPress.charCode = null; - evKeyPress.keyCode = 64; - term.keyPress(evKeyPress); - // Firefox @ - evKeyPress.charCode = 64; - evKeyPress.keyCode = 0; - term.keyPress(evKeyPress); - // \ - evKeyPress.charCode = null; - evKeyPress.keyCode = 92; - term.keyPress(evKeyPress); - // Firefox \ - evKeyPress.charCode = 92; - evKeyPress.keyCode = 0; - term.keyPress(evKeyPress); - // | - evKeyPress.charCode = null; - evKeyPress.keyCode = 124; - term.keyPress(evKeyPress); - // Firefox | - evKeyPress.charCode = 124; - evKeyPress.keyCode = 0; - term.keyPress(evKeyPress); - }); - }); - }); - - describe('unicode - surrogates', () => { - it('2 characters per cell', async function (): Promise { - 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) { - 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); - assert.equal(tchar.getWidth(), 1); - assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), ''); - term.reset(); - } - }); - 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; - 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', async function (): Promise { - 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; - - 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); - assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), ''); - term.reset(); - } - }); - it('2 characters per cell over line end without autowrap', async function (): Promise { - 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; - await term.writeP('\x1b[?7l'); // Disable wraparound mode - const width = wcwidth((0xD800 - 0xD800) * 0x400 + i - 0xDC00 + 0x10000); - if (width !== 1) { - continue; - } - 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); - assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), ''); - term.reset(); - } - }); - it('splitted surrogates', async function (): Promise { - this.timeout(10000); - const high = String.fromCharCode(0xD800); - const cell = new CellData(); - for (let i = 0xDC00; i <= 0xDCFF; ++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); - assert.equal(tchar.getWidth(), 1); - assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), ''); - term.reset(); - } - }); - }); - - describe('unicode - combining characters', () => { - const cell = new CellData(); - 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', async () => { - term.buffer.x = term.cols - 1 - 3; - 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); - assert.equal(cell.getWidth(), 1); - term.buffer.lines.get(0)!.loadCell(1, cell); - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 1); - }); - 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'); - assert.equal(cell.getChars().length, 2); - assert.equal(cell.getWidth(), 1); - } - term.buffer.lines.get(1)!.loadCell(0, cell); - assert.equal(cell.getChars(), 'e\u0301'); - assert.equal(cell.getChars().length, 2); - assert.equal(cell.getWidth(), 1); - }); - 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'); - assert.equal(cell.getChars().length, 3); - assert.equal(cell.getWidth(), 1); - } - term.buffer.lines.get(1)!.loadCell(0, cell); - assert.equal(cell.getChars(), '\uD800\uDC00\u0301'); - assert.equal(cell.getChars().length, 3); - assert.equal(cell.getWidth(), 1); - }); - }); - - describe('unicode - fullwidth characters', () => { - const cell = new CellData(); - it('cursor movement even', async () => { - assert.equal(term.buffer.x, 0); - await term.writeP('¥'); - assert.equal(term.buffer.x, 2); - }); - it('cursor movement odd', async () => { - term.buffer.x = 1; - assert.equal(term.buffer.x, 1); - await term.writeP('¥'); - assert.equal(term.buffer.x, 3); - }); - 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) { - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 0); - } else { - assert.equal(cell.getChars(), '¥'); - assert.equal(cell.getChars().length, 1); - assert.equal(cell.getWidth(), 2); - } - } - term.buffer.lines.get(1)!.loadCell(0, cell); - assert.equal(cell.getChars(), '¥'); - assert.equal(cell.getChars().length, 1); - assert.equal(cell.getWidth(), 2); - }); - it('line of ¥ odd', async () => { - term.buffer.x = 1; - 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)) { - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 0); - } else { - assert.equal(cell.getChars(), '¥'); - assert.equal(cell.getChars().length, 1); - assert.equal(cell.getWidth(), 2); - } - } - term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 1); - term.buffer.lines.get(1)!.loadCell(0, cell); - assert.equal(cell.getChars(), '¥'); - assert.equal(cell.getChars().length, 1); - assert.equal(cell.getWidth(), 2); - }); - it('line of ¥ with combining odd', async () => { - term.buffer.x = 1; - 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)) { - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 0); - } else { - assert.equal(cell.getChars(), '¥\u0301'); - assert.equal(cell.getChars().length, 2); - assert.equal(cell.getWidth(), 2); - } - } - term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 1); - term.buffer.lines.get(1)!.loadCell(0, cell); - assert.equal(cell.getChars(), '¥\u0301'); - assert.equal(cell.getChars().length, 2); - assert.equal(cell.getWidth(), 2); - }); - 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) { - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 0); - } else { - assert.equal(cell.getChars(), '¥\u0301'); - assert.equal(cell.getChars().length, 2); - assert.equal(cell.getWidth(), 2); - } - } - term.buffer.lines.get(1)!.loadCell(0, cell); - assert.equal(cell.getChars(), '¥\u0301'); - assert.equal(cell.getChars().length, 2); - assert.equal(cell.getWidth(), 2); - }); - it('line of surrogate fullwidth with combining odd', async () => { - term.buffer.x = 1; - 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)) { - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 0); - } else { - assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); - assert.equal(cell.getChars().length, 3); - assert.equal(cell.getWidth(), 2); - } - } - term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 1); - term.buffer.lines.get(1)!.loadCell(0, cell); - assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); - assert.equal(cell.getChars().length, 3); - assert.equal(cell.getWidth(), 2); - }); - 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) { - assert.equal(cell.getChars(), ''); - assert.equal(cell.getChars().length, 0); - assert.equal(cell.getWidth(), 0); - } else { - assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); - assert.equal(cell.getChars().length, 3); - assert.equal(cell.getWidth(), 2); - } - } - term.buffer.lines.get(1)!.loadCell(0, cell); - assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); - assert.equal(cell.getChars().length, 3); - assert.equal(cell.getWidth(), 2); - }); - }); - - describe('insert mode', () => { - const cell = new CellData(); - 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'); - 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', async () => { - await term.writeP(Array(9).join('0123456789').slice(-80)); - term.buffer.x = 10; - term.buffer.y = 0; - term.write('\x1b[4h'); - 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(), ''); - assert.equal(term.buffer.lines.get(0)!.loadCell(14, cell).getChars(), '¥'); - 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', async () => { - await term.writeP(Array(41).join('¥')); - term.buffer.x = 10; - term.buffer.y = 0; - term.write('\x1b[4h'); - 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 - 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(), '¥'); - assert.equal(term.buffer.lines.get(0)!.loadCell(79, cell).getChars(), ''); // empty cell after fullwidth - }); - }); - - describe('Linkifier unicode handling', () => { - let terminal: TestTerminal; - let linkifier: TestLinkifier; - let mouseZoneManager: TestMouseZoneManager; - - // other than the tests above unicode testing needs the full terminal instance - // to get the special handling of fullwidth, surrogate and combining chars in the input handler - beforeEach(() => { - terminal = new TestTerminal({ cols: 10, rows: 5 }); - linkifier = new TestLinkifier((terminal as any)._bufferService, terminal.unicodeService); - mouseZoneManager = new TestMouseZoneManager(); - linkifier.attachToDom({} as any, mouseZoneManager); - }); - - function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: { x1: number, y1: number, x2: number, y2: number }[]): Promise { - 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', () => { - return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]); - }); - 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', () => { - return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]); - }); - 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', () => { - return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]); - }); - 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', () => { - return assertLinkifiesInTerminal('12 foo', /foo/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); - }); - 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', () => { - return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); - }); - 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', () => { - return assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]); - }); - 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', () => { - return assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); - }); - 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', () => { - return assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); - }); - 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', () => { - return assertLinkifiesInTerminal('test a1b', /a1b/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]); - }); - 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', () => { - return assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]); - }); - it('combining fullwidth - match over two lines', () => { - return assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]); - }); - }); - }); - - describe('Buffer.stringIndexToBufferIndex', () => { - let terminal: TestTerminal; - - beforeEach(() => { - terminal = new TestTerminal({ rows: 5, cols: 10, scrollback: 5 }); - }); - - it('multiline ascii', async () => { - const input = 'This is ASCII text spanning multiple lines.'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - } - }); - - it('combining e\u0301 in a sentence', async () => { - const input = 'Sitting in the cafe\u0301 drinking coffee.'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < 19; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - } - // string index 18 & 19 point to combining char e\u0301 ---> same buffer Index - assert.deepEqual( - terminal.buffer.stringIndexToBufferIndex(0, 18), - terminal.buffer.stringIndexToBufferIndex(0, 19)); - // after the combining char every string index has an offset of -1 - for (let i = 19; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); - } - }); - - it('multiline combining e\u0301', async () => { - const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - // every buffer cell index contains 2 string indices - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); - } - }); - - it('surrogate char in a sentence', async () => { - const input = 'The 𝄞 is a clef widely used in modern notation.'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < 5; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - } - // string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index - assert.deepEqual( - terminal.buffer.stringIndexToBufferIndex(0, 4), - terminal.buffer.stringIndexToBufferIndex(0, 5)); - // after the combining char every string index has an offset of -1 - for (let i = 5; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); - } - }); - - it('multiline surrogate char', async () => { - const 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 - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); - } - }); - - 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.'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - // index 0..2 should map to 0 - assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1)); - assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2)); - for (let i = 2; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex); - } - }); - - it('multiline surrogate with combining', async () => { - const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - // every buffer cell index contains 3 string indices - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex); - } - }); - - it('fullwidth chars', async () => { - const input = 'These 123 are some fat numbers.'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < 6; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); - } - // string index 6, 7, 8 take 2 cells - assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7)); - assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8)); - // rest of the string has offset of +3 - for (let i = 9; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex); - } - }); - - it('multiline fullwidth chars', async () => { - const input = '12345678901234567890'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 9; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); - assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex); - } - }); - - it('fullwidth combining with emoji - match emoji cell', async () => { - const input = 'Lots of ¥\u0301 make me 😃.'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - const stringIndex = s.match(/😃/)!.index!; - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex); - 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)', 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 - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 10; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - const j = (i - 0) << 1; - assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); - } - }); - - it('test fully wrapped buffer up to last char', async () => { - const input = Array(6).join('1234567890'); - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars()); - } - }); - - 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'; - await terminal.writeP(input); - const s = terminal.buffer.iterator(true).next().content; - assert.equal(input, s); - for (let i = 0; i < input.length; ++i) { - const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); - assert.equal( - (!(i % 3)) - ? input[i] - : (i % 3 === 1) - ? input.substr(i, 2) - : input.substr(i - 1, 2), - terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars()); - } - }); - - it('should handle \t in lines correctly', async () => { - const input = '\thttps://google.de'; - 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', async () => { - const terminal = new TestTerminal({ rows: 5, cols: 10, scrollback: 5 }); - const data = [ - 'aaaaaaaaaa', - 'aaaaaaaaa\n', - 'aaaaaaaaaa', - 'aaaaaaaaa\n', - 'aaaaaaaaaa', - 'aaaaaaaaaa', - 'aaaaaaaaaa', - 'aaaaaaaaa\n', - 'aaaaaaaaaa', - 'aaaaaaaaaa' - ]; - await terminal.writeP(data.join('')); - // brute force test with insane values - assert.doesNotThrow(() => { - for (let overscan = 0; overscan < 20; ++overscan) { - for (let start = -10; start < 20; ++start) { - for (let end = -10; end < 20; ++end) { - const it = terminal.buffer.iterator(false, start, end, overscan, overscan); - while (it.hasNext()) { - it.next(); - } - } - } - } - }); - }); - }); - - describe('Windows Mode', () => { - 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) - 'aaaaaaaaa' // not wrapped - ]; - - const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: false }); - 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 }); - 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', async () => { - const data = [ - 'aaaaaaaaaa\x1b[2;1H', // cannot wrap as it's the first - 'aaaaaaaaa\x1b[3;1H', // wrapped (windows mode only) - 'aaaaaaaaa' // not wrapped - ]; - - const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: false }); - 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 }); - 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', async () => { - // not converting - const termNotConverting = new TestTerminal({ cols: 15, rows: 10 }); - 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'); - assert.equal(termNotConverting.buffer.lines.get(1)!.translateToString(true), ' World'); - - // converting - const termConverting = new TestTerminal({ cols: 15, rows: 10, convertEol: true }); - 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'); - assert.equal(termConverting.buffer.lines.get(1)!.translateToString(true), 'World'); - }); - - // FIXME: move to common/CoreTerminal.test once the trimming is moved over - describe('marker lifecycle', () => { - // create a 10x5 terminal with markers on every line - // to test marker lifecycle under various terminal actions - let markers: IMarker[]; - let disposeStack: IMarker[]; - let term: TestTerminal; + describe('scrollPages', () => { + let startYDisp: number; 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)); - await term.writeP('\x1b[r0\r\n'); - markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - await term.writeP('1\r\n'); - markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - await term.writeP('2\r\n'); - markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - await term.writeP('3\r\n'); - markers.push(term.buffers.active.addMarker(term.buffers.active.y)); - await term.writeP('4'); - for (let i = 0; i < markers.length; ++i) { - const marker = markers[i]; - marker.onDispose(() => disposeStack.push(marker)); + for (let i = 0; i < term.rows * 3; i++) { + await term.writeP('test\r\n'); + } + startYDisp = (term.rows * 2) + 1; + }); + it('should scroll a single page', () => { + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollPages(-1); + assert.equal(term.buffer.ydisp, startYDisp - (term.rows - 1)); + term.scrollPages(1); + assert.equal(term.buffer.ydisp, startYDisp); + }); + it('should scroll a multiple pages', () => { + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollPages(-2); + assert.equal(term.buffer.ydisp, startYDisp - (term.rows - 1) * 2); + term.scrollPages(2); + assert.equal(term.buffer.ydisp, startYDisp); + }); + }); + + describe('scrollToTop', () => { + beforeEach(async () => { + for (let i = 0; i < term.rows * 3; i++) { + await term.writeP('test\r\n'); } }); - it('initial', () => { - assert.deepEqual(markers.map(m => m.line), [0, 1, 2, 3, 4]); + it('should scroll to the top', () => { + assert.notEqual(term.buffer.ydisp, 0); + term.scrollToTop(); + assert.equal(term.buffer.ydisp, 0); }); - it('should dispose on normal trim off the top', async () => { - // moves top line into scrollback - await term.writeP('\n'); - assert.deepEqual(disposeStack, []); - // trims first marker - await term.writeP('\n'); - assert.deepEqual(disposeStack, [markers[0]]); - // trims second marker - 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]); - assert.deepEqual(disposeStack.map(el => (el as any)._isDisposed), [true, true]); - // trimmed markers should contain line -1 - assert.deepEqual(disposeStack.map(el => el.line), [-1, -1]); + }); + + describe('scrollToBottom', () => { + let startYDisp: number; + beforeEach(async () => { + for (let i = 0; i < term.rows * 3; i++) { + await term.writeP('test\r\n'); + } + startYDisp = (term.rows * 2) + 1; }); - 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 scroll to the bottom', () => { + term.scrollLines(-1); + term.scrollToBottom(); + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollPages(-1); + term.scrollToBottom(); + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollToTop(); + term.scrollToBottom(); + assert.equal(term.buffer.ydisp, startYDisp); }); - 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]); + }); + + describe('scrollToLine', () => { + let startYDisp: number; + beforeEach(async () => { + for (let i = 0; i < term.rows * 3; i++) { + await term.writeP('test\r\n'); + } + startYDisp = (term.rows * 2) + 1; }); - it('should dispose on resize', () => { - term.resize(10, 2); - assert.deepEqual(disposeStack, [markers[0], markers[1]]); - assert.deepEqual(markers.map(el => el.line), [-1, -1, 0, 1, 2]); + it('should scroll to requested line', () => { + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollToLine(0); + assert.equal(term.buffer.ydisp, 0); + term.scrollToLine(10); + assert.equal(term.buffer.ydisp, 10); + term.scrollToLine(startYDisp); + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollToLine(20); + assert.equal(term.buffer.ydisp, 20); + }); + it('should not scroll beyond boundary lines', () => { + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollToLine(-1); + assert.equal(term.buffer.ydisp, 0); + term.scrollToLine(startYDisp + 1); + assert.equal(term.buffer.ydisp, startYDisp); + }); + }); + + describe('keyPress', () => { + it('should scroll down, when a key is pressed and terminal is scrolled up', () => { + const event = { + type: 'keydown', + key: 'a', + keyCode: 65, + preventDefault: () => { }, + stopPropagation: () => { } + }; + + term.buffer.ydisp = 0; + term.buffer.ybase = 40; + term.keyPress(event); + + // Ensure that now the terminal is scrolled to bottom + assert.equal(term.buffer.ydisp, term.buffer.ybase); + }); + + 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++) { + await term.writeP('test\r\n'); + } + const startYDisp = (term.rows * 2) + 1; + term.attachCustomKeyEventHandler(() => { + return false; + }); + + assert.equal(term.buffer.ydisp, startYDisp); + term.scrollLines(-1); + assert.equal(term.buffer.ydisp, startYDisp - 1); + term.keyPress({ keyCode: 0 }); + assert.equal(term.buffer.ydisp, startYDisp - 1); + }); + }); + + describe('scroll() function', () => { + describe('when scrollback > 0', () => { + it('should create a new line and scroll', () => { + term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(INIT_ROWS - 1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + term.scroll(DEFAULT_ATTR_DATA.clone()); + assert.equal(term.buffer.lines.length, INIT_ROWS + 1); + assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(INIT_ROWS - 1)!.loadCell(0, new CellData()).getChars(), 'b'); + assert.equal(term.buffer.lines.get(INIT_ROWS)!.loadCell(0, new CellData()).getChars(), ''); + }); + + it('should properly scroll inside a scroll region (scrollTop set)', () => { + term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + term.buffer.scrollTop = 1; + term.scroll(DEFAULT_ATTR_DATA.clone()); + assert.equal(term.buffer.lines.length, INIT_ROWS); + assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'c'); + }); + + it('should properly scroll inside a scroll region (scrollBottom set)', () => { + term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); + term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); + term.buffer.y = 3; + term.buffer.scrollBottom = 3; + term.scroll(DEFAULT_ATTR_DATA.clone()); + assert.equal(term.buffer.lines.length, INIT_ROWS + 1); + assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a', '\'a\' should be pushed to the scrollback'); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'b'); + assert.equal(term.buffer.lines.get(2)!.loadCell(0, new CellData()).getChars(), 'c'); + assert.equal(term.buffer.lines.get(3)!.loadCell(0, new CellData()).getChars(), 'd'); + assert.equal(term.buffer.lines.get(4)!.loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(5)!.loadCell(0, new CellData()).getChars(), 'e'); + }); + + it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { + term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); + term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + term.buffer.scrollTop = 1; + term.buffer.scrollBottom = 3; + term.scroll(DEFAULT_ATTR_DATA.clone()); + assert.equal(term.buffer.lines.length, INIT_ROWS); + assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer'); + assert.equal(term.buffer.lines.get(2)!.loadCell(0, new CellData()).getChars(), 'd'); + assert.equal(term.buffer.lines.get(3)!.loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4)!.loadCell(0, new CellData()).getChars(), 'e'); + }); + }); + + describe('when scrollback === 0', () => { + beforeEach(() => { + term.optionsService.setOption('scrollback', 0); + assert.equal(term.buffer.lines.maxLength, INIT_ROWS); + }); + + it('should create a new line and shift everything up', () => { + term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(INIT_ROWS - 1)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + assert.equal(term.buffer.lines.length, INIT_ROWS); + term.scroll(DEFAULT_ATTR_DATA.clone()); + assert.equal(term.buffer.lines.length, INIT_ROWS); + // 'a' gets pushed out of buffer + assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'b'); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), ''); + assert.equal(term.buffer.lines.get(INIT_ROWS - 2)!.loadCell(0, new CellData()).getChars(), 'c'); + assert.equal(term.buffer.lines.get(INIT_ROWS - 1)!.loadCell(0, new CellData()).getChars(), ''); + }); + + it('should properly scroll inside a scroll region (scrollTop set)', () => { + term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + term.buffer.scrollTop = 1; + term.scroll(DEFAULT_ATTR_DATA.clone()); + assert.equal(term.buffer.lines.length, INIT_ROWS); + assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'c'); + }); + + it('should properly scroll inside a scroll region (scrollBottom set)', () => { + term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); + term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); + term.buffer.y = 3; + term.buffer.scrollBottom = 3; + term.scroll(DEFAULT_ATTR_DATA.clone()); + assert.equal(term.buffer.lines.length, INIT_ROWS); + assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'b'); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'c'); + assert.equal(term.buffer.lines.get(2)!.loadCell(0, new CellData()).getChars(), 'd'); + assert.equal(term.buffer.lines.get(3)!.loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4)!.loadCell(0, new CellData()).getChars(), 'e'); + }); + + it('should properly scroll inside a scroll region (scrollTop and scrollBottom set)', () => { + term.buffer.lines.get(0)!.setCell(0, CellData.fromCharData([0, 'a', 0, 'a'.charCodeAt(0)])); + term.buffer.lines.get(1)!.setCell(0, CellData.fromCharData([0, 'b', 0, 'b'.charCodeAt(0)])); + term.buffer.lines.get(2)!.setCell(0, CellData.fromCharData([0, 'c', 0, 'c'.charCodeAt(0)])); + term.buffer.lines.get(3)!.setCell(0, CellData.fromCharData([0, 'd', 0, 'd'.charCodeAt(0)])); + term.buffer.lines.get(4)!.setCell(0, CellData.fromCharData([0, 'e', 0, 'e'.charCodeAt(0)])); + term.buffer.y = INIT_ROWS - 1; // Move cursor to last line + term.buffer.scrollTop = 1; + term.buffer.scrollBottom = 3; + term.scroll(DEFAULT_ATTR_DATA.clone()); + assert.equal(term.buffer.lines.length, INIT_ROWS); + assert.equal(term.buffer.lines.get(0)!.loadCell(0, new CellData()).getChars(), 'a'); + assert.equal(term.buffer.lines.get(1)!.loadCell(0, new CellData()).getChars(), 'c', '\'b\' should be removed from the buffer'); + assert.equal(term.buffer.lines.get(2)!.loadCell(0, new CellData()).getChars(), 'd'); + assert.equal(term.buffer.lines.get(3)!.loadCell(0, new CellData()).getChars(), '', 'a blank line should be added at scrollBottom\'s index'); + assert.equal(term.buffer.lines.get(4)!.loadCell(0, new CellData()).getChars(), 'e'); + }); }); }); }); - class TestLinkifier extends Linkifier { - constructor(bufferService: IBufferService, unicodeService: IUnicodeService) { - super(bufferService, new MockLogService(), unicodeService); - Linkifier._timeBeforeLatency = 0; + describe('Third level shift', () => { + let evKeyDown: any; + let evKeyPress: any; + + beforeEach(() => { + term.clearSelection = () => { }; + // term.compositionHelper = { + // isComposing: false, + // keydown: { + // bind: () => { + // return () => { return true; }; + // } + // } + // }; + evKeyDown = { + preventDefault: () => { }, + stopPropagation: () => { }, + type: 'keydown', + altKey: null, + keyCode: null + }; + evKeyPress = { + preventDefault: () => { }, + stopPropagation: () => { }, + type: 'keypress', + altKey: null, + charCode: null, + keyCode: null + }; + }); + + describe('with macOptionIsMeta', () => { + let originalIsMac: boolean; + beforeEach(() => { + originalIsMac = term.browser.isMac; + term.options.macOptionIsMeta = true; + }); + afterEach(() => term.browser.isMac = originalIsMac); + + it('should interfere with the alt key on keyDown', () => { + evKeyDown.altKey = true; + evKeyDown.keyCode = 81; + assert.equal(term.keyDown(evKeyDown), false); + evKeyDown.altKey = true; + evKeyDown.keyCode = 192; + assert.equal(term.keyDown(evKeyDown), false); + }); + }); + + describe('On Mac OS', () => { + let originalIsMac: boolean; + beforeEach(() => { + originalIsMac = term.browser.isMac; + term.browser.isMac = true; + }); + afterEach(() => term.browser.isMac = originalIsMac); + + it('should not interfere with the alt key on keyDown', () => { + evKeyDown.altKey = true; + evKeyDown.keyCode = 81; + assert.equal(term.keyDown(evKeyDown), true); + evKeyDown.altKey = true; + evKeyDown.keyCode = 192; + term.keyDown(evKeyDown); + assert.equal(term.keyDown(evKeyDown), true); + }); + + it('should interfere with the alt + arrow keys', () => { + evKeyDown.altKey = true; + evKeyDown.keyCode = 37; + assert.equal(term.keyDown(evKeyDown), false); + evKeyDown.altKey = true; + evKeyDown.keyCode = 39; + assert.equal(term.keyDown(evKeyDown), false); + }); + + it('should emit key with alt + key on keyPress', (done) => { + const keys = ['@', '@', '\\', '\\', '|', '|']; + + term.onKey(e => { + if (e.key) { + const index = keys.indexOf(e.key); + assert(index !== -1, 'Emitted wrong key: ' + e.key); + keys.splice(index, 1); + } + if (keys.length === 0) done(); + }); + + evKeyPress.altKey = true; + // @ + evKeyPress.charCode = null; + evKeyPress.keyCode = 64; + term.keyPress(evKeyPress); + // Firefox @ + evKeyPress.charCode = 64; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + // \ + evKeyPress.charCode = null; + evKeyPress.keyCode = 92; + term.keyPress(evKeyPress); + // Firefox \ + evKeyPress.charCode = 92; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + // | + evKeyPress.charCode = null; + evKeyPress.keyCode = 124; + term.keyPress(evKeyPress); + // Firefox | + evKeyPress.charCode = 124; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + }); + }); + + describe('On MS Windows', () => { + let originalIsWindows: boolean; + beforeEach(() => { + originalIsWindows = term.browser.isWindows; + term.browser.isWindows = true; + }); + afterEach(() => term.browser.isWindows = originalIsWindows); + + it('should not interfere with the alt + ctrl key on keyDown', () => { + evKeyPress.altKey = true; + evKeyPress.ctrlKey = true; + evKeyPress.keyCode = 81; + assert.equal(term.keyDown(evKeyPress), true); + evKeyDown.altKey = true; + evKeyDown.ctrlKey = true; + evKeyDown.keyCode = 81; + term.keyDown(evKeyDown); + assert.equal(term.keyDown(evKeyPress), true); + }); + + it('should interfere with the alt + ctrl + arrow keys', () => { + evKeyDown.altKey = true; + evKeyDown.ctrlKey = true; + + evKeyDown.keyCode = 37; + assert.equal(term.keyDown(evKeyDown), false); + evKeyDown.keyCode = 39; + term.keyDown(evKeyDown); + assert.equal(term.keyDown(evKeyDown), false); + }); + + it('should emit key with alt + ctrl + key on keyPress', (done) => { + const keys = ['@', '@', '\\', '\\', '|', '|']; + + term.onKey(e => { + if (e.key) { + const index = keys.indexOf(e.key); + assert(index !== -1, 'Emitted wrong key: ' + e.key); + keys.splice(index, 1); + } + if (keys.length === 0) done(); + }); + + evKeyPress.altKey = true; + evKeyPress.ctrlKey = true; + + // @ + evKeyPress.charCode = null; + evKeyPress.keyCode = 64; + term.keyPress(evKeyPress); + // Firefox @ + evKeyPress.charCode = 64; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + // \ + evKeyPress.charCode = null; + evKeyPress.keyCode = 92; + term.keyPress(evKeyPress); + // Firefox \ + evKeyPress.charCode = 92; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + // | + evKeyPress.charCode = null; + evKeyPress.keyCode = 124; + term.keyPress(evKeyPress); + // Firefox | + evKeyPress.charCode = 124; + evKeyPress.keyCode = 0; + term.keyPress(evKeyPress); + }); + }); + }); + + describe('unicode - surrogates', () => { + it('2 characters per cell', async function (): Promise { + 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) { + 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); + assert.equal(tchar.getWidth(), 1); + assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), ''); + term.reset(); + } + }); + 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; + 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', async function (): Promise { + 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; + + 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); + assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), ''); + term.reset(); + } + }); + it('2 characters per cell over line end without autowrap', async function (): Promise { + 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; + await term.writeP('\x1b[?7l'); // Disable wraparound mode + const width = wcwidth((0xD800 - 0xD800) * 0x400 + i - 0xDC00 + 0x10000); + if (width !== 1) { + continue; + } + 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); + assert.equal(term.buffer.lines.get(1)!.loadCell(1, cell).getChars(), ''); + term.reset(); + } + }); + it('splitted surrogates', async function (): Promise { + this.timeout(10000); + const high = String.fromCharCode(0xD800); + const cell = new CellData(); + for (let i = 0xDC00; i <= 0xDCFF; ++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); + assert.equal(tchar.getWidth(), 1); + assert.equal(term.buffer.lines.get(0)!.loadCell(1, cell).getChars(), ''); + term.reset(); + } + }); + }); + + describe('unicode - combining characters', () => { + const cell = new CellData(); + 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', async () => { + term.buffer.x = term.cols - 1 - 3; + 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); + assert.equal(cell.getWidth(), 1); + term.buffer.lines.get(0)!.loadCell(1, cell); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 1); + }); + 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'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 1); + } + term.buffer.lines.get(1)!.loadCell(0, cell); + assert.equal(cell.getChars(), 'e\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 1); + }); + 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'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 1); + } + term.buffer.lines.get(1)!.loadCell(0, cell); + assert.equal(cell.getChars(), '\uD800\uDC00\u0301'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 1); + }); + }); + + describe('unicode - fullwidth characters', () => { + const cell = new CellData(); + it('cursor movement even', async () => { + assert.equal(term.buffer.x, 0); + await term.writeP('¥'); + assert.equal(term.buffer.x, 2); + }); + it('cursor movement odd', async () => { + term.buffer.x = 1; + assert.equal(term.buffer.x, 1); + await term.writeP('¥'); + assert.equal(term.buffer.x, 3); + }); + 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) { + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); + } else { + assert.equal(cell.getChars(), '¥'); + assert.equal(cell.getChars().length, 1); + assert.equal(cell.getWidth(), 2); + } + } + term.buffer.lines.get(1)!.loadCell(0, cell); + assert.equal(cell.getChars(), '¥'); + assert.equal(cell.getChars().length, 1); + assert.equal(cell.getWidth(), 2); + }); + it('line of ¥ odd', async () => { + term.buffer.x = 1; + 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)) { + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); + } else { + assert.equal(cell.getChars(), '¥'); + assert.equal(cell.getChars().length, 1); + assert.equal(cell.getWidth(), 2); + } + } + term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 1); + term.buffer.lines.get(1)!.loadCell(0, cell); + assert.equal(cell.getChars(), '¥'); + assert.equal(cell.getChars().length, 1); + assert.equal(cell.getWidth(), 2); + }); + it('line of ¥ with combining odd', async () => { + term.buffer.x = 1; + 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)) { + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); + } else { + assert.equal(cell.getChars(), '¥\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 2); + } + } + term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 1); + term.buffer.lines.get(1)!.loadCell(0, cell); + assert.equal(cell.getChars(), '¥\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 2); + }); + 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) { + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); + } else { + assert.equal(cell.getChars(), '¥\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 2); + } + } + term.buffer.lines.get(1)!.loadCell(0, cell); + assert.equal(cell.getChars(), '¥\u0301'); + assert.equal(cell.getChars().length, 2); + assert.equal(cell.getWidth(), 2); + }); + it('line of surrogate fullwidth with combining odd', async () => { + term.buffer.x = 1; + 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)) { + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); + } else { + assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 2); + } + } + term.buffer.lines.get(0)!.loadCell(term.cols - 1, cell); + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 1); + term.buffer.lines.get(1)!.loadCell(0, cell); + assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 2); + }); + 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) { + assert.equal(cell.getChars(), ''); + assert.equal(cell.getChars().length, 0); + assert.equal(cell.getWidth(), 0); + } else { + assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 2); + } + } + term.buffer.lines.get(1)!.loadCell(0, cell); + assert.equal(cell.getChars(), '\ud843\ude6d\u0301'); + assert.equal(cell.getChars().length, 3); + assert.equal(cell.getWidth(), 2); + }); + }); + + describe('insert mode', () => { + const cell = new CellData(); + 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'); + 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', async () => { + await term.writeP(Array(9).join('0123456789').slice(-80)); + term.buffer.x = 10; + term.buffer.y = 0; + term.write('\x1b[4h'); + 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(), ''); + assert.equal(term.buffer.lines.get(0)!.loadCell(14, cell).getChars(), '¥'); + 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', async () => { + await term.writeP(Array(41).join('¥')); + term.buffer.x = 10; + term.buffer.y = 0; + term.write('\x1b[4h'); + 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 + 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(), '¥'); + assert.equal(term.buffer.lines.get(0)!.loadCell(79, cell).getChars(), ''); // empty cell after fullwidth + }); + }); + + describe('Linkifier unicode handling', () => { + let terminal: TestTerminal; + let linkifier: TestLinkifier; + let mouseZoneManager: TestMouseZoneManager; + + // other than the tests above unicode testing needs the full terminal instance + // to get the special handling of fullwidth, surrogate and combining chars in the input handler + beforeEach(() => { + terminal = new TestTerminal({ cols: 10, rows: 5 }); + linkifier = new TestLinkifier((terminal as any)._bufferService, terminal.unicodeService); + mouseZoneManager = new TestMouseZoneManager(); + linkifier.attachToDom({} as any, mouseZoneManager); + }); + + function assertLinkifiesInTerminal(rowText: string, linkMatcherRegex: RegExp, links: { x1: number, y1: number, x2: number, y2: number }[]): Promise { + 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); + }); } - public get linkMatchers(): IRegisteredLinkMatcher[] { return this._linkMatchers; } - public linkifyRows(): void { super.linkifyRows(0, this._bufferService.buffer.lines.length - 1); } + describe('unicode before the match', () => { + 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', () => { + return assertLinkifiesInTerminal('e\u0301e\u0301e\u0301 foo', /foo/, [{ x1: 8, x2: 1, y1: 0, y2: 1 }]); + }); + it('surrogate - match within one line', () => { + return assertLinkifiesInTerminal('𝄞𝄞𝄞 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]); + }); + 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', () => { + return assertLinkifiesInTerminal('𓂀\u0301𓂀\u0301𓂀\u0301 foo', /foo/, [{ x1: 4, x2: 7, y1: 0, y2: 0 }]); + }); + 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', () => { + return assertLinkifiesInTerminal('12 foo', /foo/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); + }); + 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', () => { + return assertLinkifiesInTerminal('¥\u0301¥\u0301 foo', /foo/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); + }); + 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', () => { + return assertLinkifiesInTerminal('test cafe\u0301', /cafe\u0301/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]); + }); + 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', () => { + return assertLinkifiesInTerminal('test a𝄞b', /a𝄞b/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); + }); + 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', () => { + return assertLinkifiesInTerminal('test a𓂀\u0301b', /a𓂀\u0301b/, [{ x1: 5, x2: 8, y1: 0, y2: 0 }]); + }); + 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', () => { + return assertLinkifiesInTerminal('test a1b', /a1b/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]); + }); + 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', () => { + return assertLinkifiesInTerminal('test a¥\u0301b', /a¥\u0301b/, [{ x1: 5, x2: 9, y1: 0, y2: 0 }]); + }); + it('combining fullwidth - match over two lines', () => { + return assertLinkifiesInTerminal('testtest a¥\u0301b', /a¥\u0301b/, [{ x1: 9, x2: 3, y1: 0, y2: 1 }]); + }); + }); + }); + + describe('Buffer.stringIndexToBufferIndex', () => { + let terminal: TestTerminal; + + beforeEach(() => { + terminal = new TestTerminal({ rows: 5, cols: 10, scrollback: 5 }); + }); + + it('multiline ascii', async () => { + const input = 'This is ASCII text spanning multiple lines.'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + } + }); + + it('combining e\u0301 in a sentence', async () => { + const input = 'Sitting in the cafe\u0301 drinking coffee.'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < 19; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + } + // string index 18 & 19 point to combining char e\u0301 ---> same buffer Index + assert.deepEqual( + terminal.buffer.stringIndexToBufferIndex(0, 18), + terminal.buffer.stringIndexToBufferIndex(0, 19)); + // after the combining char every string index has an offset of -1 + for (let i = 19; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); + } + }); + + it('multiline combining e\u0301', async () => { + const input = 'e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301e\u0301'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + // every buffer cell index contains 2 string indices + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); + } + }); + + it('surrogate char in a sentence', async () => { + const input = 'The 𝄞 is a clef widely used in modern notation.'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < 5; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + } + // string index 4 & 5 point to surrogate char 𝄞 ---> same buffer Index + assert.deepEqual( + terminal.buffer.stringIndexToBufferIndex(0, 4), + terminal.buffer.stringIndexToBufferIndex(0, 5)); + // after the combining char every string index has an offset of -1 + for (let i = 5; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i - 1) / terminal.cols) | 0, (i - 1) % terminal.cols], bufferIndex); + } + }); + + it('multiline surrogate char', async () => { + const 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 + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i >> 1) / terminal.cols) | 0, (i >> 1) % terminal.cols], bufferIndex); + } + }); + + 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.'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + // index 0..2 should map to 0 + assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 1)); + assert.deepEqual([0, 0], terminal.buffer.stringIndexToBufferIndex(0, 2)); + for (let i = 2; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i - 2) / terminal.cols) | 0, (i - 2) % terminal.cols], bufferIndex); + } + }); + + it('multiline surrogate with combining', async () => { + const input = '𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301𓂀\u0301'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + // every buffer cell index contains 3 string indices + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(((i / 3) | 0) / terminal.cols) | 0, ((i / 3) | 0) % terminal.cols], bufferIndex); + } + }); + + it('fullwidth chars', async () => { + const input = 'These 123 are some fat numbers.'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < 6; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([(i / terminal.cols) | 0, i % terminal.cols], bufferIndex); + } + // string index 6, 7, 8 take 2 cells + assert.deepEqual([0, 8], terminal.buffer.stringIndexToBufferIndex(0, 7)); + assert.deepEqual([1, 0], terminal.buffer.stringIndexToBufferIndex(0, 8)); + // rest of the string has offset of +3 + for (let i = 9; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i + 3) / terminal.cols) | 0, (i + 3) % terminal.cols], bufferIndex); + } + }); + + it('multiline fullwidth chars', async () => { + const input = '12345678901234567890'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 9; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i); + assert.deepEqual([((i << 1) / terminal.cols) | 0, (i << 1) % terminal.cols], bufferIndex); + } + }); + + it('fullwidth combining with emoji - match emoji cell', async () => { + const input = 'Lots of ¥\u0301 make me 😃.'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + const stringIndex = s.match(/😃/)!.index!; + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, stringIndex); + 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)', 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 + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 10; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); + const j = (i - 0) << 1; + assert.deepEqual([(j / terminal.cols) | 0, j % terminal.cols], bufferIndex); + } + }); + + it('test fully wrapped buffer up to last char', async () => { + const input = Array(6).join('1234567890'); + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); + assert.equal(input[i], terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars()); + } + }); + + 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'; + await terminal.writeP(input); + const s = terminal.buffer.iterator(true).next().content; + assert.equal(input, s); + for (let i = 0; i < input.length; ++i) { + const bufferIndex = terminal.buffer.stringIndexToBufferIndex(0, i, true); + assert.equal( + (!(i % 3)) + ? input[i] + : (i % 3 === 1) + ? input.substr(i, 2) + : input.substr(i - 1, 2), + terminal.buffer.lines.get(bufferIndex[0])!.loadCell(bufferIndex[1], new CellData()).getChars()); + } + }); + + it('should handle \t in lines correctly', async () => { + const input = '\thttps://google.de'; + 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', async () => { + const terminal = new TestTerminal({ rows: 5, cols: 10, scrollback: 5 }); + const data = [ + 'aaaaaaaaaa', + 'aaaaaaaaa\n', + 'aaaaaaaaaa', + 'aaaaaaaaa\n', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaaa', + 'aaaaaaaaa\n', + 'aaaaaaaaaa', + 'aaaaaaaaaa' + ]; + await terminal.writeP(data.join('')); + // brute force test with insane values + assert.doesNotThrow(() => { + for (let overscan = 0; overscan < 20; ++overscan) { + for (let start = -10; start < 20; ++start) { + for (let end = -10; end < 20; ++end) { + const it = terminal.buffer.iterator(false, start, end, overscan, overscan); + while (it.hasNext()) { + it.next(); + } + } + } + } + }); + }); + }); + + describe('Windows Mode', () => { + 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) + 'aaaaaaaaa' // not wrapped + ]; + + const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: false }); + 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 }); + 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', async () => { + const data = [ + 'aaaaaaaaaa\x1b[2;1H', // cannot wrap as it's the first + 'aaaaaaaaa\x1b[3;1H', // wrapped (windows mode only) + 'aaaaaaaaa' // not wrapped + ]; + + const normalTerminal = new TestTerminal({ rows: 5, cols: 10, windowsMode: false }); + 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 }); + 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', async () => { + // not converting + const termNotConverting = new TestTerminal({ cols: 15, rows: 10 }); + 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'); + assert.equal(termNotConverting.buffer.lines.get(1)!.translateToString(true), ' World'); + + // converting + const termConverting = new TestTerminal({ cols: 15, rows: 10, convertEol: true }); + 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'); + assert.equal(termConverting.buffer.lines.get(1)!.translateToString(true), 'World'); + }); + + // FIXME: move to common/CoreTerminal.test once the trimming is moved over + describe('marker lifecycle', () => { + // create a 10x5 terminal with markers on every line + // to test marker lifecycle under various terminal actions + let markers: IMarker[]; + let disposeStack: IMarker[]; + let term: TestTerminal; + 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)); + await term.writeP('\x1b[r0\r\n'); + markers.push(term.buffers.active.addMarker(term.buffers.active.y)); + await term.writeP('1\r\n'); + markers.push(term.buffers.active.addMarker(term.buffers.active.y)); + await term.writeP('2\r\n'); + markers.push(term.buffers.active.addMarker(term.buffers.active.y)); + await term.writeP('3\r\n'); + markers.push(term.buffers.active.addMarker(term.buffers.active.y)); + await term.writeP('4'); + for (let i = 0; i < markers.length; ++i) { + const marker = markers[i]; + marker.onDispose(() => disposeStack.push(marker)); + } + }); + it('initial', () => { + assert.deepEqual(markers.map(m => m.line), [0, 1, 2, 3, 4]); + }); + it('should dispose on normal trim off the top', async () => { + // moves top line into scrollback + await term.writeP('\n'); + assert.deepEqual(disposeStack, []); + // trims first marker + await term.writeP('\n'); + assert.deepEqual(disposeStack, [markers[0]]); + // trims second marker + 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]); + assert.deepEqual(disposeStack.map(el => (el as any)._isDisposed), [true, true]); + // trimmed markers should contain line -1 + assert.deepEqual(disposeStack.map(el => el.line), [-1, -1]); + }); + 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', 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]); + }); + it('should dispose on resize', () => { + term.resize(10, 2); + assert.deepEqual(disposeStack, [markers[0], markers[1]]); + assert.deepEqual(markers.map(el => el.line), [-1, -1, 0, 1, 2]); + }); + }); +}); + +class TestLinkifier extends Linkifier { + constructor(bufferService: IBufferService, unicodeService: IUnicodeService) { + super(bufferService, new MockLogService(), unicodeService); + Linkifier._timeBeforeLatency = 0; } - class TestMouseZoneManager implements IMouseZoneManager { - public dispose(): void { - } - public clears: number = 0; - public zones: IMouseZone[] = []; - public add(zone: IMouseZone): void { - this.zones.push(zone); - } - public clearAll(): void { - this.clears++; - } + public get linkMatchers(): IRegisteredLinkMatcher[] { return this._linkMatchers; } + public linkifyRows(): void { super.linkifyRows(0, this._bufferService.buffer.lines.length - 1); } +} + +class TestMouseZoneManager implements IMouseZoneManager { + public dispose(): void { + } + public clears: number = 0; + public zones: IMouseZone[] = []; + public add(zone: IMouseZone): void { + this.zones.push(zone); + } + public clearAll(): void { + this.clears++; } } -); diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 9f855845f..1d076692a 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -58,7 +58,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { protected _inputHandler: InputHandler; private _writeBuffer: WriteBuffer; private _windowsMode: IDisposable | undefined; - + /** An IBufferline to clone/copy from for new blank lines */ + private _cachedBlankLine: IBufferLine | undefined; private _onBinary = new EventEmitter(); public get onBinary(): IEvent { return this._onBinary.event; } @@ -97,20 +98,21 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._instantiationService = new InstantiationService(); this.optionsService = new OptionsService(options); this._instantiationService.setService(IOptionsService, this.optionsService); + this._bufferService = this.register(this._instantiationService.createInstance(BufferService)); + this._instantiationService.setService(IBufferService, this._bufferService); this._logService = this._instantiationService.createInstance(LogService); this._instantiationService.setService(ILogService, this._logService); + this._coreService = this.register(this._instantiationService.createInstance(CoreService, () => this.scrollToBottom())); + this._instantiationService.setService(ICoreService, this._coreService); + this._coreMouseService = this._instantiationService.createInstance(CoreMouseService); + this._instantiationService.setService(ICoreMouseService, this._coreMouseService); + this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); + this._instantiationService.setService(IDirtyRowService, this._dirtyRowService); this.unicodeService = this._instantiationService.createInstance(UnicodeService); this._instantiationService.setService(IUnicodeService, this.unicodeService); this._charsetService = this._instantiationService.createInstance(CharsetService); this._instantiationService.setService(ICharsetService, this._charsetService); - this._bufferService = this.register(this._instantiationService.createInstance(BufferService)); - this._instantiationService.setService(IBufferService, this._bufferService); - this._dirtyRowService = this._instantiationService.createInstance(DirtyRowService); - this._instantiationService.setService(IDirtyRowService, this._dirtyRowService); - this._coreService = this.register(this._instantiationService.createInstance(CoreService, () => this._bufferService.scrollToBottom())); - this._instantiationService.setService(ICoreService, this._coreService); - this._coreMouseService = this._instantiationService.createInstance(CoreMouseService); - this._instantiationService.setService(ICoreMouseService, this._coreMouseService); + // Register input handler and handle/forward events this._inputHandler = new InputHandler(this._bufferService, this._charsetService, this._coreService, this._dirtyRowService, this._logService, this.optionsService, this._coreMouseService, this.unicodeService); this.register(forwardEvent(this._inputHandler.onLineFeed, this._onLineFeed)); @@ -121,6 +123,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this.register(forwardEvent(this._coreService.onData, this._onData)); this.register(forwardEvent(this._coreService.onBinary, this._onBinary)); this.register(this.optionsService.onOptionChange(key => this._updateOptions(key))); + this.register(this._bufferService.onScroll(event => this._onScroll.fire(event))); // Setup WriteBuffer this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)); @@ -135,23 +138,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this._windowsMode = undefined; } - public scrollLines(disp: number, suppressScrollEvent?: boolean): void { - this._bufferService.scrollLines(disp, suppressScrollEvent); - } - - public scrollPages(pageCount: number): void { - this._bufferService.scrollPages(pageCount); - } - public scrollToTop(): void { - this._bufferService.scrollToTop(); - } - public scrollToBottom(): void { - this._bufferService.scrollToBottom(); - } - public scrollToLine(line: number): void { - this._bufferService.scrollToLine(line); - } - public write(data: string | Uint8Array, callback?: () => void): void { this._writeBuffer.write(data, callback); } @@ -189,97 +175,18 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { * @param isWrapped Whether the new line is wrapped from the previous line. */ public scroll(eraseAttr: IAttributeData, isWrapped: boolean = false): void { - const buffer = this._bufferService.buffer; - - let newLine: IBufferLine | undefined; - newLine = this._cachedBlankLine; - if (!newLine || newLine.length !== this.cols || newLine.getFg(0) !== eraseAttr.fg || newLine.getBg(0) !== eraseAttr.bg) { - newLine = buffer.getBlankLine(eraseAttr, isWrapped); - this._cachedBlankLine = newLine; - } - newLine.isWrapped = isWrapped; - - const topRow = buffer.ybase + buffer.scrollTop; - const bottomRow = buffer.ybase + buffer.scrollBottom; - - if (buffer.scrollTop === 0) { - // Determine whether the buffer is going to be trimmed after insertion. - const willBufferBeTrimmed = buffer.lines.isFull; - - // Insert the line using the fastest method - if (bottomRow === buffer.lines.length - 1) { - if (willBufferBeTrimmed) { - buffer.lines.recycle().copyFrom(newLine); - } else { - buffer.lines.push(newLine.clone()); - } - } else { - buffer.lines.splice(bottomRow + 1, 0, newLine.clone()); - } - - // Only adjust ybase and ydisp when the buffer is not trimmed - if (!willBufferBeTrimmed) { - buffer.ybase++; - // Only scroll the ydisp with ybase if the user has not scrolled up - if (!this._bufferService.isUserScrolling) { - buffer.ydisp++; - } - } else { - // When the buffer is full and the user has scrolled up, keep the text - // stable unless ydisp is right at the top - if (this._bufferService.isUserScrolling) { - buffer.ydisp = Math.max(buffer.ydisp - 1, 0); - } - } - } else { - // scrollTop is non-zero which means no line will be going to the - // scrollback, instead we can just shift them in-place. - const scrollRegionHeight = bottomRow - topRow + 1 /* as it's zero-based */; - buffer.lines.shiftElements(topRow + 1, scrollRegionHeight - 1, -1); - buffer.lines.set(bottomRow, newLine.clone()); - } - - // Move the viewport to the bottom of the buffer unless the user is - // scrolling. - if (!this._bufferService.isUserScrolling) { - buffer.ydisp = buffer.ybase; - } - - // Flag rows that need updating - this._dirtyRowService.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); - - this._onScroll.fire({ position: buffer.ydisp, source: ScrollSource.TERMINAL }); + this._bufferService.scroll(eraseAttr, isWrapped); } /** * Scroll the display of the terminal * @param disp The number of lines to scroll down (negative scroll up). - * @param suppressScrollEvent Don't emit an onScroll event. - * @param source The source of the scroll action. Emitted as part of the onScroll event - * to avoid cyclic invocations if the event originated from the Viewport. + * @param suppressScrollEvent Don't emit the scroll event as scrollLines. This is used + * to avoid unwanted events being handled by the viewport when the event was triggered from the + * viewport originally. */ - public scrollLines(disp: number, suppressScrollEvent = false, source = ScrollSource.TERMINAL): void { - const buffer = this._bufferService.buffer; - if (disp < 0) { - if (buffer.ydisp === 0) { - return; - } - this._bufferService.isUserScrolling = true; - } else if (disp + buffer.ydisp >= buffer.ybase) { - this._bufferService.isUserScrolling = false; - } - - const oldYdisp = buffer.ydisp; - buffer.ydisp = Math.max(Math.min(buffer.ydisp + disp, buffer.ybase), 0); - - // No change occurred, don't trigger scroll/refresh - if (oldYdisp === buffer.ydisp) { - return; - } - - if (!suppressScrollEvent) { - this._onScroll.fire({ position: buffer.ydisp, source }); - } + public scrollLines(disp: number, suppressScrollEvent?: boolean): void { + this._bufferService.scrollLines(disp, suppressScrollEvent); } /** @@ -287,27 +194,27 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { * @param pageCount The number of pages to scroll (negative scrolls up). */ public scrollPages(pageCount: number): void { - this.scrollLines(pageCount * (this.rows - 1)); + this._bufferService.scrollPages(pageCount); } /** * Scrolls the display of the terminal to the top. */ public scrollToTop(): void { - this.scrollLines(-this._bufferService.buffer.ydisp); + this._bufferService.scrollToTop(); } /** * Scrolls the display of the terminal to the bottom. */ public scrollToBottom(): void { - this.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp); + this._bufferService.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp); } public scrollToLine(line: number): void { const scrollAmount = line - this._bufferService.buffer.ydisp; if (scrollAmount !== 0) { - this.scrollLines(scrollAmount); + this._bufferService.scrollLines(scrollAmount); } } diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index 93de6562a..ecd476fcf 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -18,7 +18,6 @@ import { clone } from 'common/Clone'; import { BufferService } from 'common/services/BufferService'; import { CoreService } from 'common/services/CoreService'; import { OscHandler } from 'common/parser/OscParser'; -import { DirtyRowService } from 'common/services/DirtyRowService'; function getCursor(bufferService: IBufferService): number[] { return [ @@ -76,62 +75,67 @@ describe('InputHandler', () => { function getLines(limit: number): string[] { const res: string[] = []; for (let i = 0; i < limit; ++i) { - res.push(bufferService.buffers.active.lines.get(i)!.translateToString(true)); + res.push(bufferService.buffer.lines.get(i)!.translateToString(true)); } return res; } + function reset(): void { + bufferService.buffer.y = 0; + bufferService.buffer.x = 0; + } + // This suite cannot live in InputHandler unless Terminal.scroll moved into IBufferService describe('SL/SR/DECIC/DECDC', () => { it('SL (scrollLeft)', async () => { inputHandler.parseP('12345'.repeat(6)); - assert.deepEqual(getLines(5), ['12345', '2345', '2345', '2345', '2345', '2345']); + assert.deepEqual(getLines(6), ['12345', '2345', '2345', '2345', '2345', '2345']); inputHandler.parseP('\x1b[0 @'); - assert.deepEqual(getLines(5), ['12345', '345', '345', '345', '345', '345']); + assert.deepEqual(getLines(6), ['12345', '345', '345', '345', '345', '345']); inputHandler.parseP('\x1b[2 @'); - assert.deepEqual(getLines(5), ['12345', '5', '5', '5', '5', '5']); + assert.deepEqual(getLines(6), ['12345', '5', '5', '5', '5', '5']); }); it('SR (scrollRight)', async () => { inputHandler.parseP('12345'.repeat(6)); inputHandler.parseP('\x1b[ A'); - assert.deepEqual(getLines(5), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']); + assert.deepEqual(getLines(6), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']); inputHandler.parseP('\x1b[0 A'); - assert.deepEqual(getLines(5), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']); + assert.deepEqual(getLines(6), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']); inputHandler.parseP('\x1b[2 A'); - assert.deepEqual(getLines(5), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']); + assert.deepEqual(getLines(6), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']); }); it('insertColumns (DECIC)', async () => { inputHandler.parseP('12345'.repeat(6)); inputHandler.parseP('\x1b[3;3H'); inputHandler.parseP('\x1b[\'}'); - assert.deepEqual(getLines(5), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); - inputHandler.reset(); + assert.deepEqual(getLines(6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); + reset(); inputHandler.parseP('12345'.repeat(6)); inputHandler.parseP('\x1b[3;3H'); inputHandler.parseP('\x1b[1\'}'); - assert.deepEqual(getLines(5), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); - inputHandler.reset(); + assert.deepEqual(getLines(6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); + reset(); inputHandler.parseP('12345'.repeat(6)); inputHandler.parseP('\x1b[3;3H'); inputHandler.parseP('\x1b[2\'}'); - assert.deepEqual(getLines(5), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']); + assert.deepEqual(getLines(6), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']); }); it('deleteColumns (DECDC)', async () => { inputHandler.parseP('12345'.repeat(6)); inputHandler.parseP('\x1b[3;3H'); inputHandler.parseP('\x1b[\'~'); - assert.deepEqual(getLines(5), ['12345', '1245', '1245', '1245', '1245', '1245']); - inputHandler.reset(); + assert.deepEqual(getLines(6), ['12345', '1245', '1245', '1245', '1245', '1245']); + reset(); inputHandler.parseP('12345'.repeat(6)); inputHandler.parseP('\x1b[3;3H'); inputHandler.parseP('\x1b[1\'~'); - assert.deepEqual(getLines(5), ['12345', '1245', '1245', '1245', '1245', '1245']); - inputHandler.reset(); + assert.deepEqual(getLines(6), ['12345', '1245', '1245', '1245', '1245', '1245']); + reset(); inputHandler.parseP('12345'.repeat(6)); inputHandler.parseP('\x1b[3;3H'); inputHandler.parseP('\x1b[2\'~'); - assert.deepEqual(getLines(5), ['12345', '125', '125', '125', '125', '125']); + assert.deepEqual(getLines(6), ['12345', '125', '125', '125', '125', '125']); }); }); @@ -539,68 +543,6 @@ describe('InputHandler', () => { await inputHandler.parseP('¥¥¥'); assert.deepEqual(getLines(bufferService, 2), ['¥¥', '¥']); }); - - - // This suite cannot live in InputHandler unless Terminal.scroll moved into IBufferService - describe('SL/SR/DECIC/DECDC', () => { - it('SL (scrollLeft)', async () => { - inputHandler.parseP('12345'.repeat(6)); - inputHandler.parseP('\x1b[ @'); - assert.deepEqual(getLines(bufferService, 5), ['12345', '2345', '2345', '2345', '2345', '2345']); - inputHandler.parseP('\x1b[0 @'); - assert.deepEqual(getLines(bufferService, 5), ['12345', '345', '345', '345', '345', '345']); - inputHandler.parseP('\x1b[2 @'); - assert.deepEqual(getLines(bufferService, 5), ['12345', '5', '5', '5', '5', '5']); - }); - it('SR (scrollRight)', async () => { - inputHandler.parseP('12345'.repeat(6)); - inputHandler.parseP('\x1b[ A'); - assert.deepEqual(getLines(bufferService, 5), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']); - inputHandler.parseP('\x1b[0 A'); - assert.deepEqual(getLines(bufferService, 5), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']); - inputHandler.parseP('\x1b[2 A'); - assert.deepEqual(getLines(bufferService, 5), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']); - }); - it('insertColumns (DECIC)', async () => { - inputHandler.parseP('12345'.repeat(6)); - inputHandler.parseP('\x1b[3;3H'); - inputHandler.parseP('\x1b[\'}'); - assert.deepEqual(getLines(bufferService, 5), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); - inputHandler.reset(); - inputHandler.parseP('12345'.repeat(6)); - inputHandler.parseP('\x1b[3;3H'); - inputHandler.parseP('\x1b[1\'}'); - assert.deepEqual(getLines(bufferService, 5), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); - inputHandler.reset(); - inputHandler.parseP('12345'.repeat(6)); - inputHandler.parseP('\x1b[3;3H'); - inputHandler.parseP('\x1b[2\'}'); - assert.deepEqual(getLines(bufferService, 5), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']); - }); - it('deleteColumns (DECDC)', async () => { - inputHandler.parseP('12345'.repeat(6)); - inputHandler.parseP('\x1b[3;3H'); - inputHandler.parseP('\x1b[\'~'); - assert.deepEqual(getLines(bufferService, 5), ['12345', '1245', '1245', '1245', '1245', '1245']); - inputHandler.reset(); - inputHandler.parseP('12345'.repeat(6)); - inputHandler.parseP('\x1b[3;3H'); - inputHandler.parseP('\x1b[1\'~'); - assert.deepEqual(getLines(bufferService, 5), ['12345', '1245', '1245', '1245', '1245', '1245']); - inputHandler.reset(); - inputHandler.parseP('12345'.repeat(6)); - inputHandler.parseP('\x1b[3;3H'); - inputHandler.parseP('\x1b[2\'~'); - assert.deepEqual(getLines(bufferService, 5), ['12345', '125', '125', '125', '125', '125']); - }); - }); - it('should fire the onScroll event', (done) => { - bufferService.onScroll(e => { - assert.equal(typeof e, 'number'); - done(); - }); - bufferService.scroll(DEFAULT_ATTR_DATA.clone()); - }); }); describe('alt screen', () => { @@ -1985,7 +1927,6 @@ describe('InputHandler - async handlers', () => { let bufferService: IBufferService; let coreService: ICoreService; let optionsService: MockOptionsService; - let dirtyRowService: MockDirtyRowService; let inputHandler: TestInputHandler; beforeEach(() => { From c096633f9132ea4f5b75507ea2015eb6f8ef40a0 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 1 Apr 2021 11:23:56 -0700 Subject: [PATCH 04/15] get tests to pass Co-authored-by: Daniel Imms --- src/browser/Terminal.test.ts | 10 +- src/browser/Terminal.ts | 1 - src/common/CoreTerminal.ts | 5 +- src/common/InputHandler.test.ts | 199 +++++++++++++-------------- src/common/InputHandler.ts | 8 +- src/common/Types.d.ts | 1 - src/common/services/BufferService.ts | 11 +- 7 files changed, 109 insertions(+), 126 deletions(-) diff --git a/src/browser/Terminal.test.ts b/src/browser/Terminal.test.ts index 8f10e6224..b0075d886 100644 --- a/src/browser/Terminal.test.ts +++ b/src/browser/Terminal.test.ts @@ -56,25 +56,25 @@ describe('Terminal', () => { // term.handler('fake'); // }); it('should fire the onCursorMove event', () => { - return new Promise(async r => { + return new Promise(async r => { term.onCursorMove(() => r()); await term.writeP('foo'); }); }); it('should fire the onLineFeed event', () => { - return new Promise(async r => { + return new Promise(async r => { term.onLineFeed(() => r()); await term.writeP('\n'); }); }); it('should fire a scroll event when scrollback is created', () => { - return new Promise(async r => { + 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', () => { - return new Promise(async r => { + return new Promise(async r => { await term.writeP('\n'.repeat(INIT_ROWS)); term.onScroll(() => r()); term.clear(); @@ -233,7 +233,7 @@ describe('Terminal', () => { term.paste('\r\nfoo\nbar\r'); }); it('should respect bracketed paste mode', () => { - return new Promise(async r => { + return new Promise(async r => { term.onData(e => { assert.equal(e, '\x1b[200~foo\x1b[201~'); r(); diff --git a/src/browser/Terminal.ts b/src/browser/Terminal.ts index 9d9246e38..f14bffe07 100644 --- a/src/browser/Terminal.ts +++ b/src/browser/Terminal.ts @@ -147,7 +147,6 @@ export class Terminal extends CoreTerminal implements ITerminal { this.register(this._inputHandler.onRequestBell(() => this.bell())); this.register(this._inputHandler.onRequestRefreshRows((start, end) => this.refresh(start, end))); this.register(this._inputHandler.onRequestReset(() => this.reset())); - this.register(this._inputHandler.onRequestScroll((eraseAttr, isWrapped) => this._bufferService.scroll(eraseAttr, isWrapped || undefined))); this.register(this._inputHandler.onRequestWindowsOptionsReport(type => this._reportWindowsOptions(type))); this.register(this._inputHandler.onAnsiColorChange((event) => this._changeAnsiColor(event))); this.register(forwardEvent(this._inputHandler.onCursorMove, this._onCursorMove)); diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 1d076692a..555994e49 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -123,7 +123,10 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this.register(forwardEvent(this._coreService.onData, this._onData)); this.register(forwardEvent(this._coreService.onBinary, this._onBinary)); this.register(this.optionsService.onOptionChange(key => this._updateOptions(key))); - this.register(this._bufferService.onScroll(event => this._onScroll.fire(event))); + this.register(this._bufferService.onScroll(event => { + this._onScroll.fire(event); + this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); + })); // Setup WriteBuffer this._writeBuffer = new WriteBuffer((data, promiseResult) => this._inputHandler.parse(data, promiseResult)); diff --git a/src/common/InputHandler.test.ts b/src/common/InputHandler.test.ts index ecd476fcf..cb60aec3d 100644 --- a/src/common/InputHandler.test.ts +++ b/src/common/InputHandler.test.ts @@ -71,115 +71,108 @@ describe('InputHandler', () => { inputHandler = new TestInputHandler(bufferService, new MockCharsetService(), coreService, new MockDirtyRowService(), new MockLogService(), optionsService, new MockCoreMouseService(), new MockUnicodeService()); }); - describe('Terminal InputHandler integration', () => { - function getLines(limit: number): string[] { - const res: string[] = []; - for (let i = 0; i < limit; ++i) { - res.push(bufferService.buffer.lines.get(i)!.translateToString(true)); - } - return res; - } - - function reset(): void { - bufferService.buffer.y = 0; - bufferService.buffer.x = 0; - } - - // This suite cannot live in InputHandler unless Terminal.scroll moved into IBufferService - describe('SL/SR/DECIC/DECDC', () => { - - it('SL (scrollLeft)', async () => { - inputHandler.parseP('12345'.repeat(6)); - assert.deepEqual(getLines(6), ['12345', '2345', '2345', '2345', '2345', '2345']); - inputHandler.parseP('\x1b[0 @'); - assert.deepEqual(getLines(6), ['12345', '345', '345', '345', '345', '345']); - inputHandler.parseP('\x1b[2 @'); - assert.deepEqual(getLines(6), ['12345', '5', '5', '5', '5', '5']); - }); - it('SR (scrollRight)', async () => { - inputHandler.parseP('12345'.repeat(6)); - inputHandler.parseP('\x1b[ A'); - assert.deepEqual(getLines(6), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']); - inputHandler.parseP('\x1b[0 A'); - assert.deepEqual(getLines(6), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']); - inputHandler.parseP('\x1b[2 A'); - assert.deepEqual(getLines(6), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']); - }); - it('insertColumns (DECIC)', async () => { - inputHandler.parseP('12345'.repeat(6)); - inputHandler.parseP('\x1b[3;3H'); - inputHandler.parseP('\x1b[\'}'); - assert.deepEqual(getLines(6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); - reset(); - inputHandler.parseP('12345'.repeat(6)); - inputHandler.parseP('\x1b[3;3H'); - inputHandler.parseP('\x1b[1\'}'); - assert.deepEqual(getLines(6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); - reset(); - inputHandler.parseP('12345'.repeat(6)); - inputHandler.parseP('\x1b[3;3H'); - inputHandler.parseP('\x1b[2\'}'); - assert.deepEqual(getLines(6), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']); - }); - it('deleteColumns (DECDC)', async () => { - inputHandler.parseP('12345'.repeat(6)); - inputHandler.parseP('\x1b[3;3H'); - inputHandler.parseP('\x1b[\'~'); - assert.deepEqual(getLines(6), ['12345', '1245', '1245', '1245', '1245', '1245']); - reset(); - inputHandler.parseP('12345'.repeat(6)); - inputHandler.parseP('\x1b[3;3H'); - inputHandler.parseP('\x1b[1\'~'); - assert.deepEqual(getLines(6), ['12345', '1245', '1245', '1245', '1245', '1245']); - reset(); - inputHandler.parseP('12345'.repeat(6)); - inputHandler.parseP('\x1b[3;3H'); - inputHandler.parseP('\x1b[2\'~'); - assert.deepEqual(getLines(6), ['12345', '125', '125', '125', '125', '125']); - }); + describe('SL/SR/DECIC/DECDC', () => { + beforeEach(() => { + bufferService.resize(5, 5); + optionsService.options.scrollback = 1; + bufferService.reset(); }); + it('SL (scrollLeft)', async () => { + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[ @'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '2345', '2345', '2345', '2345', '2345']); + inputHandler.parseP('\x1b[0 @'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '345', '345', '345', '345', '345']); + inputHandler.parseP('\x1b[2 @'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '5', '5', '5', '5', '5']); + }); + it('SR (scrollRight)', async () => { + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[ A'); + assert.deepEqual(getLines(bufferService, 6), ['12345', ' 1234', ' 1234', ' 1234', ' 1234', ' 1234']); + inputHandler.parseP('\x1b[0 A'); + assert.deepEqual(getLines(bufferService, 6), ['12345', ' 123', ' 123', ' 123', ' 123', ' 123']); + inputHandler.parseP('\x1b[2 A'); + assert.deepEqual(getLines(bufferService, 6), ['12345', ' 1', ' 1', ' 1', ' 1', ' 1']); + }); + it('insertColumns (DECIC)', async () => { + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[\'}'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); + bufferService.reset(); + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[1\'}'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '12 34', '12 34', '12 34', '12 34', '12 34']); + bufferService.reset(); + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[2\'}'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '12 3', '12 3', '12 3', '12 3', '12 3']); + }); + it('deleteColumns (DECDC)', async () => { + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[\'~'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '1245', '1245', '1245', '1245', '1245']); + bufferService.reset(); + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[1\'~'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '1245', '1245', '1245', '1245', '1245']); + bufferService.reset(); + inputHandler.parseP('12345'.repeat(6)); + inputHandler.parseP('\x1b[3;3H'); + inputHandler.parseP('\x1b[2\'~'); + assert.deepEqual(getLines(bufferService, 6), ['12345', '125', '125', '125', '125', '125']); + }); + }); - describe('BS with reverseWraparound set/unset', () => { - const ttyBS = '\x08 \x08'; // tty ICANON sends on pressing BS + describe('BS with reverseWraparound set/unset', () => { + const ttyBS = '\x08 \x08'; // tty ICANON sends on pressing BS + beforeEach(() => { + bufferService.resize(5, 5); + optionsService.options.scrollback = 1; + bufferService.reset(); + }); + describe('reverseWraparound set', () => { + it('should not reverse outside of scroll margins', async () => { + // prepare buffer content + inputHandler.parseP('#####abcdefghijklmnopqrstuvwxy'); + assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', 'uvwxy']); + assert.equal(bufferService.buffers.active.ydisp, 1); + assert.equal(bufferService.buffers.active.x, 5); + assert.equal(bufferService.buffers.active.y, 4); + inputHandler.parseP(ttyBS.repeat(100)); + assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']); - describe('reverseWraparound set', () => { - it('should not reverse outside of scroll margins', async () => { - // prepare buffer content - inputHandler.parseP('#####abcdefghijklmnopqrstuvwxy'); - assert.deepEqual(getLines(5), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', 'uvwxy']); - assert.equal(bufferService.buffers.active.ydisp, 1); - assert.equal(bufferService.buffers.active.x, 5); - assert.equal(bufferService.buffers.active.y, 4); - inputHandler.parseP(ttyBS.repeat(100)); - assert.deepEqual(getLines(5), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' y']); + inputHandler.parseP('\x1b[?45h'); + inputHandler.parseP('uvwxy'); - inputHandler.parseP('\x1b[?45h'); - inputHandler.parseP('uvwxy'); + // set top/bottom to 1/3 (0-based) + inputHandler.parseP('\x1b[2;4r'); + // place cursor below scroll bottom + bufferService.buffers.active.x = 5; + bufferService.buffers.active.y = 4; + inputHandler.parseP(ttyBS.repeat(100)); + assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']); - // set top/bottom to 1/3 (0-based) - inputHandler.parseP('\x1b[2;4r'); - // place cursor below scroll bottom - bufferService.buffers.active.x = 5; - bufferService.buffers.active.y = 4; - inputHandler.parseP(ttyBS.repeat(100)); - assert.deepEqual(getLines(5), ['#####', 'abcde', 'fghij', 'klmno', 'pqrst', ' ']); + inputHandler.parseP('uvwxy'); + // place cursor within scroll margins + bufferService.buffers.active.x = 5; + bufferService.buffers.active.y = 3; + inputHandler.parseP(ttyBS.repeat(100)); + assert.deepEqual(getLines(bufferService, 6), ['#####', 'abcde', ' ', ' ', ' ', 'uvwxy']); + assert.equal(bufferService.buffers.active.x, 0); + assert.equal(bufferService.buffers.active.y, bufferService.buffers.active.scrollTop); // stops at 0, scrollTop - inputHandler.parseP('uvwxy'); - // place cursor within scroll margins - bufferService.buffers.active.x = 5; - bufferService.buffers.active.y = 3; - inputHandler.parseP(ttyBS.repeat(100)); - assert.deepEqual(getLines(5), ['#####', 'abcde', ' ', ' ', ' ', 'uvwxy']); - assert.equal(bufferService.buffers.active.x, 0); - assert.equal(bufferService.buffers.active.y, bufferService.buffers.active.scrollTop); // stops at 0, scrollTop - - inputHandler.parseP('fghijklmnopqrst'); - // place cursor above scroll top - bufferService.buffers.active.x = 5; - bufferService.buffers.active.y = 0; - inputHandler.parseP(ttyBS.repeat(100)); - assert.deepEqual(getLines(5), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']); - }); + inputHandler.parseP('fghijklmnopqrst'); + // place cursor above scroll top + bufferService.buffers.active.x = 5; + bufferService.buffers.active.y = 0; + inputHandler.parseP(ttyBS.repeat(100)); + assert.deepEqual(getLines(bufferService, 6), ['#####', ' ', 'fghij', 'klmno', 'pqrst', 'uvwxy']); }); }); }); diff --git a/src/common/InputHandler.ts b/src/common/InputHandler.ts index 13b0a0e7c..3af2f9d74 100644 --- a/src/common/InputHandler.ts +++ b/src/common/InputHandler.ts @@ -240,8 +240,6 @@ export class InputHandler extends Disposable implements IInputHandler { public get onRequestRefreshRows(): IEvent { return this._onRequestRefreshRows.event; } private _onRequestReset = new EventEmitter(); public get onRequestReset(): IEvent { return this._onRequestReset.event; } - private _onRequestScroll = new EventEmitter(); - public get onRequestScroll(): IEvent { return this._onRequestScroll.event; } private _onRequestSyncScrollBar = new EventEmitter(); public get onRequestSyncScrollBar(): IEvent { return this._onRequestSyncScrollBar.event; } private _onRequestWindowsOptionsReport = new EventEmitter(); @@ -651,7 +649,7 @@ export class InputHandler extends Disposable implements IInputHandler { buffer.y++; if (buffer.y === buffer.scrollBottom + 1) { buffer.y--; - this._onRequestScroll.fire(this._eraseAttrData(), true); + this._bufferService.scroll(this._eraseAttrData(), true); } else { if (buffer.y >= this._bufferService.rows) { buffer.y = this._bufferService.rows - 1; @@ -791,7 +789,7 @@ export class InputHandler extends Disposable implements IInputHandler { buffer.y++; if (buffer.y === buffer.scrollBottom + 1) { buffer.y--; - this._onRequestScroll.fire(this._eraseAttrData()); + this._bufferService.scroll(this._eraseAttrData()); } else if (buffer.y >= this._bufferService.rows) { buffer.y = this._bufferService.rows - 1; } @@ -2987,7 +2985,7 @@ export class InputHandler extends Disposable implements IInputHandler { this._bufferService.buffer.y++; if (buffer.y === buffer.scrollBottom + 1) { buffer.y--; - this._onRequestScroll.fire(this._eraseAttrData()); + this._bufferService.scroll(this._eraseAttrData()); } else if (buffer.y >= this._bufferService.rows) { buffer.y = this._bufferService.rows - 1; } diff --git a/src/common/Types.d.ts b/src/common/Types.d.ts index 2dd3f4b8e..df2991956 100644 --- a/src/common/Types.d.ts +++ b/src/common/Types.d.ts @@ -357,7 +357,6 @@ export interface IAnsiColorChangeEvent { */ export interface IInputHandler { onTitleChange: IEvent; - onRequestScroll: IEvent; parse(data: string | Uint8Array, promiseResult?: boolean): void | Promise; print(data: Uint32Array, start: number, end: number): void; diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 7fe0cdc38..3833bb443 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -3,13 +3,12 @@ * @license MIT */ -import { IBufferService, IDirtyRowService, IInstantiationService, IOptionsService } from 'common/services/Services'; +import { IBufferService, IOptionsService } from 'common/services/Services'; import { BufferSet } from 'common/buffer/BufferSet'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; import { IAttributeData, IBufferLine } from 'common/Types'; -import { DirtyRowService } from 'common/services/DirtyRowService'; export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars export const MINIMUM_ROWS = 1; @@ -33,8 +32,6 @@ export class BufferService extends Disposable implements IBufferService { /** An IBufferline to clone/copy from for new blank lines */ private _cachedBlankLine: IBufferLine | undefined; - private _dirtyRowService: IDirtyRowService | undefined; - constructor( @IOptionsService private _optionsService: IOptionsService ) { @@ -123,12 +120,6 @@ export class BufferService extends Disposable implements IBufferService { buffer.ydisp = buffer.ybase; } - // Flag rows that need updating - if (!this._dirtyRowService) { - this._dirtyRowService = new DirtyRowService(this); - } - this._dirtyRowService?.markRangeDirty(buffer.scrollTop, buffer.scrollBottom); - this._onScroll.fire(buffer.ydisp); } From e40f340036f7cf8b463bec88c60dea796c932269 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 1 Apr 2021 11:28:41 -0700 Subject: [PATCH 05/15] use bufferService methods --- src/common/CoreTerminal.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 555994e49..4f1c8a57e 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -211,14 +211,11 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { * Scrolls the display of the terminal to the bottom. */ public scrollToBottom(): void { - this._bufferService.scrollLines(this._bufferService.buffer.ybase - this._bufferService.buffer.ydisp); + this._bufferService.scrollToBottom(); } public scrollToLine(line: number): void { - const scrollAmount = line - this._bufferService.buffer.ydisp; - if (scrollAmount !== 0) { - this._bufferService.scrollLines(scrollAmount); - } + this._bufferService.scrollToLine(line); } /** Add handler for ESC escape sequence. See xterm.d.ts for details. */ From 1c087f6bcd510ba3ce34f72397dc156d8fc5003d Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 1 Apr 2021 13:24:02 -0700 Subject: [PATCH 06/15] cherry picked commits and scrollSource --- src/common/CoreTerminal.ts | 6 +++--- src/common/services/BufferService.ts | 4 ++-- src/common/services/Services.ts | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 4f1c8a57e..72ad7d81e 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -124,7 +124,7 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { this.register(forwardEvent(this._coreService.onBinary, this._onBinary)); this.register(this.optionsService.onOptionChange(key => this._updateOptions(key))); this.register(this._bufferService.onScroll(event => { - this._onScroll.fire(event); + this._onScroll.fire({position: this._bufferService.buffer.ydisp, source: ScrollSource.TERMINAL}); this._dirtyRowService.markRangeDirty(this._bufferService.buffer.scrollTop, this._bufferService.buffer.scrollBottom); })); @@ -188,8 +188,8 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { * to avoid unwanted events being handled by the viewport when the event was triggered from the * viewport originally. */ - public scrollLines(disp: number, suppressScrollEvent?: boolean): void { - this._bufferService.scrollLines(disp, suppressScrollEvent); + public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void { + this._bufferService.scrollLines(disp, suppressScrollEvent, source); } /** diff --git a/src/common/services/BufferService.ts b/src/common/services/BufferService.ts index 3833bb443..99594d228 100644 --- a/src/common/services/BufferService.ts +++ b/src/common/services/BufferService.ts @@ -8,7 +8,7 @@ import { BufferSet } from 'common/buffer/BufferSet'; import { IBufferSet, IBuffer } from 'common/buffer/Types'; import { EventEmitter, IEvent } from 'common/EventEmitter'; import { Disposable } from 'common/Lifecycle'; -import { IAttributeData, IBufferLine } from 'common/Types'; +import { IAttributeData, IBufferLine, ScrollSource } from 'common/Types'; export const MINIMUM_COLS = 2; // Less than 2 can mess with wide chars export const MINIMUM_ROWS = 1; @@ -130,7 +130,7 @@ export class BufferService extends Disposable implements IBufferService { * to avoid unwanted events being handled by the viewport when the event was triggered from the * viewport originally. */ - public scrollLines(disp: number, suppressScrollEvent?: boolean): void { + public scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void { const buffer = this.buffer; if (disp < 0) { if (buffer.ydisp === 0) { diff --git a/src/common/services/Services.ts b/src/common/services/Services.ts index 40c0c1b2d..8b21f100a 100644 --- a/src/common/services/Services.ts +++ b/src/common/services/Services.ts @@ -5,7 +5,7 @@ import { IEvent } from 'common/EventEmitter'; import { IBuffer, IBufferSet } from 'common/buffer/Types'; -import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData } from 'common/Types'; +import { IDecPrivateModes, ICoreMouseEvent, CoreMouseEncoding, ICoreMouseProtocol, CoreMouseEventType, ICharset, IWindowOptions, IModes, IAttributeData, ScrollSource } from 'common/Types'; import { createDecorator } from 'common/services/ServiceRegistry'; export const IBufferService = createDecorator('BufferService'); @@ -23,7 +23,7 @@ export interface IBufferService { scrollToBottom(): void; scrollToTop(): void; scrollToLine(line: number): void; - scrollLines(disp: number, suppressScrollEvent?: boolean): void; + scrollLines(disp: number, suppressScrollEvent?: boolean, source?: ScrollSource): void; scrollPages(pageCount: number): void; resize(cols: number, rows: number): void; reset(): void; From b1877b4dc7b903fabaf9edbba116887ce0e83474 Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 1 Apr 2021 13:32:35 -0700 Subject: [PATCH 07/15] onRecoverContext -> onContextLoss --- addons/xterm-addon-webgl/src/WebglAddon.ts | 6 +++--- addons/xterm-addon-webgl/src/WebglRenderer.ts | 11 +++-------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/addons/xterm-addon-webgl/src/WebglAddon.ts b/addons/xterm-addon-webgl/src/WebglAddon.ts index 8eeda832e..c07bc0d70 100644 --- a/addons/xterm-addon-webgl/src/WebglAddon.ts +++ b/addons/xterm-addon-webgl/src/WebglAddon.ts @@ -12,8 +12,8 @@ import { EventEmitter } from 'common/EventEmitter'; export class WebglAddon implements ITerminalAddon { private _terminal?: Terminal; private _renderer?: WebglRenderer; - private _onRecoverContext = new EventEmitter(); - public get onRecoverContext(): IEvent { return this._onRecoverContext.event; } + private _onContextLoss = new EventEmitter(); + public get onContextLoss(): IEvent { return this._onContextLoss.event; } constructor( private _preserveDrawingBuffer?: boolean @@ -27,7 +27,7 @@ export class WebglAddon implements ITerminalAddon { const renderService: IRenderService = (terminal)._core._renderService; const colors: IColorSet = (terminal)._core._colorManager.colors; this._renderer = new WebglRenderer(terminal, colors, this._preserveDrawingBuffer); - this._renderer.onRecoverContext(() => this._onRecoverContext.fire()); + this._renderer.onContextLoss(() => this._onContextLoss.fire()); renderService.setRenderer(this._renderer); } diff --git a/addons/xterm-addon-webgl/src/WebglRenderer.ts b/addons/xterm-addon-webgl/src/WebglRenderer.ts index 86e2900d3..08f83b523 100644 --- a/addons/xterm-addon-webgl/src/WebglRenderer.ts +++ b/addons/xterm-addon-webgl/src/WebglRenderer.ts @@ -42,8 +42,8 @@ export class WebglRenderer extends Disposable implements IRenderer { private _onRequestRedraw = new EventEmitter(); public get onRequestRedraw(): IEvent { return this._onRequestRedraw.event; } - private _onRecoverContext = new EventEmitter(); - public get onRecoverContext(): IEvent { return this._onRecoverContext.event; } + private _onContextLoss = new EventEmitter(); + public get onContextLoss(): IEvent { return this._onContextLoss.event; } constructor( private _terminal: Terminal, @@ -87,7 +87,7 @@ export class WebglRenderer extends Disposable implements IRenderer { throw new Error('WebGL2 not supported ' + this._gl); } - this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLost(e); })); + this.register(addDisposableDomListener(this._canvas, 'webglcontextlost', (e) => { this._onContextLoss.fire(e); })); this._core.screenElement!.appendChild(this._canvas); @@ -100,11 +100,6 @@ export class WebglRenderer extends Disposable implements IRenderer { this._isAttached = document.body.contains(this._core.screenElement!); } - private _onContextLost(e: Event): void { - e.preventDefault(); - this._onRecoverContext.fire(); - } - public dispose(): void { for (const l of this._renderLayers) { l.dispose(); From c5339116b5685042afaed3fb31cd7114fc0a094f Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 1 Apr 2021 15:45:10 -0700 Subject: [PATCH 08/15] Add note to readme on handling context loss --- addons/xterm-addon-webgl/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/addons/xterm-addon-webgl/README.md b/addons/xterm-addon-webgl/README.md index 756999db5..7927a5138 100644 --- a/addons/xterm-addon-webgl/README.md +++ b/addons/xterm-addon-webgl/README.md @@ -19,3 +19,18 @@ terminal.loadAddon(new WebglAddon()); ``` See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts) for more advanced usage. + +### Handling Context Loss + +The browser may drop WebGL contexts for various reasons like OOM or after the system has been suspended. There is an API exposed that fires the `webglcontextlost` event fires on the canvas so embedders can handle it however they wish. An easy way but suboptimal way to handle this is by disposing of WebglAddon when the event fires: + +```ts +const terminal = new Terminal(); +const addon = new WebglAddon(); +addon.onContextLoss(e => { + addon.dispose(); +}); +terminal.loadAddon(addon); +``` + +Read more about handling WebGL context losses on the [Khronos wiki](https://www.khronos.org/webgl/wiki/HandlingContextLost). From ecb485d2f58f215fb5a1d90d96f691287837bdbf Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 1 Apr 2021 15:54:47 -0700 Subject: [PATCH 09/15] Remove _cachedBlankLine --- src/common/CoreTerminal.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/common/CoreTerminal.ts b/src/common/CoreTerminal.ts index 72ad7d81e..699a1b1c4 100644 --- a/src/common/CoreTerminal.ts +++ b/src/common/CoreTerminal.ts @@ -58,8 +58,6 @@ export abstract class CoreTerminal extends Disposable implements ICoreTerminal { protected _inputHandler: InputHandler; private _writeBuffer: WriteBuffer; private _windowsMode: IDisposable | undefined; - /** An IBufferline to clone/copy from for new blank lines */ - private _cachedBlankLine: IBufferLine | undefined; private _onBinary = new EventEmitter(); public get onBinary(): IEvent { return this._onBinary.event; } From 117f990055b3f0eee5890cce3be90d0902804f58 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 1 Apr 2021 16:39:59 -0700 Subject: [PATCH 10/15] Don't pad powerline glyphs Fixes #3278 --- .../src/atlas/WebglCharAtlas.ts | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 223ec16ae..6a0f9a678 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -367,8 +367,20 @@ export class WebglCharAtlas implements IDisposable { this._tmpCtx.globalAlpha = DIM_OPACITY; } + // Check if the char is a powerline glyph + let isPowerlineGlyph = false; + if (chars.length === 1) { + const code = chars.charCodeAt(0); + if (code >= 0xE0A0 && code <= 0xE0D6) { + isPowerlineGlyph = true; + } + } + + // For powerline glyphs left/top padding is excluded (https://github.com/microsoft/vscode/issues/120129) + const padding = isPowerlineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING; + // Draw the character - this._tmpCtx.fillText(chars, TMP_CANVAS_GLYPH_PADDING, TMP_CANVAS_GLYPH_PADDING + this._config.scaledCharHeight / 2); + this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight / 2); this._tmpCtx.restore(); // clear the background from the character to avoid issues with drawing over the previous @@ -391,7 +403,7 @@ export class WebglCharAtlas implements IDisposable { return NULL_RASTERIZED_GLYPH; } - const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox); + const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, padding); const clippedImageData = this._clipImageData(imageData, this._workBoundingBox); // Check if there is enough room in the current row and go to next if needed @@ -424,7 +436,7 @@ export class WebglCharAtlas implements IDisposable { * @param imageData The image data to read. * @param boundingBox An IBoundingBox to put the clipped bounding box values. */ - private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox): IRasterizedGlyph { + private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, padding: number): IRasterizedGlyph { boundingBox.top = 0; let found = false; for (let y = 0; y < this._tmpCanvas.height; y++) { @@ -497,8 +509,8 @@ export class WebglCharAtlas implements IDisposable { y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT }, offset: { - x: -boundingBox.left + TMP_CANVAS_GLYPH_PADDING, - y: -boundingBox.top + TMP_CANVAS_GLYPH_PADDING + x: -boundingBox.left + padding, + y: -boundingBox.top + padding } }; } From b6d3120086d8b5fda0e42b5fb05f719cb836da5c Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 1 Apr 2021 16:55:27 -0700 Subject: [PATCH 11/15] Restrict all sides of powerline glyphs --- .../src/atlas/WebglCharAtlas.ts | 42 ++++++++++--------- 1 file changed, 23 insertions(+), 19 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index 6a0f9a678..d368405ba 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -367,7 +367,9 @@ export class WebglCharAtlas implements IDisposable { this._tmpCtx.globalAlpha = DIM_OPACITY; } - // Check if the char is a powerline glyph + // Check if the char is a powerline glyph, these will be restricted to a single cell glyph, no + // padding on either side that are allowed for other glyphs since they are designed to be pixel + // perfect but may render with "bad" anti-aliasing let isPowerlineGlyph = false; if (chars.length === 1) { const code = chars.charCodeAt(0); @@ -403,7 +405,7 @@ export class WebglCharAtlas implements IDisposable { return NULL_RASTERIZED_GLYPH; } - const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, padding); + const rasterizedGlyph = this._findGlyphBoundingBox(imageData, this._workBoundingBox, isPowerlineGlyph); const clippedImageData = this._clipImageData(imageData, this._workBoundingBox); // Check if there is enough room in the current row and go to next if needed @@ -436,12 +438,14 @@ export class WebglCharAtlas implements IDisposable { * @param imageData The image data to read. * @param boundingBox An IBoundingBox to put the clipped bounding box values. */ - private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, padding: number): IRasterizedGlyph { + private _findGlyphBoundingBox(imageData: ImageData, boundingBox: IBoundingBox, restrictedGlyph: boolean): IRasterizedGlyph { boundingBox.top = 0; + const height = restrictedGlyph ? this._config.scaledCharHeight : this._tmpCanvas.height; + const width = restrictedGlyph ? this._config.scaledCharWidth : this._tmpCanvas.width; let found = false; - for (let y = 0; y < this._tmpCanvas.height; y++) { - for (let x = 0; x < this._tmpCanvas.width; x++) { - const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const alphaOffset = y * width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { boundingBox.top = y; found = true; @@ -454,9 +458,9 @@ export class WebglCharAtlas implements IDisposable { } boundingBox.left = 0; found = false; - for (let x = 0; x < this._tmpCanvas.width; x++) { - for (let y = 0; y < this._tmpCanvas.height; y++) { - const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; + for (let x = 0; x < width; x++) { + for (let y = 0; y < height; y++) { + const alphaOffset = y * width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { boundingBox.left = x; found = true; @@ -467,11 +471,11 @@ export class WebglCharAtlas implements IDisposable { break; } } - boundingBox.right = this._tmpCanvas.width; + boundingBox.right = width; found = false; - for (let x = this._tmpCanvas.width - 1; x >= 0; x--) { - for (let y = 0; y < this._tmpCanvas.height; y++) { - const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; + for (let x = width - 1; x >= 0; x--) { + for (let y = 0; y < height; y++) { + const alphaOffset = y * width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { boundingBox.right = x; found = true; @@ -482,11 +486,11 @@ export class WebglCharAtlas implements IDisposable { break; } } - boundingBox.bottom = this._tmpCanvas.height; + boundingBox.bottom = height; found = false; - for (let y = this._tmpCanvas.height - 1; y >= 0; y--) { - for (let x = 0; x < this._tmpCanvas.width; x++) { - const alphaOffset = y * this._tmpCanvas.width * 4 + x * 4 + 3; + for (let y = height - 1; y >= 0; y--) { + for (let x = 0; x < width; x++) { + const alphaOffset = y * width * 4 + x * 4 + 3; if (imageData.data[alphaOffset] !== 0) { boundingBox.bottom = y; found = true; @@ -509,8 +513,8 @@ export class WebglCharAtlas implements IDisposable { y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT }, offset: { - x: -boundingBox.left + padding, - y: -boundingBox.top + padding + x: -boundingBox.left + (restrictedGlyph ? TMP_CANVAS_GLYPH_PADDING : 0), + y: -boundingBox.top + (restrictedGlyph ? TMP_CANVAS_GLYPH_PADDING : 0) } }; } From 113086f97ad767d0d78a7fdd2b0e25a09c69534b Mon Sep 17 00:00:00 2001 From: meganrogge Date: Thu, 1 Apr 2021 16:57:19 -0700 Subject: [PATCH 12/15] tweak readme, expose API --- addons/xterm-addon-webgl/README.md | 2 +- addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/addons/xterm-addon-webgl/README.md b/addons/xterm-addon-webgl/README.md index 7927a5138..67fafe7bf 100644 --- a/addons/xterm-addon-webgl/README.md +++ b/addons/xterm-addon-webgl/README.md @@ -22,7 +22,7 @@ See the full [API](https://github.com/xtermjs/xterm.js/blob/master/addons/xterm- ### Handling Context Loss -The browser may drop WebGL contexts for various reasons like OOM or after the system has been suspended. There is an API exposed that fires the `webglcontextlost` event fires on the canvas so embedders can handle it however they wish. An easy way but suboptimal way to handle this is by disposing of WebglAddon when the event fires: +The browser may drop WebGL contexts for various reasons like OOM or after the system has been suspended. There is an API exposed that fires the `webglcontextlost` event fired on the canvas so embedders can handle it however they wish. An easy, but suboptimal way, to handle this is by disposing of WebglAddon when the event fires: ```ts const terminal = new Terminal(); diff --git a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts index 5c15aa178..d95d89617 100644 --- a/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts +++ b/addons/xterm-addon-webgl/typings/xterm-addon-webgl.d.ts @@ -3,6 +3,7 @@ * @license MIT */ +import { IEvent } from 'node-pty'; import { Terminal, ITerminalAddon } from 'xterm'; declare module 'xterm-addon-webgl' { @@ -29,5 +30,10 @@ declare module 'xterm-addon-webgl' { * Clears the terminal's texture atlas and triggers a redraw. */ public clearTextureAtlas(): void; + + /** + * Fired when the WebglRenderer loses context + */ + public get onContextLoss(): IEvent; } } From 62ca4b7e137ec6f8c5845fd4a5ab28846d190f5b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 1 Apr 2021 17:00:02 -0700 Subject: [PATCH 13/15] Correct restricted glyph padding --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index d368405ba..a1995faae 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -513,8 +513,8 @@ export class WebglCharAtlas implements IDisposable { y: (boundingBox.bottom - boundingBox.top + 1) / TEXTURE_HEIGHT }, offset: { - x: -boundingBox.left + (restrictedGlyph ? TMP_CANVAS_GLYPH_PADDING : 0), - y: -boundingBox.top + (restrictedGlyph ? TMP_CANVAS_GLYPH_PADDING : 0) + x: -boundingBox.left + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING), + y: -boundingBox.top + (restrictedGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING) } }; } From 62f5b8291d89e45879070df9f246abcb5f764820 Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 1 Apr 2021 17:05:29 -0700 Subject: [PATCH 14/15] Use ideographic over middle This seems to correctly center the glyphs within the cell, making powerline fonts appear perfectly aligned. Fixes #3281 --- addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts index a1995faae..1396d09f8 100644 --- a/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts +++ b/addons/xterm-addon-webgl/src/atlas/WebglCharAtlas.ts @@ -358,7 +358,7 @@ export class WebglCharAtlas implements IDisposable { const fontStyle = italic ? 'italic' : ''; this._tmpCtx.font = `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; - this._tmpCtx.textBaseline = 'middle'; + this._tmpCtx.textBaseline = 'ideographic'; this._tmpCtx.fillStyle = this._getForegroundCss(bg, bgColorMode, bgColor, fg, fgColorMode, fgColor, inverse, bold); @@ -382,7 +382,7 @@ export class WebglCharAtlas implements IDisposable { const padding = isPowerlineGlyph ? 0 : TMP_CANVAS_GLYPH_PADDING; // Draw the character - this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight / 2); + this._tmpCtx.fillText(chars, padding, padding + this._config.scaledCharHeight); this._tmpCtx.restore(); // clear the background from the character to avoid issues with drawing over the previous From a510ffb3c03c2db160579bea6fded2191ee59f0b Mon Sep 17 00:00:00 2001 From: Daniel Imms <2193314+Tyriar@users.noreply.github.com> Date: Thu, 1 Apr 2021 17:10:14 -0700 Subject: [PATCH 15/15] Use ideographic baseline for canvas --- .../xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts | 4 ++-- src/browser/renderer/BaseRenderLayer.ts | 8 ++++---- src/browser/renderer/atlas/DynamicCharAtlas.ts | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts index da3b4d418..6229fb7f4 100644 --- a/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts +++ b/addons/xterm-addon-webgl/src/renderLayer/BaseRenderLayer.ts @@ -224,12 +224,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected _fillCharTrueColor(terminal: Terminal, cell: CellData, x: number, y: number): void { this._ctx.font = this._getFont(terminal, false, false); - this._ctx.textBaseline = 'middle'; + this._ctx.textBaseline = 'ideographic'; this._clipRow(terminal, y); this._ctx.fillText( cell.getChars(), x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight); } /** diff --git a/src/browser/renderer/BaseRenderLayer.ts b/src/browser/renderer/BaseRenderLayer.ts index 8afec352c..b7646bee8 100644 --- a/src/browser/renderer/BaseRenderLayer.ts +++ b/src/browser/renderer/BaseRenderLayer.ts @@ -242,12 +242,12 @@ export abstract class BaseRenderLayer implements IRenderLayer { */ protected _fillCharTrueColor(cell: CellData, x: number, y: number): void { this._ctx.font = this._getFont(false, false); - this._ctx.textBaseline = 'middle'; + this._ctx.textBaseline = 'ideographic'; this._clipRow(y); this._ctx.fillText( cell.getChars(), x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight); } /** @@ -320,7 +320,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { private _drawUncachedChars(cell: ICellData, x: number, y: number, fgOverride?: IColor): void { this._ctx.save(); this._ctx.font = this._getFont(!!cell.isBold(), !!cell.isItalic()); - this._ctx.textBaseline = 'middle'; + this._ctx.textBaseline = 'ideographic'; if (cell.isInverse()) { if (fgOverride) { @@ -362,7 +362,7 @@ export abstract class BaseRenderLayer implements IRenderLayer { this._ctx.fillText( cell.getChars(), x * this._scaledCellWidth + this._scaledCharLeft, - y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight / 2); + y * this._scaledCellHeight + this._scaledCharTop + this._scaledCharHeight); this._ctx.restore(); } diff --git a/src/browser/renderer/atlas/DynamicCharAtlas.ts b/src/browser/renderer/atlas/DynamicCharAtlas.ts index 696fb63c4..bf90bab6e 100644 --- a/src/browser/renderer/atlas/DynamicCharAtlas.ts +++ b/src/browser/renderer/atlas/DynamicCharAtlas.ts @@ -256,7 +256,7 @@ export class DynamicCharAtlas extends BaseCharAtlas { const fontStyle = glyph.italic ? 'italic' : ''; this._tmpCtx.font = `${fontStyle} ${fontWeight} ${this._config.fontSize * this._config.devicePixelRatio}px ${this._config.fontFamily}`; - this._tmpCtx.textBaseline = 'middle'; + this._tmpCtx.textBaseline = 'ideographic'; this._tmpCtx.fillStyle = this._getForegroundColor(glyph).css; @@ -265,7 +265,7 @@ export class DynamicCharAtlas extends BaseCharAtlas { this._tmpCtx.globalAlpha = DIM_OPACITY; } // Draw the character - this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight / 2); + this._tmpCtx.fillText(glyph.chars, 0, this._config.scaledCharHeight); this._tmpCtx.restore(); // clear the background from the character to avoid issues with drawing over the previous