From e2885f3a3877c36564195dd3fe0d9c24d924dc3d Mon Sep 17 00:00:00 2001 From: meganrogge Date: Wed, 31 Mar 2021 20:07:54 -0700 Subject: [PATCH 1/6] 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 5724715f..f3bcdfda 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 1ab207c7..9d9246e3 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 936aa0c8..9f855845 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 b5067193..93de6562 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 01ceacbd..dce0f570 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 47e54729..7fe0cdc3 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 1fbf57fb..40c0c1b2 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 2/6] 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 f3bcdfda..8f10e622 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 9f855845..1d076692 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 93de6562..ecd476fc 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 3/6] 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 8f10e622..b0075d88 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 9d9246e3..f14bffe0 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 1d076692..555994e4 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 ecd476fc..cb60aec3 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 13b0a0e7..3af2f9d7 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 2dd3f4b8..df299195 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 7fe0cdc3..3833bb44 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 4/6] 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 555994e4..4f1c8a57 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 5/6] 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 4f1c8a57..72ad7d81 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 3833bb44..99594d22 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 40c0c1b2..8b21f100 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 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 6/6] 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 72ad7d81..699a1b1c 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; }