From 376c790e1d3feb5facc6817c383b296378d4f3cd Mon Sep 17 00:00:00 2001 From: Noam Date: Fri, 30 Nov 2018 20:39:05 +0200 Subject: [PATCH 01/20] implement find multiple matches in line. start search from current selection. add unit tests. --- src/addons/search/Interfaces.ts | 7 ++- src/addons/search/SearchHelper.ts | 86 +++++++++++++++++++------------ src/addons/search/search.test.ts | 70 +++++++++++++++++++++++-- 3 files changed, 126 insertions(+), 37 deletions(-) diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts index af06c5d1..e03b70c4 100644 --- a/src/addons/search/Interfaces.ts +++ b/src/addons/search/Interfaces.ts @@ -25,10 +25,13 @@ export interface ISearchOptions { regex?: boolean; wholeWord?: boolean; caseSensitive?: boolean; + reverseSearch?: boolean; } -export interface ISearchResult { - term: string; +export interface ISearchIndex { col: number; row: number; } +export interface ISearchResult extends ISearchIndex { + term: string; +} diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 7919932a..dd055991 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces'; +import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult, ISearchIndex } from './Interfaces'; const nonWordCharacters = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; /** @@ -29,27 +29,30 @@ export class SearchHelper implements ISearchHelper { } let result: ISearchResult; - let startRow = this._terminal._core.buffer.ydisp; + let startCol: number = 0; if (this._terminal._core.selectionManager.selectionEnd) { // Start from the selection end if there is a selection if (this._terminal.getSelection().length !== 0) { startRow = this._terminal._core.selectionManager.selectionEnd[1]; + startCol = this._terminal._core.selectionManager.selectionEnd[0]; } } // Search from ydisp + 1 to end - for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { - result = this._findInLine(term, y, searchOptions); + for (let y = startRow; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { + result = this._findInLine(term, {row: y, col: startCol}, searchOptions); if (result) { break; } + startCol = 0; } // Search from the top to the current ydisp if (!result) { for (let y = 0; y < startRow; y++) { - result = this._findInLine(term, y, searchOptions); + startCol = 0; + result = this._findInLine(term, {row: y, col: startCol}, searchOptions); if (result) { break; } @@ -72,28 +75,35 @@ export class SearchHelper implements ISearchHelper { return false; } - let result: ISearchResult; + searchOptions.reverseSearch = true; + let result: ISearchResult; let startRow = this._terminal._core.buffer.ydisp; + let startCol: number = this._terminal._core.buffer.lines.get(startRow).length; + if (this._terminal._core.selectionManager.selectionStart) { // Start from the selection end if there is a selection if (this._terminal.getSelection().length !== 0) { startRow = this._terminal._core.selectionManager.selectionStart[1]; + startCol = this._terminal._core.selectionManager.selectionStart[0]; } } // Search from ydisp + 1 to end - for (let y = startRow - 1; y >= 0; y--) { - result = this._findInLine(term, y, searchOptions); + for (let y = startRow; y >= 0; y--) { + result = this._findInLine(term, {row: y, col: startCol}, searchOptions); if (result) { break; } + startCol = y > 0 ? this._terminal._core.buffer.lines.get(y - 1).length : 0; } // Search from the top to the current ydisp if (!result) { - for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) { - result = this._findInLine(term, y, searchOptions); + const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1; + for (let y = searchFrom; y > startRow; y--) { + startCol = this._terminal._core.buffer.lines.get(y).length; + result = this._findInLine(term, {row: y, col: startCol}, searchOptions); if (result) { break; } @@ -125,61 +135,73 @@ export class SearchHelper implements ISearchHelper { * @param searchOptions Search options. * @return The search result if it was found. */ - protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult { - if (this._terminal._core.buffer.lines.get(y).isWrapped) { + protected _findInLine(term: string, searchIndex: ISearchIndex, searchOptions: ISearchOptions = {}): ISearchResult { + if (this._terminal._core.buffer.lines.get(searchIndex.row).isWrapped) { return; } - const stringLine = this.translateBufferLineToStringWithWrap(y, true); - const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase(); + const stringLine = this.translateBufferLineToStringWithWrap(searchIndex.row, true); const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase(); - let searchIndex = -1; + const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase(); + let resultIndex = -1; if (searchOptions.regex) { 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]; + let foundTerm: RegExpExecArray; + if (searchOptions.reverseSearch) { + while (foundTerm = searchRegex.exec(searchStringLine.slice(0, searchIndex.col))) { + resultIndex = searchRegex.lastIndex - foundTerm[0].length; + term = foundTerm[0]; + searchRegex.lastIndex -= (term.length - 1); + } + } else { + foundTerm = searchRegex.exec(searchStringLine.slice(searchIndex.col)); + if (foundTerm && foundTerm[0].length > 0) { + resultIndex = searchIndex.col + (searchRegex.lastIndex - foundTerm[0].length); + term = foundTerm[0]; + } } } else { - searchIndex = searchStringLine.indexOf(searchTerm); + if (searchOptions.reverseSearch) { + resultIndex = searchStringLine.lastIndexOf(searchTerm, searchIndex.col - searchTerm.length); + } else { + resultIndex = searchStringLine.indexOf(searchTerm, searchIndex.col); + } } - if (searchIndex >= 0) { + if (resultIndex >= 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; + if (resultIndex >= this._terminal.cols) { + searchIndex.row += Math.floor(resultIndex / this._terminal.cols); + resultIndex = resultIndex % this._terminal.cols; } - if (searchOptions.wholeWord && !this._isWholeWord(searchIndex, searchStringLine, term)) { + if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) { return; } - const line = this._terminal._core.buffer.lines.get(y); + const line = this._terminal._core.buffer.lines.get(searchIndex.row); - for (let i = 0; i < searchIndex; i++) { + for (let i = 0; i < resultIndex; i++) { const charData = line.get(i); // Adjust the searchIndex to normalize emoji into single chars const char = charData[1/*CHAR_DATA_CHAR_INDEX*/]; if (char.length > 1) { - searchIndex -= char.length - 1; + resultIndex -= char.length - 1; } // Adjust the searchIndex for empty characters following wide unicode // chars (eg. CJK) const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/]; if (charWidth === 0) { - searchIndex++; + resultIndex++; } } return { term, - col: searchIndex, - row: y + col: resultIndex, + row: searchIndex.row }; } } - /** * 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 diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index 3e0b8154..be9ec479 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -7,7 +7,7 @@ declare var require: any; import { assert, expect } from 'chai'; import * as search from './search'; import { SearchHelper } from './SearchHelper'; -import { ISearchOptions, ISearchResult } from './Interfaces'; +import { ISearchOptions, ISearchResult, ISearchIndex } from './Interfaces'; class MockTerminalPlain {} @@ -29,8 +29,11 @@ class MockTerminal { } class TestSearchHelper extends SearchHelper { - public findInLine(term: string, y: number, searchOptions?: ISearchOptions): ISearchResult { - return this._findInLine(term, y, searchOptions); + public findInLine(term: string, rowNumber: number, searchOptions?: ISearchOptions): ISearchResult { + return this._findInLine(term, {row: rowNumber, col: 0}, searchOptions); + } + public findFromIndex(term: string, searchIndex: ISearchIndex, searchOptions?: ISearchOptions): ISearchResult { + return this._findInLine(term, searchIndex, searchOptions); } } @@ -247,5 +250,66 @@ describe('search addon', () => { expect(hello4).eql(undefined); expect(hello5).eql(undefined); }); + it('should find multiple matches in line', function(): void { + search.apply(MockTerminal); + const term = new MockTerminal({cols: 20, rows: 5}); + term.core.write('helloooo helloooo\r\naaaAAaaAAA'); + term.pushWriteData(); + const searchOptions = { + regex: false, + wholeWord: false, + caseSensitive: false + }; + const find0 = term.searchHelper.findFromIndex('hello', {row: 0, col: 0}, searchOptions); + const find1 = term.searchHelper.findFromIndex('hello', {row: 0, col: find0.col + find0.term.length}, searchOptions); + const find2 = term.searchHelper.findFromIndex('aaaa', {row: 1, col: 0}, searchOptions); + const find3 = term.searchHelper.findFromIndex('aaaa', {row: 1, col: find2.col + find2.term.length}, searchOptions); + const find4 = term.searchHelper.findFromIndex('aaaa', {row: 1, col: find3.col + find3.term.length}, searchOptions); + expect(find0).eql({col: 0, row: 0, term: 'hello'}); + expect(find1).eql({col: 9, row: 0, term: 'hello'}); + expect(find2).eql({col: 0, row: 1, term: 'aaaa'}); + expect(find3).eql({col: 4, row: 1, term: 'aaaa'}); + expect(find4).eql(undefined); + }); + it('should find multiple matches in line - reverse search', function(): void { + search.apply(MockTerminal); + const term = new MockTerminal({cols: 20, rows: 5}); + term.core.write('it is what it is'); + term.pushWriteData(); + const searchOptions = { + regex: false, + wholeWord: false, + caseSensitive: false, + reverseSearch: true + }; + const find0 = term.searchHelper.findFromIndex('is', {row: 0, col: 16}, searchOptions); + const find1 = term.searchHelper.findFromIndex('is', {row: 0, col: find0.col}, searchOptions); + const find2 = term.searchHelper.findFromIndex('it', {row: 0, col: 16}, searchOptions); + const find3 = term.searchHelper.findFromIndex('it', {row: 0, col: find2.col}, searchOptions); + expect(find0).eql({col: 14, row: 0, term: 'is'}); + expect(find1).eql({col: 3, row: 0, term: 'is'}); + expect(find2).eql({col: 11, row: 0, term: 'it'}); + expect(find3).eql({col: 0, row: 0, term: 'it'}); + }); + it('should find multiple matches in line - reverse search with regex', function(): void { + search.apply(MockTerminal); + const term = new MockTerminal({cols: 20, rows: 5}); + term.core.write('zzzABCzzzzABCABC'); + term.pushWriteData(); + const searchOptions = { + regex: true, + wholeWord: false, + caseSensitive: true, + reverseSearch: true + }; + const find0 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: 16}, searchOptions); + const find1 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find0.col}, searchOptions); + const find2 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find1.col}, searchOptions); + const find3 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find2.col}, searchOptions); + expect(find0).eql({col: 13, row: 0, term: 'ABC'}); + expect(find1).eql({col: 10, row: 0, term: 'ABC'}); + expect(find2).eql({col: 3, row: 0, term: 'ABC'}); + expect(find3).eql(undefined); + }); }); }); From 278d696709b799c20481107ebf2a6ff53ae0764b Mon Sep 17 00:00:00 2001 From: Noam Date: Fri, 7 Dec 2018 22:56:57 +0200 Subject: [PATCH 02/20] remove reverseSearch from ISearchOptions add isReverseSearch argument to findInLine modify unit tests --- src/addons/search/Interfaces.ts | 2 +- src/addons/search/SearchHelper.ts | 15 ++++++--------- src/addons/search/search.test.ts | 28 ++++++++++++++-------------- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts index e03b70c4..e15224d1 100644 --- a/src/addons/search/Interfaces.ts +++ b/src/addons/search/Interfaces.ts @@ -25,13 +25,13 @@ export interface ISearchOptions { regex?: boolean; wholeWord?: boolean; caseSensitive?: boolean; - reverseSearch?: boolean; } export interface ISearchIndex { col: number; row: number; } + export interface ISearchResult extends ISearchIndex { term: string; } diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index dd055991..5052a07d 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -5,7 +5,6 @@ import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult, ISearchIndex } from './Interfaces'; const nonWordCharacters = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; - /** * A class that knows how to search the terminal and how to display the results. */ @@ -74,9 +73,7 @@ export class SearchHelper implements ISearchHelper { if (!term || term.length === 0) { return false; } - - searchOptions.reverseSearch = true; - + const isReverseSearch = true; let result: ISearchResult; let startRow = this._terminal._core.buffer.ydisp; let startCol: number = this._terminal._core.buffer.lines.get(startRow).length; @@ -91,7 +88,7 @@ export class SearchHelper implements ISearchHelper { // Search from ydisp + 1 to end for (let y = startRow; y >= 0; y--) { - result = this._findInLine(term, {row: y, col: startCol}, searchOptions); + result = this._findInLine(term, {row: y, col: startCol}, searchOptions, isReverseSearch); if (result) { break; } @@ -103,7 +100,7 @@ export class SearchHelper implements ISearchHelper { const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1; for (let y = searchFrom; y > startRow; y--) { startCol = this._terminal._core.buffer.lines.get(y).length; - result = this._findInLine(term, {row: y, col: startCol}, searchOptions); + result = this._findInLine(term, {row: y, col: startCol}, searchOptions, isReverseSearch); if (result) { break; } @@ -135,7 +132,7 @@ export class SearchHelper implements ISearchHelper { * @param searchOptions Search options. * @return The search result if it was found. */ - protected _findInLine(term: string, searchIndex: ISearchIndex, searchOptions: ISearchOptions = {}): ISearchResult { + protected _findInLine(term: string, searchIndex: ISearchIndex, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult { if (this._terminal._core.buffer.lines.get(searchIndex.row).isWrapped) { return; } @@ -148,7 +145,7 @@ export class SearchHelper implements ISearchHelper { if (searchOptions.regex) { const searchRegex = RegExp(searchTerm, 'g'); let foundTerm: RegExpExecArray; - if (searchOptions.reverseSearch) { + if (isReverseSearch) { while (foundTerm = searchRegex.exec(searchStringLine.slice(0, searchIndex.col))) { resultIndex = searchRegex.lastIndex - foundTerm[0].length; term = foundTerm[0]; @@ -162,7 +159,7 @@ export class SearchHelper implements ISearchHelper { } } } else { - if (searchOptions.reverseSearch) { + if (isReverseSearch) { resultIndex = searchStringLine.lastIndexOf(searchTerm, searchIndex.col - searchTerm.length); } else { resultIndex = searchStringLine.indexOf(searchTerm, searchIndex.col); diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index be9ec479..0a38f755 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -32,8 +32,8 @@ class TestSearchHelper extends SearchHelper { public findInLine(term: string, rowNumber: number, searchOptions?: ISearchOptions): ISearchResult { return this._findInLine(term, {row: rowNumber, col: 0}, searchOptions); } - public findFromIndex(term: string, searchIndex: ISearchIndex, searchOptions?: ISearchOptions): ISearchResult { - return this._findInLine(term, searchIndex, searchOptions); + public findFromIndex(term: string, searchIndex: ISearchIndex, searchOptions?: ISearchOptions, isReverseSearch?: boolean): ISearchResult { + return this._findInLine(term, searchIndex, searchOptions, isReverseSearch); } } @@ -279,13 +279,13 @@ describe('search addon', () => { const searchOptions = { regex: false, wholeWord: false, - caseSensitive: false, - reverseSearch: true + caseSensitive: false }; - const find0 = term.searchHelper.findFromIndex('is', {row: 0, col: 16}, searchOptions); - const find1 = term.searchHelper.findFromIndex('is', {row: 0, col: find0.col}, searchOptions); - const find2 = term.searchHelper.findFromIndex('it', {row: 0, col: 16}, searchOptions); - const find3 = term.searchHelper.findFromIndex('it', {row: 0, col: find2.col}, searchOptions); + const isReverseSearch = true; + const find0 = term.searchHelper.findFromIndex('is', {row: 0, col: 16}, searchOptions, isReverseSearch); + const find1 = term.searchHelper.findFromIndex('is', {row: 0, col: find0.col}, searchOptions, isReverseSearch); + const find2 = term.searchHelper.findFromIndex('it', {row: 0, col: 16}, searchOptions, isReverseSearch); + const find3 = term.searchHelper.findFromIndex('it', {row: 0, col: find2.col}, searchOptions, isReverseSearch); expect(find0).eql({col: 14, row: 0, term: 'is'}); expect(find1).eql({col: 3, row: 0, term: 'is'}); expect(find2).eql({col: 11, row: 0, term: 'it'}); @@ -299,13 +299,13 @@ describe('search addon', () => { const searchOptions = { regex: true, wholeWord: false, - caseSensitive: true, - reverseSearch: true + caseSensitive: true }; - const find0 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: 16}, searchOptions); - const find1 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find0.col}, searchOptions); - const find2 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find1.col}, searchOptions); - const find3 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find2.col}, searchOptions); + const isReverseSearch = true; + const find0 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: 16}, searchOptions, isReverseSearch); + const find1 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find0.col}, searchOptions, isReverseSearch); + const find2 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find1.col}, searchOptions, isReverseSearch); + const find3 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find2.col}, searchOptions, isReverseSearch); expect(find0).eql({col: 13, row: 0, term: 'ABC'}); expect(find1).eql({col: 10, row: 0, term: 'ABC'}); expect(find2).eql({col: 3, row: 0, term: 'ABC'}); From a0a8003d608d180f551f3745f2fe1f747e35dc98 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 11 Dec 2018 09:06:50 -0800 Subject: [PATCH 03/20] Add unit tests debug target --- .gitignore | 1 - .vscode/launch.json | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 .vscode/launch.json diff --git a/.gitignore b/.gitignore index 25e93d9b..b50ab2d9 100644 --- a/.gitignore +++ b/.gitignore @@ -12,7 +12,6 @@ npm-debug.log /.idea/ .env build/ -.vscode/ .DS_Store fixtures/typings-test/*.js package-lock.json diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..1c3aaf0f --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,23 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Unit Tests", + "cwd": "${workspaceRoot}", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha", + "windows": { + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/mocha.cmd" + }, + "runtimeArgs": [ + "--colors", + "--recursive", + "${workspaceRoot}/lib" + ], + "sourceMaps": true, + "outFiles": [ "${workspaceRoot}/lib/**/*.js" ], + "internalConsoleOptions": "openOnSessionStart" + } + ] +} From 943def1391e06f08488c574c681eb9ffd4aef474 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 11 Dec 2018 09:37:20 -0800 Subject: [PATCH 04/20] Add debug target for client, fix source maps in demo to point to ts --- .vscode/launch.json | 10 ++++++++++ demo/start.js | 5 +++++ package.json | 2 +- yarn.lock | 21 +++++---------------- 4 files changed, 21 insertions(+), 17 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 1c3aaf0f..36008195 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,6 +18,16 @@ "sourceMaps": true, "outFiles": [ "${workspaceRoot}/lib/**/*.js" ], "internalConsoleOptions": "openOnSessionStart" + }, + { + "type": "chrome", + "request": "launch", + "name": "Demo", + "url": "http://0.0.0.0:3000", + "windows": { + "url": "http://127.0.0.1:3000" + }, + "webRoot": "${workspaceFolder}/" } ] } diff --git a/demo/start.js b/demo/start.js index ad9e9f2f..f53ae7cc 100644 --- a/demo/start.js +++ b/demo/start.js @@ -22,6 +22,11 @@ const clientConfig = { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/ + }, + { + test: /\.js$/, + use: ["source-map-loader"], + enforce: "pre" } ] }, diff --git a/package.json b/package.json index 4aef95af..d4c4099b 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "nodemon": "1.10.2", "nyc": "^11.8.0", "sorcery": "^0.10.0", - "source-map-loader": "^0.2.3", + "source-map-loader": "^0.2.4", "ts-loader": "^4.5.0", "tslint": "^5.9.1", "tslint-consistent-codestyle": "^1.13.0", diff --git a/yarn.lock b/yarn.lock index db31bfe1..54fdb7bb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3894,16 +3894,6 @@ loader-utils@^1.0.2, loader-utils@^1.1.0: emojis-list "^2.0.0" json5 "^0.5.0" -loader-utils@~0.2.2: - version "0.2.17" - resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.17.tgz#f86e6374d43205a6e6c60e9196f17c0299bfb348" - integrity sha1-+G5jdNQyBabmxg6RlvF8Apm/s0g= - dependencies: - big.js "^3.1.3" - emojis-list "^2.0.0" - json5 "^0.5.0" - object-assign "^4.0.1" - locate-path@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" @@ -6039,14 +6029,13 @@ source-list-map@^2.0.0: resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.0.tgz#aaa47403f7b245a92fbc97ea08f250d6087ed085" integrity sha512-I2UmuJSRr/T8jisiROLU3A3ltr+swpniSmNPI4Ml3ZCX6tVnDsuZzK7F2hl5jTqbZBWCEKlj5HRQiPExXLgE8A== -source-map-loader@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-0.2.3.tgz#d4b0c8cd47d54edce3e6bfa0f523f452b5b0e521" - integrity sha512-MYbFX9DYxmTQFfy2v8FC1XZwpwHKYxg3SK8Wb7VPBKuhDjz8gi9re2819MsG4p49HDyiOSUKlmZ+nQBArW5CGw== +source-map-loader@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/source-map-loader/-/source-map-loader-0.2.4.tgz#c18b0dc6e23bf66f6792437557c569a11e072271" + integrity sha512-OU6UJUty+i2JDpTItnizPrlpOIBLmQbWMuBg9q5bVtnHACqw1tn9nNwqJLbv0/00JjnJb/Ee5g5WS5vrRv7zIQ== dependencies: async "^2.5.0" - loader-utils "~0.2.2" - source-map "~0.6.1" + loader-utils "^1.1.0" source-map-resolve@^0.5.0, source-map-resolve@^0.5.1: version "0.5.2" From d7f284c676eb59ad917d23675e8e9917648b951c Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Tue, 11 Dec 2018 09:56:33 -0800 Subject: [PATCH 05/20] Add debug target for demo server, merge 2 start processes into one --- .vscode/launch.json | 13 +++- demo/server.js | 174 +++++++++++++++++++++++--------------------- demo/start.js | 4 +- package.json | 1 + 4 files changed, 105 insertions(+), 87 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 36008195..c7bf7381 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -22,12 +22,23 @@ { "type": "chrome", "request": "launch", - "name": "Demo", + "name": "Demo Client", "url": "http://0.0.0.0:3000", "windows": { "url": "http://127.0.0.1:3000" }, "webRoot": "${workspaceFolder}/" + }, + { + "type": "node", + "request": "launch", + "name": "Demo Server", + "runtimeExecutable": "npm", + "runtimeArgs": [ + "run", + "start-debug" + ], + "port": 9229 } ] } diff --git a/demo/server.js b/demo/server.js index ebe2f644..5ff9ca61 100644 --- a/demo/server.js +++ b/demo/server.js @@ -1,100 +1,106 @@ var express = require('express'); -var app = express(); -var expressWs = require('express-ws')(app); +var expressWs = require('express-ws'); var os = require('os'); var pty = require('node-pty'); -var terminals = {}, - logs = {}; +function startServer() { + var app = express(); + expressWs(app); -app.use('/build', express.static(__dirname + '/../build')); + var terminals = {}, + logs = {}; -app.get('/', function(req, res){ - res.sendFile(__dirname + '/index.html'); -}); + app.use('/build', express.static(__dirname + '/../build')); -app.get('/style.css', function(req, res){ - res.sendFile(__dirname + '/style.css'); -}); - -app.get('/dist/client-bundle.js', function(req, res){ - res.sendFile(__dirname + '/dist/client-bundle.js'); -}); - -app.post('/terminals', function (req, res) { - var cols = parseInt(req.query.cols), - rows = parseInt(req.query.rows), - term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], { - name: 'xterm-color', - cols: cols || 80, - rows: rows || 24, - cwd: process.env.PWD, - env: process.env - }); - - console.log('Created terminal with PID: ' + term.pid); - terminals[term.pid] = term; - logs[term.pid] = ''; - term.on('data', function(data) { - logs[term.pid] += data; + app.get('/', function(req, res){ + res.sendFile(__dirname + '/index.html'); }); - res.send(term.pid.toString()); - res.end(); -}); -app.post('/terminals/:pid/size', function (req, res) { - var pid = parseInt(req.params.pid), - cols = parseInt(req.query.cols), - rows = parseInt(req.query.rows), - term = terminals[pid]; + app.get('/style.css', function(req, res){ + res.sendFile(__dirname + '/style.css'); + }); - term.resize(cols, rows); - console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.'); - res.end(); -}); + app.get('/dist/client-bundle.js', function(req, res){ + res.sendFile(__dirname + '/dist/client-bundle.js'); + }); -app.ws('/terminals/:pid', function (ws, req) { - var term = terminals[parseInt(req.params.pid)]; - console.log('Connected to terminal ' + term.pid); - ws.send(logs[term.pid]); + app.post('/terminals', function (req, res) { + var cols = parseInt(req.query.cols), + rows = parseInt(req.query.rows), + term = pty.spawn(process.platform === 'win32' ? 'cmd.exe' : 'bash', [], { + name: 'xterm-color', + cols: cols || 80, + rows: rows || 24, + cwd: process.env.PWD, + env: process.env + }); - function buffer(socket, timeout) { - let s = ''; - let sender = null; - return (data) => { - s += data; - if (!sender) { - sender = setTimeout(() => { - socket.send(s); - s = ''; - sender = null; - }, timeout); - } - }; - } - const send = buffer(ws, 5); + console.log('Created terminal with PID: ' + term.pid); + terminals[term.pid] = term; + logs[term.pid] = ''; + term.on('data', function(data) { + logs[term.pid] += data; + }); + res.send(term.pid.toString()); + res.end(); + }); - term.on('data', function(data) { - try { - send(data); - } catch (ex) { - // The WebSocket is not open, ignore + app.post('/terminals/:pid/size', function (req, res) { + var pid = parseInt(req.params.pid), + cols = parseInt(req.query.cols), + rows = parseInt(req.query.rows), + term = terminals[pid]; + + term.resize(cols, rows); + console.log('Resized terminal ' + pid + ' to ' + cols + ' cols and ' + rows + ' rows.'); + res.end(); + }); + + app.ws('/terminals/:pid', function (ws, req) { + var term = terminals[parseInt(req.params.pid)]; + console.log('Connected to terminal ' + term.pid); + ws.send(logs[term.pid]); + + function buffer(socket, timeout) { + let s = ''; + let sender = null; + return (data) => { + s += data; + if (!sender) { + sender = setTimeout(() => { + socket.send(s); + s = ''; + sender = null; + }, timeout); + } + }; } - }); - ws.on('message', function(msg) { - term.write(msg); - }); - ws.on('close', function () { - term.kill(); - console.log('Closed terminal ' + term.pid); - // Clean things up - delete terminals[term.pid]; - delete logs[term.pid]; - }); -}); + const send = buffer(ws, 5); -var port = process.env.PORT || 3000, - host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0'; + term.on('data', function(data) { + try { + send(data); + } catch (ex) { + // The WebSocket is not open, ignore + } + }); + ws.on('message', function(msg) { + term.write(msg); + }); + ws.on('close', function () { + term.kill(); + console.log('Closed terminal ' + term.pid); + // Clean things up + delete terminals[term.pid]; + delete logs[term.pid]; + }); + }); -console.log('App listening to http://' + host + ':' + port); -app.listen(port, host); + var port = process.env.PORT || 3000, + host = os.platform() === 'win32' ? '127.0.0.1' : '0.0.0.0'; + + console.log('App listening to http://' + host + ':' + port); + app.listen(port, host); +} + +module.exports = startServer; diff --git a/demo/start.js b/demo/start.js index f53ae7cc..78f1ff1d 100644 --- a/demo/start.js +++ b/demo/start.js @@ -8,9 +8,9 @@ const cp = require('child_process'); const path = require('path'); const webpack = require('webpack'); +const startServer = require('./server.js'); -// Launch server -cp.spawn('node', [path.resolve(__dirname, 'server.js')], { stdio: 'inherit' }); +startServer(); // Build/watch client source const clientConfig = { diff --git a/package.json b/package.json index d4c4099b..b9d1d34d 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ }, "scripts": { "start": "node demo/start", + "start-debug": "node --inspect-brk demo/start", "start-zmodem": "node demo/zmodem/app", "lint": "tslint 'src/**/*.ts' './demo/**/*.ts'", "pretest": "npm run layering", From 9cd7bf2af1178c29d6be4254bd13ed0c191ae299 Mon Sep 17 00:00:00 2001 From: Linmiao Xu Date: Wed, 19 Dec 2018 13:11:45 +0900 Subject: [PATCH 06/20] Weblinks should not allow quotes at end of urls enclosed in quotes --- src/addons/webLinks/webLinks.test.ts | 24 ++++++++++++++++++++++++ src/addons/webLinks/webLinks.ts | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/addons/webLinks/webLinks.test.ts b/src/addons/webLinks/webLinks.test.ts index 8ada2510..1e8a4ae7 100644 --- a/src/addons/webLinks/webLinks.test.ts +++ b/src/addons/webLinks/webLinks.test.ts @@ -63,4 +63,28 @@ describe('webLinks addon', () => { assert.equal(uri, 'http://foo.com/colon:test'); }); + + it('should not allow " character at the end of a URI enclosed with ""', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = '"http://foo.com/"'; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/'); + }); + + it('should not allow \' character at the end of a URI enclosed with \'\'', () => { + const term = new MockTerminal(); + webLinks.webLinksInit(term); + + const row = '\'http://foo.com/\''; + + const match = row.match(term.regex); + const uri = match[term.options.matchIndex]; + + assert.equal(uri, 'http://foo.com/'); + }); }); diff --git a/src/addons/webLinks/webLinks.ts b/src/addons/webLinks/webLinks.ts index 75d79104..f0d69cc5 100644 --- a/src/addons/webLinks/webLinks.ts +++ b/src/addons/webLinks/webLinks.ts @@ -14,7 +14,7 @@ const ipClause = '((\\d{1,3}\\.){3}\\d{1,3})'; const localHostClause = '(localhost)'; const portClause = '(:\\d{1,5})'; const hostClause = '((' + domainBodyClause + '\\.' + tldClause + ')|' + ipClause + '|' + localHostClause + ')' + portClause + '?'; -const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:\\s])'; +const pathClause = '(\\/[\\/\\w\\.\\-%~:]*)*([^:"\'\\s])'; const queryStringHashFragmentCharacterSet = '[0-9\\w\\[\\]\\(\\)\\/\\?\\!#@$%&\'*+,:;~\\=\\.\\-]*'; const queryStringClause = '(\\?' + queryStringHashFragmentCharacterSet + ')?'; const hashFragmentClause = '(#' + queryStringHashFragmentCharacterSet + ')?'; From 1104bcbccb523a856f2181060fc34ff73e4a3d48 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Thu, 20 Dec 2018 09:41:44 -0500 Subject: [PATCH 07/20] Allow holding key on mac to send multiple keys to terminal --- src/core/input/Keyboard.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/input/Keyboard.ts b/src/core/input/Keyboard.ts index 8c6f3c59..eccc3769 100644 --- a/src/core/input/Keyboard.ts +++ b/src/core/input/Keyboard.ts @@ -349,6 +349,8 @@ export function evaluateKeyboardEvent( if (ev.keyCode === 65) { // cmd + a result.type = KeyboardResultType.SELECT_ALL; } + } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.key !== 'Shift') { + result.key = ev.key; } break; } From ec1060187d4789508a779744ab208efb0a92ca61 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Thu, 20 Dec 2018 09:50:18 -0500 Subject: [PATCH 08/20] Add tests --- src/core/input/Keyboard.test.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/core/input/Keyboard.test.ts b/src/core/input/Keyboard.test.ts index e9846831..e0735a4a 100644 --- a/src/core/input/Keyboard.test.ts +++ b/src/core/input/Keyboard.test.ts @@ -281,5 +281,15 @@ describe('Keyboard', () => { assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }).key, '\x1b[B'); assert.equal(testEvaluateKeyboardEvent({ keyCode: 0, key: 'UIKeyInputDownArrow' }, { applicationCursorMode: true }).key, '\x1bOB'); }); + + it('should handle lowercase letters', () => { + assert.equal(testEvaluateKeyboardEvent({ keyCode: 65, key: 'a' }).key, 'a'); + assert.equal(testEvaluateKeyboardEvent({ keyCode: 189, key: '-' }).key, '-'); + }); + + it('should handle uppercase letters', () => { + assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 65, key: 'A' }).key, 'A'); + }); + }); }); From b820f23da6817e299ca18e11bbd1008c45b0a024 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Thu, 20 Dec 2018 09:50:57 -0500 Subject: [PATCH 09/20] Add additional test --- src/core/input/Keyboard.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/input/Keyboard.test.ts b/src/core/input/Keyboard.test.ts index e0735a4a..a0dc3cbc 100644 --- a/src/core/input/Keyboard.test.ts +++ b/src/core/input/Keyboard.test.ts @@ -289,6 +289,7 @@ describe('Keyboard', () => { it('should handle uppercase letters', () => { assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 65, key: 'A' }).key, 'A'); + assert.equal(testEvaluateKeyboardEvent({ shiftKey: true, keyCode: 49, key: '!' }).key, '!'); }); }); From 0782fbb8284a0813d1edd8e11931f3977397a86e Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Thu, 20 Dec 2018 09:57:24 -0500 Subject: [PATCH 10/20] Fix check --- src/core/input/Keyboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/input/Keyboard.ts b/src/core/input/Keyboard.ts index eccc3769..31e97e35 100644 --- a/src/core/input/Keyboard.ts +++ b/src/core/input/Keyboard.ts @@ -349,7 +349,7 @@ export function evaluateKeyboardEvent( if (ev.keyCode === 65) { // cmd + a result.type = KeyboardResultType.SELECT_ALL; } - } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.key !== 'Shift') { + } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 65) { result.key = ev.key; } break; From 64dd9d46fbe18720651ad218ef39375d414fece7 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Thu, 20 Dec 2018 14:21:35 -0500 Subject: [PATCH 11/20] Change keyCode cutoff --- src/core/input/Keyboard.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/input/Keyboard.ts b/src/core/input/Keyboard.ts index 31e97e35..21c81a81 100644 --- a/src/core/input/Keyboard.ts +++ b/src/core/input/Keyboard.ts @@ -349,7 +349,7 @@ export function evaluateKeyboardEvent( if (ev.keyCode === 65) { // cmd + a result.type = KeyboardResultType.SELECT_ALL; } - } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 65) { + } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 48) { result.key = ev.key; } break; From a4b4d082fb1f0a84105554fc4c42edb07bbaf362 Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Fri, 21 Dec 2018 07:38:14 -0500 Subject: [PATCH 12/20] Don't include num lock and scroll lock --- src/core/input/Keyboard.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/input/Keyboard.ts b/src/core/input/Keyboard.ts index 21c81a81..37e663f5 100644 --- a/src/core/input/Keyboard.ts +++ b/src/core/input/Keyboard.ts @@ -349,7 +349,8 @@ export function evaluateKeyboardEvent( if (ev.keyCode === 65) { // cmd + a result.type = KeyboardResultType.SELECT_ALL; } - } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 48) { + } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 48 + && ev.keyCode !== 144 && ev.keyCode !== 145) { // Include only keys that that result in a character; don't include num lock and scroll lock result.key = ev.key; } break; From 204e42a1513fdbdeb0a664e2471161d770f8c2c1 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 15:15:12 -0800 Subject: [PATCH 13/20] Fix incremental search --- src/addons/search/SearchHelper.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 408e1f29..d3040f73 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -49,8 +49,7 @@ export class SearchHelper implements ISearchHelper { // For incremental search, use existing row if (this._terminal.getSelection().length !== 0) { startRow = incremental ? selectionManager.selectionStart[1] : selectionManager.selectionEnd[1]; - // TODO: Fix for incremental - startCol = this._terminal._core.selectionManager.selectionEnd[0]; + startCol = incremental ? selectionManager.selectionStart[0] : selectionManager.selectionEnd[0]; } } From 76302e2146bb2724137a86aa73d6b3b3c4f709c6 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 15:32:34 -0800 Subject: [PATCH 14/20] Get multiple matches working after incremental changes --- src/addons/search/SearchHelper.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index d3040f73..0a66fdff 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -52,11 +52,12 @@ export class SearchHelper implements ISearchHelper { startCol = incremental ? selectionManager.selectionStart[0] : selectionManager.selectionEnd[0]; } } + console.log(`Start from ${startCol},${startRow}`); this._initLinesCache(); // Search from startRow to end - for (let y = incremental ? startRow : startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { + for (let y = startRow; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { result = this._findInLine(term, { row: y, col: startCol }, searchOptions); if (result) { break; @@ -111,7 +112,7 @@ export class SearchHelper implements ISearchHelper { this._initLinesCache(); // Search from startRow to top - for (let y = incremental ? startRow : startRow - 1; y >= 0; y--) { + for (let y = startRow; y >= 0; y--) { result = this._findInLine(term, {row: y, col: startCol}, searchOptions, isReverseSearch); if (result) { break; @@ -216,6 +217,7 @@ export class SearchHelper implements ISearchHelper { resultIndex = searchStringLine.lastIndexOf(searchTerm, searchIndex.col - searchTerm.length); } else { resultIndex = searchStringLine.indexOf(searchTerm, searchIndex.col); + console.log('resultIndex', resultIndex); } } From e0d535e0da1050aaa19860747c0cdedf6017cd38 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 15:49:08 -0800 Subject: [PATCH 15/20] Fix previous search, remove incremental previous search --- demo/client.ts | 6 ++-- src/addons/search/Interfaces.ts | 5 ++- src/addons/search/SearchHelper.ts | 57 ++++++++++++++++++------------- 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/demo/client.ts b/demo/client.ts index d99ff66d..acc83cb8 100644 --- a/demo/client.ts +++ b/demo/client.ts @@ -113,9 +113,9 @@ function createTerminal(): void { }); addDomListener(actionElements.findPrevious, 'keyup', (e) => { - const searchOptions = getSearchOptions(); - searchOptions.incremental = e.key !== `Enter`; - term.findPrevious(actionElements.findPrevious.value, searchOptions); + if (e.key === `Enter`) { + term.findPrevious(actionElements.findPrevious.value, getSearchOptions()); + } }); // fit is called within a setTimeout, cols and rows need this. diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts index 76fe4bc9..2489844e 100644 --- a/src/addons/search/Interfaces.ts +++ b/src/addons/search/Interfaces.ts @@ -25,7 +25,10 @@ export interface ISearchOptions { regex?: boolean; wholeWord?: boolean; caseSensitive?: boolean; - /** Assume caller implements 'search as you type' where findNext gets called when search input changes */ + /** + * Use this when you want the selection to expand if it still matches as the + * user types. Note that this only affects findNext. + */ incremental?: boolean; } diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 0a66fdff..8972917b 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -52,24 +52,27 @@ export class SearchHelper implements ISearchHelper { startCol = incremental ? selectionManager.selectionStart[0] : selectionManager.selectionEnd[0]; } } - console.log(`Start from ${startCol},${startRow}`); this._initLinesCache(); - // Search from startRow to end - for (let y = startRow; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { - result = this._findInLine(term, { row: y, col: startCol }, searchOptions); - if (result) { - break; + // Search startRow + result = this._findInLine(term, { row: startRow, col: startCol }, searchOptions); + + // Search from startRow + 1 to end + if (!result) { + for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { + result = this._findInLine(term, { row: y, col: 0 }, searchOptions); + if (result) { + break; + } } - startCol = 0; } - // Search from the top to the startRow + // Search from the top to the startRow (search the whole startRow again in + // case startCol > 0) if (!result) { - for (let y = 0; y < startRow; y++) { - startCol = 0; - result = this._findInLine(term, {row: y, col: startCol}, searchOptions); + for (let y = 0; y <= startRow; y++) { + result = this._findInLine(term, {row: y, col: 0}, searchOptions); if (result) { break; } @@ -89,7 +92,6 @@ export class SearchHelper implements ISearchHelper { */ public findPrevious(term: string, searchOptions?: ISearchOptions): boolean { const selectionManager = this._terminal._core.selectionManager; - const {incremental} = searchOptions; let result: ISearchResult; if (!term || term.length === 0) { @@ -111,21 +113,31 @@ export class SearchHelper implements ISearchHelper { this._initLinesCache(); - // Search from startRow to top - for (let y = startRow; y >= 0; y--) { - result = this._findInLine(term, {row: y, col: startCol}, searchOptions, isReverseSearch); - if (result) { - break; + // Search startRow + result = this._findInLine(term, { row: startRow, col: startCol }, searchOptions, isReverseSearch); + + // Search from startRow - 1 to top + if (!result) { + for (let y = startRow - 1; y >= 0; y--) { + result = this._findInLine(term, { + row: y, + col: this._terminal._core.buffer.lines.get(y).length + }, searchOptions, isReverseSearch); + if (result) { + break; + } } - startCol = y > 0 ? this._terminal._core.buffer.lines.get(y - 1).length : 0; } - // Search from the bottom to startRow + // Search from the bottom to startRow (search the whole startRow again in + // case startCol > 0) if (!result) { const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1; - for (let y = searchFrom; y > startRow; y--) { - startCol = this._terminal._core.buffer.lines.get(y).length; - result = this._findInLine(term, {row: y, col: startCol}, searchOptions, isReverseSearch); + for (let y = searchFrom; y >= startRow; y--) { + result = this._findInLine(term, { + row: y, + col: this._terminal._core.buffer.lines.get(y).length + }, searchOptions, isReverseSearch); if (result) { break; } @@ -217,7 +229,6 @@ export class SearchHelper implements ISearchHelper { resultIndex = searchStringLine.lastIndexOf(searchTerm, searchIndex.col - searchTerm.length); } else { resultIndex = searchStringLine.indexOf(searchTerm, searchIndex.col); - console.log('resultIndex', resultIndex); } } From 1a1f7e984fcda6ca4f565a717d8f5ed8fe0a993f Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 16:03:31 -0800 Subject: [PATCH 16/20] Remove ISearchIndex object --- src/addons/search/Interfaces.ts | 7 ++--- src/addons/search/SearchHelper.ts | 49 ++++++++++++++----------------- src/addons/search/search.test.ts | 34 ++++++++++----------- 3 files changed, 41 insertions(+), 49 deletions(-) diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts index 2489844e..a1f05895 100644 --- a/src/addons/search/Interfaces.ts +++ b/src/addons/search/Interfaces.ts @@ -32,11 +32,8 @@ export interface ISearchOptions { incremental?: boolean; } -export interface ISearchIndex { +export interface ISearchResult { + term: string; col: number; row: number; } - -export interface ISearchResult extends ISearchIndex { - term: string; -} diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 8972917b..756f2da7 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -3,7 +3,7 @@ * @license MIT */ -import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult, ISearchIndex } from './Interfaces'; +import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces'; const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?'; const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs @@ -56,12 +56,12 @@ export class SearchHelper implements ISearchHelper { this._initLinesCache(); // Search startRow - result = this._findInLine(term, { row: startRow, col: startCol }, searchOptions); + result = this._findInLine(term, startRow, startCol, searchOptions); // Search from startRow + 1 to end if (!result) { for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) { - result = this._findInLine(term, { row: y, col: 0 }, searchOptions); + result = this._findInLine(term, y, 0, searchOptions); if (result) { break; } @@ -72,7 +72,7 @@ export class SearchHelper implements ISearchHelper { // case startCol > 0) if (!result) { for (let y = 0; y <= startRow; y++) { - result = this._findInLine(term, {row: y, col: 0}, searchOptions); + result = this._findInLine(term, y, 0, searchOptions); if (result) { break; } @@ -114,15 +114,12 @@ export class SearchHelper implements ISearchHelper { this._initLinesCache(); // Search startRow - result = this._findInLine(term, { row: startRow, col: startCol }, searchOptions, isReverseSearch); + result = this._findInLine(term, startRow, startCol, searchOptions, isReverseSearch); // Search from startRow - 1 to top if (!result) { for (let y = startRow - 1; y >= 0; y--) { - result = this._findInLine(term, { - row: y, - col: this._terminal._core.buffer.lines.get(y).length - }, searchOptions, isReverseSearch); + result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch); if (result) { break; } @@ -134,10 +131,7 @@ export class SearchHelper implements ISearchHelper { if (!result) { const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1; for (let y = searchFrom; y >= startRow; y--) { - result = this._findInLine(term, { - row: y, - col: this._terminal._core.buffer.lines.get(y).length - }, searchOptions, isReverseSearch); + result = this._findInLine(term, y, this._terminal._core.buffer.lines.get(y).length, searchOptions, isReverseSearch); if (result) { break; } @@ -187,20 +181,21 @@ export class SearchHelper implements ISearchHelper { * 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 The search term. - * @param y The line to search. + * @param row The line to start the search from. + * @param col The column to start the search from. * @param searchOptions Search options. * @return The search result if it was found. */ - protected _findInLine(term: string, searchIndex: ISearchIndex, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult { - if (this._terminal._core.buffer.lines.get(searchIndex.row).isWrapped) { + protected _findInLine(term: string, row: number, col: number, searchOptions: ISearchOptions = {}, isReverseSearch: boolean = false): ISearchResult { + if (this._terminal._core.buffer.lines.get(row).isWrapped) { return; } - let stringLine = this._linesCache ? this._linesCache[searchIndex.row] : void 0; + let stringLine = this._linesCache ? this._linesCache[row] : void 0; if (stringLine === void 0) { - stringLine = this.translateBufferLineToStringWithWrap(searchIndex.row, true); + stringLine = this.translateBufferLineToStringWithWrap(row, true); if (this._linesCache) { - this._linesCache[searchIndex.row] = stringLine; + this._linesCache[row] = stringLine; } } @@ -212,37 +207,37 @@ export class SearchHelper implements ISearchHelper { const searchRegex = RegExp(searchTerm, 'g'); let foundTerm: RegExpExecArray; if (isReverseSearch) { - while (foundTerm = searchRegex.exec(searchStringLine.slice(0, searchIndex.col))) { + while (foundTerm = searchRegex.exec(searchStringLine.slice(0, col))) { resultIndex = searchRegex.lastIndex - foundTerm[0].length; term = foundTerm[0]; searchRegex.lastIndex -= (term.length - 1); } } else { - foundTerm = searchRegex.exec(searchStringLine.slice(searchIndex.col)); + foundTerm = searchRegex.exec(searchStringLine.slice(col)); if (foundTerm && foundTerm[0].length > 0) { - resultIndex = searchIndex.col + (searchRegex.lastIndex - foundTerm[0].length); + resultIndex = col + (searchRegex.lastIndex - foundTerm[0].length); term = foundTerm[0]; } } } else { if (isReverseSearch) { - resultIndex = searchStringLine.lastIndexOf(searchTerm, searchIndex.col - searchTerm.length); + resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length); } else { - resultIndex = searchStringLine.indexOf(searchTerm, searchIndex.col); + resultIndex = searchStringLine.indexOf(searchTerm, col); } } if (resultIndex >= 0) { // Adjust the row number and search index if needed since a "line" of text can span multiple rows if (resultIndex >= this._terminal.cols) { - searchIndex.row += Math.floor(resultIndex / this._terminal.cols); + row += Math.floor(resultIndex / this._terminal.cols); resultIndex = resultIndex % this._terminal.cols; } if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) { return; } - const line = this._terminal._core.buffer.lines.get(searchIndex.row); + const line = this._terminal._core.buffer.lines.get(row); for (let i = 0; i < resultIndex; i++) { const charData = line.get(i); @@ -261,7 +256,7 @@ export class SearchHelper implements ISearchHelper { return { term, col: resultIndex, - row: searchIndex.row + row }; } } diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index e9c11fb2..6551fa8b 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -7,7 +7,7 @@ declare var require: any; import { assert, expect } from 'chai'; import * as search from './search'; import { SearchHelper } from './SearchHelper'; -import { ISearchOptions, ISearchResult, ISearchIndex } from './Interfaces'; +import { ISearchOptions, ISearchResult } from './Interfaces'; class MockTerminalPlain {} @@ -30,10 +30,10 @@ class MockTerminal { class TestSearchHelper extends SearchHelper { public findInLine(term: string, rowNumber: number, searchOptions?: ISearchOptions): ISearchResult { - return this._findInLine(term, {row: rowNumber, col: 0}, searchOptions); + return this._findInLine(term, rowNumber, 0, searchOptions); } - public findFromIndex(term: string, searchIndex: ISearchIndex, searchOptions?: ISearchOptions, isReverseSearch?: boolean): ISearchResult { - return this._findInLine(term, searchIndex, searchOptions, isReverseSearch); + public findFromIndex(term: string, row: number, col: number, searchOptions?: ISearchOptions, isReverseSearch?: boolean): ISearchResult { + return this._findInLine(term, row, col, searchOptions, isReverseSearch); } } @@ -258,11 +258,11 @@ describe('search addon', () => { wholeWord: false, caseSensitive: false }; - const find0 = term.searchHelper.findFromIndex('hello', {row: 0, col: 0}, searchOptions); - const find1 = term.searchHelper.findFromIndex('hello', {row: 0, col: find0.col + find0.term.length}, searchOptions); - const find2 = term.searchHelper.findFromIndex('aaaa', {row: 1, col: 0}, searchOptions); - const find3 = term.searchHelper.findFromIndex('aaaa', {row: 1, col: find2.col + find2.term.length}, searchOptions); - const find4 = term.searchHelper.findFromIndex('aaaa', {row: 1, col: find3.col + find3.term.length}, searchOptions); + const find0 = term.searchHelper.findFromIndex('hello', 0, 0, searchOptions); + const find1 = term.searchHelper.findFromIndex('hello', 0, find0.col + find0.term.length, searchOptions); + const find2 = term.searchHelper.findFromIndex('aaaa', 1, 0, searchOptions); + const find3 = term.searchHelper.findFromIndex('aaaa', 1, find2.col + find2.term.length, searchOptions); + const find4 = term.searchHelper.findFromIndex('aaaa', 1, find3.col + find3.term.length, searchOptions); expect(find0).eql({col: 0, row: 0, term: 'hello'}); expect(find1).eql({col: 9, row: 0, term: 'hello'}); expect(find2).eql({col: 0, row: 1, term: 'aaaa'}); @@ -280,10 +280,10 @@ describe('search addon', () => { caseSensitive: false }; const isReverseSearch = true; - const find0 = term.searchHelper.findFromIndex('is', {row: 0, col: 16}, searchOptions, isReverseSearch); - const find1 = term.searchHelper.findFromIndex('is', {row: 0, col: find0.col}, searchOptions, isReverseSearch); - const find2 = term.searchHelper.findFromIndex('it', {row: 0, col: 16}, searchOptions, isReverseSearch); - const find3 = term.searchHelper.findFromIndex('it', {row: 0, col: find2.col}, searchOptions, isReverseSearch); + const find0 = term.searchHelper.findFromIndex('is', 0, 16, searchOptions, isReverseSearch); + const find1 = term.searchHelper.findFromIndex('is', 0, find0.col, searchOptions, isReverseSearch); + const find2 = term.searchHelper.findFromIndex('it', 0, 16, searchOptions, isReverseSearch); + const find3 = term.searchHelper.findFromIndex('it', 0, find2.col, searchOptions, isReverseSearch); expect(find0).eql({col: 14, row: 0, term: 'is'}); expect(find1).eql({col: 3, row: 0, term: 'is'}); expect(find2).eql({col: 11, row: 0, term: 'it'}); @@ -300,10 +300,10 @@ describe('search addon', () => { caseSensitive: true }; const isReverseSearch = true; - const find0 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: 16}, searchOptions, isReverseSearch); - const find1 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find0.col}, searchOptions, isReverseSearch); - const find2 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find1.col}, searchOptions, isReverseSearch); - const find3 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find2.col}, searchOptions, isReverseSearch); + const find0 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, 16, searchOptions, isReverseSearch); + const find1 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find0.col, searchOptions, isReverseSearch); + const find2 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find1.col, searchOptions, isReverseSearch); + const find3 = term.searchHelper.findFromIndex('[A-Z]{3}', 0, find2.col, searchOptions, isReverseSearch); expect(find0).eql({col: 13, row: 0, term: 'ABC'}); expect(find1).eql({col: 10, row: 0, term: 'ABC'}); expect(find2).eql({col: 3, row: 0, term: 'ABC'}); From d698faa18366df2592f5992ace5517d366382f79 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Wed, 26 Dec 2018 16:12:53 -0800 Subject: [PATCH 17/20] Comment how the regex reverse search while works --- src/addons/search/SearchHelper.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 756f2da7..42a3a122 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -207,6 +207,7 @@ export class SearchHelper implements ISearchHelper { const searchRegex = RegExp(searchTerm, 'g'); let foundTerm: RegExpExecArray; if (isReverseSearch) { + // This loop will get the resultIndex of the _last_ regex match in the range 0..col while (foundTerm = searchRegex.exec(searchStringLine.slice(0, col))) { resultIndex = searchRegex.lastIndex - foundTerm[0].length; term = foundTerm[0]; From 9bcfb4d7225e178a93389b96c9b1225e85f74182 Mon Sep 17 00:00:00 2001 From: Daniel Imms Date: Thu, 27 Dec 2018 10:04:48 -0800 Subject: [PATCH 18/20] Tidy up wrapping style --- src/core/input/Keyboard.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/core/input/Keyboard.ts b/src/core/input/Keyboard.ts index 37e663f5..9d86b349 100644 --- a/src/core/input/Keyboard.ts +++ b/src/core/input/Keyboard.ts @@ -349,8 +349,9 @@ export function evaluateKeyboardEvent( if (ev.keyCode === 65) { // cmd + a result.type = KeyboardResultType.SELECT_ALL; } - } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && ev.keyCode >= 48 - && ev.keyCode !== 144 && ev.keyCode !== 145) { // Include only keys that that result in a character; don't include num lock and scroll lock + } else if (ev.key && !ev.ctrlKey && !ev.altKey && !ev.metaKey && + ev.keyCode >= 48 && ev.keyCode !== 144 && ev.keyCode !== 145) { + // Include only keys that that result in a character; don't include num lock and scroll lock result.key = ev.key; } break; From bb198a980cde892c247e403b435535e2a676eee9 Mon Sep 17 00:00:00 2001 From: Vincent Woo Date: Thu, 27 Dec 2018 13:54:11 -0800 Subject: [PATCH 19/20] Small indent fix --- src/ui/CharMeasure.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/CharMeasure.ts b/src/ui/CharMeasure.ts index 7d1e5e48..2dfc4eb5 100644 --- a/src/ui/CharMeasure.ts +++ b/src/ui/CharMeasure.ts @@ -38,7 +38,7 @@ export class CharMeasure extends EventEmitter implements ICharMeasure { } public measure(options: ITerminalOptions): void { - this._measureElement.style.fontFamily = options.fontFamily; + this._measureElement.style.fontFamily = options.fontFamily; this._measureElement.style.fontSize = `${options.fontSize}px`; const geometry = this._measureElement.getBoundingClientRect(); // The element is likely currently display:none, we should retain the From 913150fe13ff45379accd92d67dc71ebf54d4f98 Mon Sep 17 00:00:00 2001 From: ntchjb Date: Sat, 29 Dec 2018 03:03:23 +0700 Subject: [PATCH 20/20] Fix search addons: searchTerm should not be matched if the beginning of matching index < 0 --- src/addons/search/SearchHelper.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 42a3a122..96cd845d 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -222,7 +222,9 @@ export class SearchHelper implements ISearchHelper { } } else { if (isReverseSearch) { - resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length); + if (col - searchTerm.length >= 0) { + resultIndex = searchStringLine.lastIndexOf(searchTerm, col - searchTerm.length); + } } else { resultIndex = searchStringLine.indexOf(searchTerm, col); }