From 70afb13c9be836321c75dd4e86019712ea70036b Mon Sep 17 00:00:00 2001 From: peacefullatom Date: Sun, 2 Sep 2018 02:31:26 +0500 Subject: [PATCH 01/34] #900 Add tests for events on Terminal --- src/Terminal.test.ts | 142 +++++++++++++++++++++++++++++++++++++------ 1 file changed, 123 insertions(+), 19 deletions(-) diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 83ab9a68..eb6c4723 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -26,7 +26,7 @@ describe('term.js addons', () => { beforeEach(() => { term = new TestTerminal(termOptions); - term.refresh = () => {}; + term.refresh = () => { }; (term).renderer = new MockRenderer(); term.viewport = new MockViewport(); (term)._compositionHelper = new MockCompositionHelper(); @@ -37,8 +37,8 @@ describe('term.js addons', () => { }; (term).element = { classList: { - toggle: () => {}, - remove: () => {} + toggle: () => { }, + remove: () => { } } }; }); @@ -68,24 +68,128 @@ describe('term.js addons', () => { }); }); + describe('on', () => { + beforeEach(() => { + term.on('key', () => { }); + term.on('keypress', () => { }); + term.on('keydown', () => { }); + }); + + describe('data', () => { + it('should emit a data event', (done) => { + term.on('data', () => { + done(); + }); + + term.handler('fake'); + }); + }); + + describe(`keypress (including 'key' event)`, () => { + it('should receive a string and event object', () => { + const evKeyPress = { + preventDefault: () => { }, + stopPropagation: () => { }, + type: 'keypress', + keyCode: 77 + }; + + term.on('keypress', (key, event) => { + assert.equal(typeof key, 'string'); + expect(event).to.be.an.instanceof(Object); + }); + + term.on('key', (key, event) => { + assert.equal(typeof key, 'string'); + expect(event).to.be.an.instanceof(Object); + }); + + term.keyPress(evKeyPress); + }); + }); + + describe(`keydown (including 'key' event)`, () => { + it('should receive a string and event object', () => { + const evKeyDown = { + preventDefault: () => { }, + stopPropagation: () => { }, + type: 'keydown', + keyCode: 77 + }; + + term.on('keydown', (key, event) => { + assert.equal(typeof key, 'string'); + expect(event).to.be.an.instanceof(Object); + }); + + term.on('key', (key, event) => { + assert.equal(typeof key, 'string'); + expect(event).to.be.an.instanceof(Object); + }); + + term.keyDown(evKeyDown); + }); + }); + + describe('refresh', () => { + it('should receive an object: {start: number, end: number}', () => { + term.on('refresh', (data) => { + expect(data).to.have.keys(['start', 'end']); + assert.equal(typeof data.start, 'number'); + assert.equal(typeof data.end, 'number'); + }); + term.refresh(0, term.rows - 1); + }); + }); + + describe('resize', () => { + it('should receive an object: {cols: number, rows: number}', () => { + term.on('resize', (data) => { + expect(data).to.have.keys(['cols', 'rows']); + assert.equal(typeof data.cols, 'number'); + assert.equal(typeof data.rows, 'number'); + }); + term.resize(1, 1); + }); + }); + + describe('scroll', () => { + it('should receive a number', () => { + term.on('scroll', (ydisp) => { + assert.equal(typeof ydisp, 'number'); + }); + term.scroll(); + }); + }); + + describe('title', () => { + it('should receive a string', () => { + term.on('title', (title) => { + assert.equal(typeof title, 'string'); + }); + term.handleTitle('title'); + }); + }); + }); + describe('attachCustomKeyEventHandler', () => { const evKeyDown = { - preventDefault: () => {}, - stopPropagation: () => {}, + preventDefault: () => { }, + stopPropagation: () => { }, type: 'keydown', keyCode: 77 }; const evKeyPress = { - preventDefault: () => {}, - stopPropagation: () => {}, + preventDefault: () => { }, + stopPropagation: () => { }, type: 'keypress', keyCode: 77 }; beforeEach(() => { - term.handler = () => {}; - term.showCursor = () => {}; - term.clearSelection = () => {}; + term.handler = () => { }; + term.showCursor = () => { }; + term.clearSelection = () => { }; }); it('should process the keydown/keypress event based on what the handler returns', () => { @@ -305,8 +409,8 @@ describe('term.js addons', () => { type: 'keydown', key: 'a', keyCode: 65, - preventDefault: () => {}, - stopPropagation: () => {} + preventDefault: () => { }, + stopPropagation: () => { } }; term.buffer.ydisp = 0; @@ -473,9 +577,9 @@ describe('term.js addons', () => { let evKeyPress: any; beforeEach(() => { - term.handler = () => {}; - term.showCursor = () => {}; - term.clearSelection = () => {}; + term.handler = () => { }; + term.showCursor = () => { }; + term.clearSelection = () => { }; // term.compositionHelper = { // isComposing: false, // keydown: { @@ -485,15 +589,15 @@ describe('term.js addons', () => { // } // }; evKeyDown = { - preventDefault: () => {}, - stopPropagation: () => {}, + preventDefault: () => { }, + stopPropagation: () => { }, type: 'keydown', altKey: null, keyCode: null }; evKeyPress = { - preventDefault: () => {}, - stopPropagation: () => {}, + preventDefault: () => { }, + stopPropagation: () => { }, type: 'keypress', altKey: null, charCode: null, From 48cf73c1522ef5a659faeca37a7abfd712c5339d Mon Sep 17 00:00:00 2001 From: peacefullatom Date: Wed, 5 Sep 2018 01:11:23 +0500 Subject: [PATCH 02/34] rewrote to async --- src/Terminal.test.ts | 47 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index eb6c4723..5eb188ce 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -86,22 +86,32 @@ describe('term.js addons', () => { }); describe(`keypress (including 'key' event)`, () => { - it('should receive a string and event object', () => { + it('should receive a string and event object', (done) => { + let steps = 0; + + const finish = () => { + if ((++steps) === 2) { + done(); + } + }; + const evKeyPress = { preventDefault: () => { }, stopPropagation: () => { }, type: 'keypress', - keyCode: 77 + keyCode: 13 }; term.on('keypress', (key, event) => { assert.equal(typeof key, 'string'); expect(event).to.be.an.instanceof(Object); + finish(); }); term.on('key', (key, event) => { assert.equal(typeof key, 'string'); expect(event).to.be.an.instanceof(Object); + finish(); }); term.keyPress(evKeyPress); @@ -109,22 +119,31 @@ describe('term.js addons', () => { }); describe(`keydown (including 'key' event)`, () => { - it('should receive a string and event object', () => { + it(`should receive an event object for 'keydown' and a string and event object for 'key'`, (done) => { + let steps = 0; + + const finish = () => { + if ((++steps) === 2) { + done(); + } + }; + const evKeyDown = { preventDefault: () => { }, stopPropagation: () => { }, type: 'keydown', - keyCode: 77 + keyCode: 13 }; - term.on('keydown', (key, event) => { - assert.equal(typeof key, 'string'); + term.on('keydown', (event) => { expect(event).to.be.an.instanceof(Object); + finish(); }); term.on('key', (key, event) => { assert.equal(typeof key, 'string'); expect(event).to.be.an.instanceof(Object); + finish(); }); term.keyDown(evKeyDown); @@ -132,41 +151,49 @@ describe('term.js addons', () => { }); describe('refresh', () => { - it('should receive an object: {start: number, end: number}', () => { + it('should receive an object: {start: number, end: number}', (done) => { term.on('refresh', (data) => { expect(data).to.have.keys(['start', 'end']); assert.equal(typeof data.start, 'number'); assert.equal(typeof data.end, 'number'); + done(); }); + term.refresh(0, term.rows - 1); }); }); describe('resize', () => { - it('should receive an object: {cols: number, rows: number}', () => { + it('should receive an object: {cols: number, rows: number}', (done) => { term.on('resize', (data) => { expect(data).to.have.keys(['cols', 'rows']); assert.equal(typeof data.cols, 'number'); assert.equal(typeof data.rows, 'number'); + done(); }); + term.resize(1, 1); }); }); describe('scroll', () => { - it('should receive a number', () => { + it('should receive a number', (done) => { term.on('scroll', (ydisp) => { assert.equal(typeof ydisp, 'number'); + done(); }); + term.scroll(); }); }); describe('title', () => { - it('should receive a string', () => { + it('should receive a string', (done) => { term.on('title', (title) => { assert.equal(typeof title, 'string'); + done(); }); + term.handleTitle('title'); }); }); From 4eede85ef609a1143deaacd664007cc76d035e04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Breitbart?= Date: Sat, 8 Sep 2018 17:26:19 +0200 Subject: [PATCH 03/34] fixes #1667 --- demo/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 6e657692..08545c09 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -101,7 +101,7 @@ function createTerminal(): void { if (e.key === 'Enter') { e.preventDefault(); let searchOptions = { - regex: document.getElementById('regex').checked, + regex: (document.getElementById('regex') as HTMLInputElement).checked, wholeWord: false, caseSensitive: false }; @@ -112,7 +112,7 @@ function createTerminal(): void { if (e.key === 'Enter') { e.preventDefault(); let searchOptions = { - regex: document.getElementById('regex').checked, + regex: (document.getElementById('regex') as HTMLInputElement).checked, wholeWord: false, caseSensitive: false }; From f4bb4a22d4a6cdd1cdfa996c51ed477ef1a29e59 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 8 Sep 2018 13:01:59 -0700 Subject: [PATCH 04/34] Fallback on default color when linkifier can't find the character This probably doesn't fix the underlying problem, but it will make it obvious by using the white background color and more easily help identify the problem case. Fixes #1669 --- src/Linkifier.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Linkifier.ts b/src/Linkifier.ts index 8615daf8..6080c4ae 100644 --- a/src/Linkifier.ts +++ b/src/Linkifier.ts @@ -220,8 +220,11 @@ export class Linkifier extends EventEmitter implements ILinkifier { // Get cell color const line = this._terminal.buffer.lines.get(this._terminal.buffer.ydisp + rowIndex); const char = line.get(index); - const attr: number = char[CHAR_DATA_ATTR_INDEX]; - const fg = (attr >> 9) & 0x1ff; + let fg: number | undefined; + if (char) { + const attr: number = char[CHAR_DATA_ATTR_INDEX]; + fg = (attr >> 9) & 0x1ff; + } // Ensure the link is valid before registering if (matcher.validationCallback) { From c291ab10585f5de5a6ad6e1a8a20d64a067a090b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sat, 8 Sep 2018 14:04:04 -0700 Subject: [PATCH 05/34] Remove failing refresh test --- src/Terminal.test.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/Terminal.test.ts b/src/Terminal.test.ts index 5eb188ce..e8a5d2d8 100644 --- a/src/Terminal.test.ts +++ b/src/Terminal.test.ts @@ -150,19 +150,6 @@ describe('term.js addons', () => { }); }); - describe('refresh', () => { - it('should receive an object: {start: number, end: number}', (done) => { - term.on('refresh', (data) => { - expect(data).to.have.keys(['start', 'end']); - assert.equal(typeof data.start, 'number'); - assert.equal(typeof data.end, 'number'); - done(); - }); - - term.refresh(0, term.rows - 1); - }); - }); - describe('resize', () => { it('should receive an object: {cols: number, rows: number}', (done) => { term.on('resize', (data) => { From 715c078b6db93eb0d6f825e4d08ff815d9920017 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 9 Sep 2018 07:22:18 -0700 Subject: [PATCH 06/34] Fix lint in demo --- demo/client.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 08545c09..4af03548 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -100,7 +100,7 @@ function createTerminal(): void { addDomListener(actionElements.findNext, 'keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); - let searchOptions = { + const searchOptions = { regex: (document.getElementById('regex') as HTMLInputElement).checked, wholeWord: false, caseSensitive: false @@ -111,7 +111,7 @@ function createTerminal(): void { addDomListener(actionElements.findPrevious, 'keypress', (e) => { if (e.key === 'Enter') { e.preventDefault(); - let searchOptions = { + const searchOptions = { regex: (document.getElementById('regex') as HTMLInputElement).checked, wholeWord: false, caseSensitive: false From d943a5c890c1d38c91ff49a4c7962368ee19790e Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Sun, 9 Sep 2018 07:28:22 -0700 Subject: [PATCH 07/34] Run tslint on the demo Fixes #1673 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f2591d03..468c027e 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "scripts": { "start": "node demo/start", "start-zmodem": "node demo/zmodem/app", - "lint": "tslint 'src/**/*.ts'", + "lint": "tslint 'src/**/*.ts' './demo/**/*.ts'", "test": "npm-run-all mocha lint", "test-debug": "node --inspect-brk node_modules/.bin/gulp test", "test-suite": "gulp mocha-suite --test", From 09028d4d86a4feb57935b61c213e1b88de3767e8 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 6 Sep 2018 11:36:36 -0700 Subject: [PATCH 08/34] Added support for searching accross wrapped lines --- src/Buffer.ts | 27 +++++++++ src/addons/search/SearchHelper.ts | 2 +- src/addons/search/search.test.ts | 92 +++++++++++++++++-------------- 3 files changed, 80 insertions(+), 41 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 9487be4a..f872b485 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -265,6 +265,33 @@ export class Buffer implements IBuffer { return lineString.substring(startIndex, endIndex); } + /** + * Translates a buffer line to a string, with optional start and end columns. + * Wide characters will count as two columns in the resulting string. This + * function is useful for getting the actual text underneath the raw selection + * position. + * @param line The line being translated. + * @param trimRight Whether to trim whitespace to the right. + */ + public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string { + // Get full line + let lineString = ''; + let lineWrapsToNext = true; + if (this.lines.get(lineIndex).isWrapped) { + // This terminal line is a continuation of the previous line. + return ''; + } + + do { + lineString += this.translateBufferLineToString(lineIndex, true); + lineIndex++; + const nextLine = this.lines.get(lineIndex); + lineWrapsToNext = nextLine ? this.lines.get(lineIndex).isWrapped : false; + } while (lineWrapsToNext); + + return lineString; + } + public getWrappedRangeForLine(y: number): { first: number, last: number } { let first = y; let last = y; diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 63fd047b..61984543 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -107,7 +107,7 @@ export class SearchHelper implements ISearchHelper { * @return The search result if it was found. */ protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult { - const lowerStringLine = this._terminal._core.buffer.translateBufferLineToString(y, true).toLowerCase(); + const lowerStringLine = this._terminal._core.buffer.translateBufferLineToStringWithWrap(y, true).toLowerCase(); const lowerTerm = term.toLowerCase(); let searchIndex = -1; if (searchOptions.regex) { diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index 8fcde950..261fae5b 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -40,45 +40,57 @@ describe('search addon', function(): void { assert.equal(typeof (MockTerminalPlain).prototype.findPrevious, 'function'); }); }); - it('Searchhelper - should find correct position', function(): void { - search.apply(MockTerminal); - const term = new MockTerminal({cols: 20, rows: 3}); - term.core.write('Hello World\r\ntest\n123....hello'); - term.pushWriteData(); - const hello0 = term.searchHelper.findInLine('Hello', 0); - const hello1 = term.searchHelper.findInLine('Hello', 1); - const hello2 = term.searchHelper.findInLine('Hello', 2); - expect(hello0).eql({col: 0, row: 0, term: 'Hello'}); - expect(hello1).eql(undefined); - expect(hello2).eql({col: 11, row: 2, term: 'Hello'}); - }); - it('should respect search regex', function(): void { - search.apply(MockTerminal); - const term = new MockTerminal({cols: 10, rows: 4}); - term.core.write('abcdefghijklmnopqrstuvwxyz\r\n~/dev '); - /* - abcdefghij - klmnopqrst - uvwxyz - ~/dev - */ - term.pushWriteData(); - const searchOptions = { - regex: true, - wholeWord: false, - caseSensitive: false - }; - const hello0 = term.searchHelper.findInLine('dee*', 0, searchOptions); - term.searchHelper.findInLine('jkk*', 0, searchOptions); - term.searchHelper.findInLine('mnn*', 1, searchOptions); - const tilda0 = term.searchHelper.findInLine('^~', 3, searchOptions); - const tilda1 = term.searchHelper.findInLine('^[~]', 3, searchOptions); - const tilda2 = term.searchHelper.findInLine('^\\~', 3, searchOptions); - expect(hello0).eql({col: 3, row: 0, term: 'de'}); - // TODO: uncomment this test when line wrap search is checked in expect(hello1).eql({col: 9, row: 0, term: 'jk'}); - // TODO: uncomment this test when line wrap search is checked in expect(hello2).eql(undefined); - expect(tilda0).eql({col: 0, row: 3, term: '~'}); - expect(tilda1).eql({col: 0, row: 3, term: '~'}); - expect(tilda2).eql({col: 0, row: 3, term: '~'}); + describe('find', () => { + it('Searchhelper - should find correct position', function(): void { + search.apply(MockTerminal); + const term = new MockTerminal({cols: 20, rows: 3}); + term.core.write('Hello World\r\ntest\n123....hello'); + term.pushWriteData(); + const hello0 = term.searchHelper.findInLine('Hello', 0); + const hello1 = term.searchHelper.findInLine('Hello', 1); + const hello2 = term.searchHelper.findInLine('Hello', 2); + expect(hello0).eql({col: 0, row: 0, term: 'Hello'}); + expect(hello1).eql(undefined); + expect(hello2).eql({col: 11, row: 2, term: 'Hello'}); + }); + it('should find search term accross line wrap', function(): void { + search.apply(MockTerminal); + const term = new MockTerminal({cols: 10, rows: 2}); + term.core.write('texttextHellotext'); + term.pushWriteData(); + const hello0 = (term.searchHelper as any)._findInLine('Hello', 0); + const hello1 = (term.searchHelper as any)._findInLine('Hello', 1); + expect(hello0).eql({col: 8, row: 0, term: 'Hello'}); + expect(hello1).eql(undefined); + }); + it('should respect search regex', function(): void { + search.apply(MockTerminal); + const term = new MockTerminal({cols: 10, rows: 4}); + term.core.write('abcdefghijklmnopqrstuvwxyz\r\n~/dev '); + /* + abcdefghij + klmnopqrst + uvwxyz + ~/dev + */ + term.pushWriteData(); + const searchOptions = { + regex: true, + wholeWord: false, + caseSensitive: false + }; + const hello0 = term.searchHelper.findInLine('dee*', 0, searchOptions); + const hello1 = term.searchHelper.findInLine('jkk*', 0, searchOptions); + const hello2 = term.searchHelper.findInLine('mnn*', 1, searchOptions); + const tilda0 = term.searchHelper.findInLine('^~', 3, searchOptions); + const tilda1 = term.searchHelper.findInLine('^[~]', 3, searchOptions); + const tilda2 = term.searchHelper.findInLine('^\\~', 3, searchOptions); + expect(hello0).eql({col: 3, row: 0, term: 'de'}); + expect(hello1).eql({col: 9, row: 0, term: 'jk'}); + expect(hello2).eql(undefined); + expect(tilda0).eql({col: 0, row: 3, term: '~'}); + expect(tilda1).eql({col: 0, row: 3, term: '~'}); + expect(tilda2).eql({col: 0, row: 3, term: '~'}); + }); }); }); From bc156294aa459f8b1fb07a402e43abfa03852ee7 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 6 Sep 2018 11:40:49 -0700 Subject: [PATCH 09/34] Udated comment --- src/Buffer.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index f872b485..ad18dd0c 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -266,7 +266,7 @@ export class Buffer implements IBuffer { } /** - * Translates a buffer line to a string, with optional start and end columns. + * Translates a buffer line to a string, including subsequent lines if they are wraps. * Wide characters will count as two columns in the resulting string. This * function is useful for getting the actual text underneath the raw selection * position. From 1460a3479620de5c705562da4ff72d80b11ebcd2 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 6 Sep 2018 13:53:03 -0700 Subject: [PATCH 10/34] Added another test case and fixed the multi-line issue that uncovered --- src/Buffer.ts | 7 +------ src/addons/search/SearchHelper.ts | 5 +++-- src/addons/search/search.test.ts | 10 ++++++++-- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index ad18dd0c..0cc2ee4d 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -274,13 +274,8 @@ export class Buffer implements IBuffer { * @param trimRight Whether to trim whitespace to the right. */ public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string { - // Get full line let lineString = ''; - let lineWrapsToNext = true; - if (this.lines.get(lineIndex).isWrapped) { - // This terminal line is a continuation of the previous line. - return ''; - } + let lineWrapsToNext: boolean; do { lineString += this.translateBufferLineToString(lineIndex, true); diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 61984543..89447da1 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -120,8 +120,9 @@ export class SearchHelper implements ISearchHelper { } else { searchIndex = lowerStringLine.indexOf(lowerTerm); } - if (searchIndex >= 0) { - const line = this._terminal._core.buffer.lines.get(y); + + const line = this._terminal._core.buffer.lines.get(y); + if ((searchIndex >= 0) && (searchIndex < line.length)) { for (let i = 0; i < searchIndex; i++) { const charData = line.get(i); // Adjust the searchIndex to normalize emoji into single chars diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index 261fae5b..74f9cbc2 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -55,13 +55,19 @@ describe('search addon', function(): void { }); it('should find search term accross line wrap', function(): void { search.apply(MockTerminal); - const term = new MockTerminal({cols: 10, rows: 2}); - term.core.write('texttextHellotext'); + const term = new MockTerminal({cols: 10, rows: 5}); + term.core.write('texttextHellotext\r\n'); + term.core.write('texttexttextHellotext'); term.pushWriteData(); + const hello0 = (term.searchHelper as any)._findInLine('Hello', 0); const hello1 = (term.searchHelper as any)._findInLine('Hello', 1); + const hello2 = (term.searchHelper as any)._findInLine('Hello', 2); + const hello3 = (term.searchHelper as any)._findInLine('Hello', 3); expect(hello0).eql({col: 8, row: 0, term: 'Hello'}); expect(hello1).eql(undefined); + expect(hello2).eql(undefined); + expect(hello3).eql({col: 2, row: 3, term: 'Hello'}); }); it('should respect search regex', function(): void { search.apply(MockTerminal); From 54ed988fd17bface784d098d96184d8e03491936 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Fri, 7 Sep 2018 10:43:24 -0700 Subject: [PATCH 11/34] Fixed issues around searching lines twice and considering a wrapped line both it's own line and part of the previous line. --- src/Buffer.ts | 22 --------------- src/addons/search/SearchHelper.ts | 45 ++++++++++++++++++++++++++----- src/addons/search/search.test.ts | 13 +++++++-- 3 files changed, 50 insertions(+), 30 deletions(-) diff --git a/src/Buffer.ts b/src/Buffer.ts index 0cc2ee4d..9487be4a 100644 --- a/src/Buffer.ts +++ b/src/Buffer.ts @@ -265,28 +265,6 @@ export class Buffer implements IBuffer { return lineString.substring(startIndex, endIndex); } - /** - * Translates a buffer line to a string, including subsequent lines if they are wraps. - * Wide characters will count as two columns in the resulting string. This - * function is useful for getting the actual text underneath the raw selection - * position. - * @param line The line being translated. - * @param trimRight Whether to trim whitespace to the right. - */ - public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string { - let lineString = ''; - let lineWrapsToNext: boolean; - - do { - lineString += this.translateBufferLineToString(lineIndex, true); - lineIndex++; - const nextLine = this.lines.get(lineIndex); - lineWrapsToNext = nextLine ? this.lines.get(lineIndex).isWrapped : false; - } while (lineWrapsToNext); - - return lineString; - } - public getWrappedRangeForLine(y: number): { first: number, last: number } { let first = y; let last = y; diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 89447da1..0ee5ad12 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -100,15 +100,20 @@ export class SearchHelper implements ISearchHelper { } /** - * Searches a line for a search term. - * @param term The search term. + * Searches a line for a search term. Takes the provided terminal line and searches the text line, which may contain + * subsequent terminal lines if the text is wrapped. If the provided line number is part of a wrapped text line that + * started on an earlier line then it is skipped since it will be properly searched when the terminal line that the + * text starts on is searched. + * @param term Tne search term. * @param y The line to search. * @param searchOptions Search options. * @return The search result if it was found. */ protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult { - const lowerStringLine = this._terminal._core.buffer.translateBufferLineToStringWithWrap(y, true).toLowerCase(); - const lowerTerm = term.toLowerCase(); + if (this._terminal._core.buffer.lines.get(y).isWrapped) { + return; + } + const lowerStringLine = this.translateBufferLineToStringWithWrap(y, true).toLowerCase(); const lowerTerm = term.toLowerCase(); let searchIndex = -1; if (searchOptions.regex) { const searchRegex = RegExp(lowerTerm, 'g'); @@ -121,8 +126,14 @@ export class SearchHelper implements ISearchHelper { searchIndex = lowerStringLine.indexOf(lowerTerm); } - const line = this._terminal._core.buffer.lines.get(y); - if ((searchIndex >= 0) && (searchIndex < line.length)) { + if (searchIndex >= 0) { + // Adjust the row number and search index if needed since a "line" of text can span multiple rows + if (searchIndex >= this._terminal.cols) { + y += Math.floor(searchIndex / this._terminal.cols); + searchIndex = searchIndex % this._terminal.cols; + } + const line = this._terminal._core.buffer.lines.get(y); + for (let i = 0; i < searchIndex; i++) { const charData = line.get(i); // Adjust the searchIndex to normalize emoji into single chars @@ -145,6 +156,28 @@ export class SearchHelper implements ISearchHelper { } } + /** + * Translates a buffer line to a string, including subsequent lines if they are wraps. + * Wide characters will count as two columns in the resulting string. This + * function is useful for getting the actual text underneath the raw selection + * position. + * @param line The line being translated. + * @param trimRight Whether to trim whitespace to the right. + */ + public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string { + let lineString = ''; + let lineWrapsToNext: boolean; + + do { + lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, true); + lineIndex++; + const nextLine = this._terminal._core.buffer.lines.get(lineIndex); + lineWrapsToNext = nextLine ? this._terminal._core.buffer.lines.get(lineIndex).isWrapped : false; + } while (lineWrapsToNext); + + return lineString; + } + /** * Selects and scrolls to a result. * @param result The result to select. diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index 74f9cbc2..edf2a9f8 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -13,10 +13,19 @@ class MockTerminalPlain {} class MockTerminal { private _core: any; +<<<<<<< HEAD public searchHelper: TestSearchHelper; constructor(options: any) { this._core = new (require('../../../lib/Terminal').Terminal)(options); this.searchHelper = new TestSearchHelper(this as any); +======= + public searchHelper: ISearchHelper; + public cols: number; + constructor(options: any) { + this._core = new (require('../../../lib/Terminal').Terminal)(options); + this.searchHelper = new SearchHelper(this as any); + this.cols = options.cols; +>>>>>>> Fixed issues around searching lines twice and considering a wrapped line both it's own line and part of the previous line. } get core(): any { return this._core; @@ -66,8 +75,8 @@ describe('search addon', function(): void { const hello3 = (term.searchHelper as any)._findInLine('Hello', 3); expect(hello0).eql({col: 8, row: 0, term: 'Hello'}); expect(hello1).eql(undefined); - expect(hello2).eql(undefined); - expect(hello3).eql({col: 2, row: 3, term: 'Hello'}); + expect(hello2).eql({col: 2, row: 3, term: 'Hello'}); + expect(hello3).eql(undefined); }); it('should respect search regex', function(): void { search.apply(MockTerminal); From d5c17a0cc9fd7eb191d592689b31dc68b690d30a Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Fri, 7 Sep 2018 10:47:14 -0700 Subject: [PATCH 12/34] Added one more test case --- src/addons/search/search.test.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index edf2a9f8..2e0778e0 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -68,15 +68,24 @@ describe('search addon', function(): void { term.core.write('texttextHellotext\r\n'); term.core.write('texttexttextHellotext'); term.pushWriteData(); + /* + texttextHe + llotext + texttextte + xtHellotex + t + */ const hello0 = (term.searchHelper as any)._findInLine('Hello', 0); const hello1 = (term.searchHelper as any)._findInLine('Hello', 1); const hello2 = (term.searchHelper as any)._findInLine('Hello', 2); const hello3 = (term.searchHelper as any)._findInLine('Hello', 3); + const llo = (term.searchHelper as any)._findInLine('llo', 1); expect(hello0).eql({col: 8, row: 0, term: 'Hello'}); expect(hello1).eql(undefined); expect(hello2).eql({col: 2, row: 3, term: 'Hello'}); expect(hello3).eql(undefined); + expect(llo).eql(undefined); }); it('should respect search regex', function(): void { search.apply(MockTerminal); From bcf223e23599b1151cea93a19abd97d5494e3bfe Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 10 Sep 2018 10:43:03 -0700 Subject: [PATCH 13/34] Fixed merge errors --- src/addons/search/SearchHelper.ts | 3 ++- src/addons/search/search.test.ts | 9 +-------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 0ee5ad12..207e566b 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -113,7 +113,8 @@ export class SearchHelper implements ISearchHelper { if (this._terminal._core.buffer.lines.get(y).isWrapped) { return; } - const lowerStringLine = this.translateBufferLineToStringWithWrap(y, true).toLowerCase(); const lowerTerm = term.toLowerCase(); + const lowerStringLine = this.translateBufferLineToStringWithWrap(y, true).toLowerCase(); + const lowerTerm = term.toLowerCase(); let searchIndex = -1; if (searchOptions.regex) { const searchRegex = RegExp(lowerTerm, 'g'); diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index 2e0778e0..74796930 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -13,19 +13,12 @@ class MockTerminalPlain {} class MockTerminal { private _core: any; -<<<<<<< HEAD public searchHelper: TestSearchHelper; - constructor(options: any) { - this._core = new (require('../../../lib/Terminal').Terminal)(options); - this.searchHelper = new TestSearchHelper(this as any); -======= - public searchHelper: ISearchHelper; public cols: number; constructor(options: any) { this._core = new (require('../../../lib/Terminal').Terminal)(options); - this.searchHelper = new SearchHelper(this as any); + this.searchHelper = new TestSearchHelper(this as any); this.cols = options.cols; ->>>>>>> Fixed issues around searching lines twice and considering a wrapped line both it's own line and part of the previous line. } get core(): any { return this._core; From b2ba2819169982111abf573d08c27d155e9d6541 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 10 Sep 2018 12:09:26 -0700 Subject: [PATCH 14/34] Fix references in demo to addons --- demo/client.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 4af03548..099aab14 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -8,12 +8,12 @@ /// import { Terminal } from '../lib/public/Terminal'; -import * as attach from '../build/addons/attach/attach'; -import * as fit from '../build/addons/fit/fit'; -import * as fullscreen from '../build/addons/fullscreen/fullscreen'; -import * as search from '../build/addons/search/search'; -import * as webLinks from '../build/addons/webLinks/webLinks'; -import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat'; +import * as attach from '../lib/addons/attach/attach'; +import * as fit from '../lib/addons/fit/fit'; +import * as fullscreen from '../lib/addons/fullscreen/fullscreen'; +import * as search from '../lib/addons/search/search'; +import * as webLinks from '../lib/addons/webLinks/webLinks'; +import * as winptyCompat from '../lib/addons/winptyCompat/winptyCompat'; // Pulling in the module's types relies on the above, it's looks a // little weird here as we're importing "this" module From 5e004b21a3ac020768ff97347e7a636af184d10b Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 10 Sep 2018 12:13:48 -0700 Subject: [PATCH 15/34] Disallow the empty string from being selected by find --- src/addons/search/SearchHelper.ts | 2 +- src/addons/search/search.test.ts | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 207e566b..4cc13e63 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -119,7 +119,7 @@ export class SearchHelper implements ISearchHelper { if (searchOptions.regex) { const searchRegex = RegExp(lowerTerm, 'g'); const foundTerm = searchRegex.exec(lowerStringLine); - if (foundTerm) { + if (foundTerm && foundTerm[0].length > 0) { searchIndex = searchRegex.lastIndex - foundTerm[0].length; term = foundTerm[0]; } diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index 74796930..014b01d8 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -34,7 +34,7 @@ class TestSearchHelper extends SearchHelper { } } -describe('search addon', function(): void { +describe('search addon', () => { describe('apply', () => { it('should register findNext and findPrevious', () => { search.apply(MockTerminalPlain); @@ -43,7 +43,7 @@ describe('search addon', function(): void { }); }); describe('find', () => { - it('Searchhelper - should find correct position', function(): void { + it('Searchhelper - should find correct position', () => { search.apply(MockTerminal); const term = new MockTerminal({cols: 20, rows: 3}); term.core.write('Hello World\r\ntest\n123....hello'); @@ -55,7 +55,7 @@ describe('search addon', function(): void { expect(hello1).eql(undefined); expect(hello2).eql({col: 11, row: 2, term: 'Hello'}); }); - it('should find search term accross line wrap', function(): void { + it('should find search term accross line wrap', () => { search.apply(MockTerminal); const term = new MockTerminal({cols: 10, rows: 5}); term.core.write('texttextHellotext\r\n'); @@ -80,7 +80,7 @@ describe('search addon', function(): void { expect(hello3).eql(undefined); expect(llo).eql(undefined); }); - it('should respect search regex', function(): void { + it('should respect search regex', () => { search.apply(MockTerminal); const term = new MockTerminal({cols: 10, rows: 4}); term.core.write('abcdefghijklmnopqrstuvwxyz\r\n~/dev '); @@ -109,5 +109,13 @@ describe('search addon', function(): void { expect(tilda1).eql({col: 0, row: 3, term: '~'}); expect(tilda2).eql({col: 0, row: 3, term: '~'}); }); + it('should not select empty lines', () => { + search.apply(MockTerminal); + const term = new MockTerminal({cols: 20, rows: 3}); + term.core.write(' '); + term.pushWriteData(); + const line = term.searchHelper.findInLine('^.*$', 0, { regex: true }); + expect(line).eql(undefined); + }); }); }); From fed579238a70bd5eaad097454dc796b92a388432 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Thu, 6 Sep 2018 14:24:32 -0700 Subject: [PATCH 16/34] Added optional case sensitive searching --- demo/client.ts | 4 ++-- demo/index.html | 1 + src/addons/search/SearchHelper.ts | 15 +++++++++------ src/addons/search/search.test.ts | 12 ++++++++++++ 4 files changed, 24 insertions(+), 8 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index 099aab14..5c477ae7 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -103,7 +103,7 @@ function createTerminal(): void { const searchOptions = { regex: (document.getElementById('regex') as HTMLInputElement).checked, wholeWord: false, - caseSensitive: false + caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked }; term.findNext(actionElements.findNext.value, searchOptions); } @@ -114,7 +114,7 @@ function createTerminal(): void { const searchOptions = { regex: (document.getElementById('regex') as HTMLInputElement).checked, wholeWord: false, - caseSensitive: false + caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked }; term.findPrevious(actionElements.findPrevious.value, searchOptions); } diff --git a/demo/index.html b/demo/index.html index ae29d135..553b9a8b 100644 --- a/demo/index.html +++ b/demo/index.html @@ -17,6 +17,7 @@ +

diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 4cc13e63..90df0567 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -113,18 +113,21 @@ export class SearchHelper implements ISearchHelper { if (this._terminal._core.buffer.lines.get(y).isWrapped) { return; } - const lowerStringLine = this.translateBufferLineToStringWithWrap(y, true).toLowerCase(); - const lowerTerm = term.toLowerCase(); - let searchIndex = -1; + + const stringLIne = this.translateBufferLineToStringWithWrap(y, true); + const searchStringLine = searchOptions.caseSensitive ? stringLIne : stringLIne.toLowerCase(); + const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase(); + let searchIndex = searchStringLine.indexOf(searchTerm); + if (searchOptions.regex) { - const searchRegex = RegExp(lowerTerm, 'g'); - const foundTerm = searchRegex.exec(lowerStringLine); + const searchRegex = RegExp(searchTerm, 'g'); + const foundTerm = searchRegex.exec(searchStringLine); if (foundTerm && foundTerm[0].length > 0) { searchIndex = searchRegex.lastIndex - foundTerm[0].length; term = foundTerm[0]; } } else { - searchIndex = lowerStringLine.indexOf(lowerTerm); + searchIndex = searchStringLine.indexOf(searchTerm); } if (searchIndex >= 0) { diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index 014b01d8..8c3f1373 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -117,5 +117,17 @@ describe('search addon', () => { const line = term.searchHelper.findInLine('^.*$', 0, { regex: true }); expect(line).eql(undefined); }); + it('should respect case sensitive', function(): void { + search.apply(MockTerminal); + const term = new MockTerminal({cols: 20, rows: 4}); + term.core.write('Hello World\r\n123....hello\r\nmoreTestHello'); + term.pushWriteData(); + const hello0 = (term.searchHelper as any)._findInLine('Hello', 0, true); + const hello1 = (term.searchHelper as any)._findInLine('Hello', 1, true); + const hello2 = (term.searchHelper as any)._findInLine('Hello', 2, true); + expect(hello0).eql({col: 0, row: 0, term: 'Hello'}); + expect(hello1).eql(undefined); + expect(hello2).eql({col: 8, row: 2, term: 'Hello'}); + }); }); }); From 16de86f5ea839ebaf346650701a550fc78e620cb Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 10 Sep 2018 13:34:28 -0700 Subject: [PATCH 17/34] Fixed an unintentionally capitalized i --- src/addons/search/SearchHelper.ts | 4 ++-- src/addons/search/search.test.ts | 11 ++++++++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 90df0567..6d82b840 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -114,8 +114,8 @@ export class SearchHelper implements ISearchHelper { return; } - const stringLIne = this.translateBufferLineToStringWithWrap(y, true); - const searchStringLine = searchOptions.caseSensitive ? stringLIne : stringLIne.toLowerCase(); + const stringLine = this.translateBufferLineToStringWithWrap(y, true); + const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase(); const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase(); let searchIndex = searchStringLine.indexOf(searchTerm); diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index 8c3f1373..56dd0d67 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -122,9 +122,14 @@ describe('search addon', () => { const term = new MockTerminal({cols: 20, rows: 4}); term.core.write('Hello World\r\n123....hello\r\nmoreTestHello'); term.pushWriteData(); - const hello0 = (term.searchHelper as any)._findInLine('Hello', 0, true); - const hello1 = (term.searchHelper as any)._findInLine('Hello', 1, true); - const hello2 = (term.searchHelper as any)._findInLine('Hello', 2, true); + const searchOptions = { + regex: false, + wholeWord: false, + caseSensitive: true + }; + const hello0 = (term.searchHelper as any)._findInLine('Hello', 0, searchOptions); + const hello1 = (term.searchHelper as any)._findInLine('Hello', 1, searchOptions); + const hello2 = (term.searchHelper as any)._findInLine('Hello', 2, searchOptions); expect(hello0).eql({col: 0, row: 0, term: 'Hello'}); expect(hello1).eql(undefined); expect(hello2).eql({col: 8, row: 2, term: 'Hello'}); From ade3e0943c9b48588c76183bd69f4ac94a915adf Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Mon, 10 Sep 2018 13:54:29 -0700 Subject: [PATCH 18/34] Fixed a merge mistake --- src/addons/search/SearchHelper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 6d82b840..2db4953f 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -117,7 +117,7 @@ export class SearchHelper implements ISearchHelper { const stringLine = this.translateBufferLineToStringWithWrap(y, true); const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase(); const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase(); - let searchIndex = searchStringLine.indexOf(searchTerm); + let searchIndex = -1; if (searchOptions.regex) { const searchRegex = RegExp(searchTerm, 'g'); From c88bc62ff900a3c2d67c985381520e0b70528140 Mon Sep 17 00:00:00 2001 From: "azure-pipelines[bot]" Date: Tue, 11 Sep 2018 05:53:14 +0000 Subject: [PATCH 19/34] Set up CI with Azure Pipelines --- azure-pipelines.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 azure-pipelines.yml diff --git a/azure-pipelines.yml b/azure-pipelines.yml new file mode 100644 index 00000000..722a3631 --- /dev/null +++ b/azure-pipelines.yml @@ -0,0 +1,18 @@ +# Node.js +# Build a general Node.js application with npm. +# Add steps that analyze code, save build artifacts, deploy, and more: +# https://docs.microsoft.com/vsts/pipelines/languages/javascript + +pool: + vmImage: 'Ubuntu 16.04' + +steps: +- task: NodeTool@0 + inputs: + versionSpec: '8.x' + displayName: 'Install Node.js' + +- script: | + npm install + npm run build + displayName: 'npm install and build' From 3a97dbecde99ad7c5c784ca88c6830b97fc14464 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 10 Sep 2018 23:01:18 -0700 Subject: [PATCH 20/34] Add more platforms, build out script --- azure-pipelines.yml | 75 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 722a3631..f9d41638 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -3,16 +3,69 @@ # Add steps that analyze code, save build artifacts, deploy, and more: # https://docs.microsoft.com/vsts/pipelines/languages/javascript -pool: - vmImage: 'Ubuntu 16.04' +jobs: +- job: Linux + pool: + vmImage: 'ubuntu-16.04' + steps: + - task: NodeTool@0 + inputs: + versionSpec: '8.x' + displayName: 'Install Node.js' + - script: | + yarn install + displayName: 'Install yarn dependencies' + - script: | + yarn tsc + displayName: 'Build' + - script: | + yarn mocha + displayName: 'Test' + - script: | + yarn lint + displayName: 'Lint' + - script: | + yarn test-coverage + displayName: 'Generate and publish coverage' -steps: -- task: NodeTool@0 - inputs: - versionSpec: '8.x' - displayName: 'Install Node.js' +- job: macOS + pool: + vmImage: 'xcode9-macos10.13' + steps: + - task: NodeTool@0 + inputs: + versionSpec: '8.x' + displayName: 'Install Node.js' + - script: | + yarn install + displayName: 'Install yarn dependencies' + - script: | + yarn tsc + displayName: 'Build' + - script: | + yarn mocha + displayName: 'Test' + - script: | + yarn lint + displayName: 'Lint' -- script: | - npm install - npm run build - displayName: 'npm install and build' +- job: Windows + pool: + vmImage: 'vs2017-win2016' + steps: + - task: NodeTool@0 + inputs: + versionSpec: '8.x' + displayName: 'Install Node.js' + - script: | + yarn install + displayName: 'Install yarn dependencies' + - script: | + yarn tsc + displayName: 'Build' + - script: | + yarn mocha + displayName: 'Test' + - script: | + yarn lint + displayName: 'Lint' From 0446cf3aee71fd2f20010d7f1e4d399e9d9c5286 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 10 Sep 2018 23:04:33 -0700 Subject: [PATCH 21/34] Install yarn manually on Linux --- azure-pipelines.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index f9d41638..c4bdbc69 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -12,9 +12,15 @@ jobs: inputs: versionSpec: '8.x' displayName: 'Install Node.js' + - script: | + curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - + echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list + sudo apt-get update && sudo apt-get install yarn + sudo apt-get install --no-install-recommends yarn + displayName: 'Install yarn' - script: | yarn install - displayName: 'Install yarn dependencies' + displayName: 'Install npm dependencies' - script: | yarn tsc displayName: 'Build' From 5909885d43512f362c5819481619caf7206e90cf Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 10 Sep 2018 23:10:42 -0700 Subject: [PATCH 22/34] Download and install yarn directly from deb --- azure-pipelines.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c4bdbc69..8c16d59c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,10 +13,8 @@ jobs: versionSpec: '8.x' displayName: 'Install Node.js' - script: | - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list - sudo apt-get update && sudo apt-get install yarn - sudo apt-get install --no-install-recommends yarn + curl -sLO https://github.com/yarnpkg/yarn/releases/download/v1.10.0/yarn_1.10.0_all.deb + sudo dpkg -i yarn_1.10.0_all.deb displayName: 'Install yarn' - script: | yarn install From dade9ba0501458eed46b17fec870dfbb61a7c300 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 10 Sep 2018 23:17:03 -0700 Subject: [PATCH 23/34] Simplify steps --- azure-pipelines.yml | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 8c16d59c..b0bc32f7 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -17,11 +17,8 @@ jobs: sudo dpkg -i yarn_1.10.0_all.deb displayName: 'Install yarn' - script: | - yarn install - displayName: 'Install npm dependencies' - - script: | - yarn tsc - displayName: 'Build' + yarn + displayName: 'Install dependencies and build' - script: | yarn mocha displayName: 'Test' @@ -41,11 +38,8 @@ jobs: versionSpec: '8.x' displayName: 'Install Node.js' - script: | - yarn install - displayName: 'Install yarn dependencies' - - script: | - yarn tsc - displayName: 'Build' + yarn + displayName: 'Install dependencies and build' - script: | yarn mocha displayName: 'Test' @@ -62,11 +56,8 @@ jobs: versionSpec: '8.x' displayName: 'Install Node.js' - script: | - yarn install - displayName: 'Install yarn dependencies' - - script: | - yarn tsc - displayName: 'Build' + yarn + displayName: 'Install dependencies and build' - script: | yarn mocha displayName: 'Test' From 5d37a173830c165aa7fb2de508e7b981a6521059 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 10 Sep 2018 23:49:52 -0700 Subject: [PATCH 24/34] Install latest yarn with install script Avoids dpkg lock flakes --- azure-pipelines.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b0bc32f7..a9ce66f5 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -13,8 +13,7 @@ jobs: versionSpec: '8.x' displayName: 'Install Node.js' - script: | - curl -sLO https://github.com/yarnpkg/yarn/releases/download/v1.10.0/yarn_1.10.0_all.deb - sudo dpkg -i yarn_1.10.0_all.deb + curl -o- -L https://yarnpkg.com/install.sh | bash displayName: 'Install yarn' - script: | yarn From 6ebf17bf04338cba7bff5ab50a1597c6a28ff221 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Mon, 10 Sep 2018 23:53:45 -0700 Subject: [PATCH 25/34] Export yarn path --- azure-pipelines.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index a9ce66f5..54a9b08c 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -14,6 +14,7 @@ jobs: displayName: 'Install Node.js' - script: | curl -o- -L https://yarnpkg.com/install.sh | bash + export PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH" displayName: 'Install yarn' - script: | yarn From 35483b3bf418ccb280eaeaeb27767d326263d8cb Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 11 Sep 2018 00:00:33 -0700 Subject: [PATCH 26/34] Source bashrc after yarn install --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 54a9b08c..dba7bf05 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -14,7 +14,7 @@ jobs: displayName: 'Install Node.js' - script: | curl -o- -L https://yarnpkg.com/install.sh | bash - export PATH="$HOME/.yarn/bin:$HOME/.config/yarn/global/node_modules/.bin:$PATH" + . ~/.bashrc displayName: 'Install yarn' - script: | yarn From 07b5bd0b64a7402259c528bd124ef0bd5c4d9169 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 11 Sep 2018 00:04:27 -0700 Subject: [PATCH 27/34] Echo path --- azure-pipelines.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index dba7bf05..7d444136 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -14,7 +14,8 @@ jobs: displayName: 'Install Node.js' - script: | curl -o- -L https://yarnpkg.com/install.sh | bash - . ~/.bashrc + echo 'PATH' + echo $PATH displayName: 'Install yarn' - script: | yarn From d23c65d1db0c60c5bb9804828bbf734837d10819 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 11 Sep 2018 00:09:36 -0700 Subject: [PATCH 28/34] Symlink yarn to /usr/bin --- azure-pipelines.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7d444136..7b68fcb2 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -14,8 +14,7 @@ jobs: displayName: 'Install Node.js' - script: | curl -o- -L https://yarnpkg.com/install.sh | bash - echo 'PATH' - echo $PATH + ln -s $HOME/.yarn/bin/yarn /usr/bin/yarn displayName: 'Install yarn' - script: | yarn From e03c93fb818008e01a7507e43c15277525b156fc Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 11 Sep 2018 00:19:42 -0700 Subject: [PATCH 29/34] Install yarn using installer task --- azure-pipelines.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 7b68fcb2..4ab162c8 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -12,10 +12,10 @@ jobs: inputs: versionSpec: '8.x' displayName: 'Install Node.js' - - script: | - curl -o- -L https://yarnpkg.com/install.sh | bash - ln -s $HOME/.yarn/bin/yarn /usr/bin/yarn - displayName: 'Install yarn' + - task: geeklearningio.gl-vsts-tasks-yarn.yarn-installer-task.YarnInstaller@2 + inputs: + versionSpec: "1.9.4" + displayName: 'Install Yarn' - script: | yarn displayName: 'Install dependencies and build' From 1edc732c555bb940482a486e73c7f6f9bde1447d Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 11 Sep 2018 11:34:47 -0700 Subject: [PATCH 30/34] Use new Azure Pipelines badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 583faf7f..d4e9c546 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # [![xterm.js logo](logo-full.png)](https://xtermjs.org) -[![VSTS Build status](https://xtermjs.visualstudio.com/_apis/public/build/definitions/3e323cf7-5760-460d-af64-ee5675baf366/1/badge)](https://xtermjs.visualstudio.com/xterm.js/_build/index?definitionId=1) +[![Build Status](https://xtermjs.visualstudio.com/xterm.js/_apis/build/status/xtermjs.xterm.js)](https://xtermjs.visualstudio.com/xterm.js/_build/latest?definitionId=3) [![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm) From 7d2bdacf77ba0258bd0e8f5cc18c4855a93a4a89 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 11 Sep 2018 15:48:24 -0700 Subject: [PATCH 31/34] Added test case for when regex and case sensitive are both selected --- src/addons/search/search.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index 56dd0d67..a96f7d01 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -134,5 +134,30 @@ describe('search addon', () => { expect(hello1).eql(undefined); expect(hello2).eql({col: 8, row: 2, term: 'Hello'}); }); + it('should respect case sensitive + regex', function(): void { + search.apply(MockTerminal); + const term = new MockTerminal({cols: 20, rows: 4}); + term.core.write('hellohello\r\nHelloHello'); + term.pushWriteData(); + + /** + * hellohello + * HelloHello + */ + + const searchOptions = { + regex: true, + wholeWord: false, + caseSensitive: true + }; + const hello0 = (term.searchHelper as any)._findInLine('Hello', 0, searchOptions); + const hello1 = (term.searchHelper as any)._findInLine('Hello$', 0, searchOptions); + const hello2 = (term.searchHelper as any)._findInLine('Hello', 1, searchOptions); + const hello3 = (term.searchHelper as any)._findInLine('Hello$', 1, searchOptions); + expect(hello0).eql(undefined); + expect(hello1).eql(undefined); + expect(hello2).eql({col: 0, row: 1, term: 'Hello'}); + expect(hello3).eql({col: 5, row: 1, term: 'Hello'}); + }); }); }); From 8979eea810400cdfc32ad08265a0ae31b554aed9 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 11 Sep 2018 16:12:53 -0700 Subject: [PATCH 32/34] Fixed translateBufferLineToStringWithWrap to correctly not remove spaces in the middle of a multiline wrapped string --- src/addons/search/SearchHelper.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 4cc13e63..051fa047 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -170,10 +170,10 @@ export class SearchHelper implements ISearchHelper { let lineWrapsToNext: boolean; do { - lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, true); - lineIndex++; const nextLine = this._terminal._core.buffer.lines.get(lineIndex); - lineWrapsToNext = nextLine ? this._terminal._core.buffer.lines.get(lineIndex).isWrapped : false; + lineWrapsToNext = nextLine ? nextLine.isWrapped : false; + lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight); + lineIndex++; } while (lineWrapsToNext); return lineString; From 5aa665697151aba3c99d5ee857046bcc7ff6f400 Mon Sep 17 00:00:00 2001 From: Alex Ross Date: Tue, 11 Sep 2018 16:27:22 -0700 Subject: [PATCH 33/34] Added test and fixed uncovered buy --- src/addons/search/SearchHelper.ts | 2 +- src/addons/search/search.test.ts | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 051fa047..2bf96471 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -170,7 +170,7 @@ export class SearchHelper implements ISearchHelper { let lineWrapsToNext: boolean; do { - const nextLine = this._terminal._core.buffer.lines.get(lineIndex); + const nextLine = this._terminal._core.buffer.lines.get(lineIndex + 1); lineWrapsToNext = nextLine ? nextLine.isWrapped : false; lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, !lineWrapsToNext && trimRight); lineIndex++; diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index 014b01d8..13138f07 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -59,14 +59,15 @@ describe('search addon', () => { search.apply(MockTerminal); const term = new MockTerminal({cols: 10, rows: 5}); term.core.write('texttextHellotext\r\n'); - term.core.write('texttexttextHellotext'); + term.core.write('texttexttextHellotext goodbye'); term.pushWriteData(); /* texttextHe llotext texttextte xtHellotex - t + t (these spaces included intentionally) + goodbye */ const hello0 = (term.searchHelper as any)._findInLine('Hello', 0); @@ -74,11 +75,13 @@ describe('search addon', () => { const hello2 = (term.searchHelper as any)._findInLine('Hello', 2); const hello3 = (term.searchHelper as any)._findInLine('Hello', 3); const llo = (term.searchHelper as any)._findInLine('llo', 1); + const goodbye = (term.searchHelper as any)._findInLine('goodbye', 2); expect(hello0).eql({col: 8, row: 0, term: 'Hello'}); expect(hello1).eql(undefined); expect(hello2).eql({col: 2, row: 3, term: 'Hello'}); expect(hello3).eql(undefined); expect(llo).eql(undefined); + expect(goodbye).eql({col: 0, row: 5, term: 'goodbye'}); }); it('should respect search regex', () => { search.apply(MockTerminal); From 68766d540f4e8e5ce7aa5b67b33290c93fb256f2 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 13 Sep 2018 08:19:59 -0700 Subject: [PATCH 34/34] Use new domain for azure pipelines --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d4e9c546..f2f16c98 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # [![xterm.js logo](logo-full.png)](https://xtermjs.org) -[![Build Status](https://xtermjs.visualstudio.com/xterm.js/_apis/build/status/xtermjs.xterm.js)](https://xtermjs.visualstudio.com/xterm.js/_build/latest?definitionId=3) +[![Build Status](https://dev.azure.com/xtermjs/xterm.js/_apis/build/status/xtermjs.xterm.js)](https://dev.azure.com/xtermjs/xterm.js/_build/latest?definitionId=3) [![Coverage Status](https://coveralls.io/repos/github/xtermjs/xterm.js/badge.svg?branch=master)](https://coveralls.io/github/xtermjs/xterm.js?branch=master) [![Gitter](https://badges.gitter.im/sourcelair/xterm.js.svg)](https://gitter.im/sourcelair/xterm.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) [![jsDelivr Hits](https://data.jsdelivr.com/v1/package/npm/xterm/badge?style=rounded)](https://www.jsdelivr.com/package/npm/xterm)