Skip to content

Commit

Permalink
Merge 204e42a into 272cb08
Browse files Browse the repository at this point in the history
  • Loading branch information
noamyogev84 authored Dec 26, 2018
2 parents 272cb08 + 204e42a commit 0ba7c29
Show file tree
Hide file tree
Showing 3 changed files with 124 additions and 35 deletions.
7 changes: 5 additions & 2 deletions src/addons/search/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ export interface ISearchOptions {
incremental?: boolean;
}

export interface ISearchResult {
term: string;
export interface ISearchIndex {
col: number;
row: number;
}

export interface ISearchResult extends ISearchIndex {
term: string;
}
82 changes: 52 additions & 30 deletions src/addons/search/SearchHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* @license MIT
*/

import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces';
import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult, ISearchIndex } from './Interfaces';

const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs
Expand Down Expand Up @@ -41,30 +41,34 @@ export class SearchHelper implements ISearchHelper {
return false;
}

let startCol: number = 0;
let startRow = this._terminal._core.buffer.ydisp;

if (selectionManager.selectionEnd) {
// Start from the selection end if there is a selection
// For incremental search, use existing row
if (this._terminal.getSelection().length !== 0) {
startRow = incremental ? selectionManager.selectionStart[1] : selectionManager.selectionEnd[1];
startCol = incremental ? selectionManager.selectionStart[0] : selectionManager.selectionEnd[0];
}
}

this._initLinesCache();

// Search from startRow to end
for (let y = incremental ? startRow : startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
result = this._findInLine(term, y, searchOptions);
result = this._findInLine(term, { row: y, col: startCol }, searchOptions);
if (result) {
break;
}
startCol = 0;
}

// Search from the top to the startRow
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;
}
Expand Down Expand Up @@ -92,29 +96,35 @@ export class SearchHelper implements ISearchHelper {
return false;
}

const isReverseSearch = true;
let startRow = this._terminal._core.buffer.ydisp;
let startCol: number = this._terminal._core.buffer.lines.get(startRow).length;

if (selectionManager.selectionStart) {
// Start from the selection start if there is a selection
if (this._terminal.getSelection().length !== 0) {
startRow = selectionManager.selectionStart[1];
startCol = selectionManager.selectionStart[0];
}
}

this._initLinesCache();

// Search from startRow to top
for (let y = incremental ? startRow : startRow - 1; y >= 0; y--) {
result = this._findInLine(term, y, searchOptions);
result = this._findInLine(term, {row: y, col: startCol}, searchOptions, isReverseSearch);
if (result) {
break;
}
startCol = y > 0 ? this._terminal._core.buffer.lines.get(y - 1).length : 0;
}

// Search from the bottom to startRow
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, isReverseSearch);
if (result) {
break;
}
Expand Down Expand Up @@ -168,68 +178,80 @@ 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 = {}, isReverseSearch: boolean = false): ISearchResult {
if (this._terminal._core.buffer.lines.get(searchIndex.row).isWrapped) {
return;
}

let stringLine = this._linesCache ? this._linesCache[y] : void 0;
let stringLine = this._linesCache ? this._linesCache[searchIndex.row] : void 0;
if (stringLine === void 0) {
stringLine = this.translateBufferLineToStringWithWrap(y, true);
stringLine = this.translateBufferLineToStringWithWrap(searchIndex.row, true);
if (this._linesCache) {
this._linesCache[y] = stringLine;
this._linesCache[searchIndex.row] = stringLine;
}
}

const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
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 (isReverseSearch) {
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 (isReverseSearch) {
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
Expand Down
70 changes: 67 additions & 3 deletions src/addons/search/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {}

Expand All @@ -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, isReverseSearch?: boolean): ISearchResult {
return this._findInLine(term, searchIndex, searchOptions, isReverseSearch);
}
}

Expand Down Expand Up @@ -245,5 +248,66 @@ describe('search addon', () => {
expect(hello4).eql(undefined);
expect(hello5).eql(undefined);
});
it('should find multiple matches in line', function(): void {
search.apply(<any>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(<any>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
};
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'});
expect(find3).eql({col: 0, row: 0, term: 'it'});
});
it('should find multiple matches in line - reverse search with regex', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('zzzABCzzzzABCABC');
term.pushWriteData();
const searchOptions = {
regex: true,
wholeWord: false,
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);
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);
});
});
});

0 comments on commit 0ba7c29

Please sign in to comment.