Skip to content

Commit

Permalink
Merge 7cd6e8a into 5bc7fc4
Browse files Browse the repository at this point in the history
  • Loading branch information
noamyogev84 committed Oct 19, 2018
2 parents 5bc7fc4 + 7cd6e8a commit 99749c2
Show file tree
Hide file tree
Showing 3 changed files with 187 additions and 73 deletions.
1 change: 1 addition & 0 deletions src/addons/search/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export interface ISearchOptions {
regex?: boolean;
wholeWord?: boolean;
caseSensitive?: boolean;
matchMultiple?: boolean;
}

export interface ISearchResult {
Expand Down
191 changes: 118 additions & 73 deletions src/addons/search/SearchHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ const nonWordCharacters = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
* A class that knows how to search the terminal and how to display the results.
*/
export class SearchHelper implements ISearchHelper {
private _currentLineMatches: ISearchResult[] = [];
constructor(private _terminal: ISearchAddonTerminal) {
// TODO: Search for multiple instances on 1 line
// TODO: Don't use the actual selection, instead use a "find selection" so multiple instances can be highlighted
// TODO: Highlight other instances in the viewport
}
Expand All @@ -29,29 +29,32 @@ export class SearchHelper implements ISearchHelper {
}

let result: ISearchResult;

let startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionEnd) {
// Start from the selection end if there is a selection
startRow = this._terminal._core.selectionManager.selectionEnd[1];
}

// 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);
if (result) {
break;
if (this._currentLineMatches.length > 0) {
result = this._currentLineMatches.shift();
} else {
let startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionEnd) {
// Start from the selection end if there is a selection
startRow = this._terminal._core.selectionManager.selectionEnd[1];
}
}

// Search from the top to the current ydisp
if (!result) {
for (let y = 0; y < startRow; y++) {
// 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);
if (result) {
break;
}
}

// Search from the top to the current ydisp
if (!result) {
for (let y = 0; y < startRow; y++) {
result = this._findInLine(term, y, searchOptions);
if (result) {
break;
}
}
}
}

// Set selection and scroll if a result was found
Expand All @@ -71,29 +74,36 @@ export class SearchHelper implements ISearchHelper {
}

let result: ISearchResult;

let startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionStart) {
// Start from the selection end if there is a selection
startRow = this._terminal._core.selectionManager.selectionStart[1];
}

// Search from ydisp + 1 to end
for (let y = startRow - 1; y >= 0; y--) {
result = this._findInLine(term, y, searchOptions);
if (result) {
break;
if (this._currentLineMatches.length > 0) {
result = this._currentLineMatches.pop();
} else {
let startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionStart) {
// Start from the selection end if there is a selection
startRow = this._terminal._core.selectionManager.selectionStart[1];
}
}

// 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--) {
// Search from ydisp + 1 to end
for (let y = startRow - 1; y >= 0; y--) {
result = this._findInLine(term, y, searchOptions);
this._currentLineMatches.unshift(result); // Handle backward search
result = this._currentLineMatches.pop();
if (result) {
break;
}
}

// 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);
this._currentLineMatches.unshift(result); // Handle backward search
result = this._currentLineMatches.pop();
if (result) {
break;
}
}
}
}

// Set selection and scroll if a result was found
Expand Down Expand Up @@ -128,51 +138,19 @@ 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 = -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];
}
} else {
searchIndex = searchStringLine.indexOf(searchTerm);
}
const lineMatch = this._getNextMatch(term, searchStringLine, y, searchOptions);

if (searchIndex >= 0) {
if (lineMatch) {
// 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 (lineMatch.col >= this._terminal.cols) {
lineMatch.row += Math.floor(lineMatch.col / this._terminal.cols);
lineMatch.col = lineMatch.col % this._terminal.cols;
}
if (searchOptions.wholeWord && !this._isWholeWord(searchIndex, searchStringLine, term)) {
if (searchOptions.wholeWord && !this._isWholeWord(lineMatch.col, searchStringLine, lineMatch.term)) {
return;
}

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
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
if (char.length > 1) {
searchIndex -= 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++;
}
}
return {
term,
col: searchIndex,
row: y
};
this._normalizeMatch(lineMatch);
return lineMatch;
}
}

Expand Down Expand Up @@ -212,4 +190,71 @@ export class SearchHelper implements ISearchHelper {
this._terminal.scrollLines(result.row - this._terminal._core.buffer.ydisp);
return true;
}

/**
* Returnes an array of matches in the given line.
* @param term The term to search for.
* @param searchString The text to search in.
* @param row The row number.
* @param searchOptions search options,
* @return An array of matches or first match.
*/
private _getNextMatch(term: string, searchString: string, row: number, searchOptions: ISearchOptions): ISearchResult {

if (this._currentLineMatches.length > 0 && searchOptions.matchMultiple) {
return this._currentLineMatches.shift();
}

const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();

let currentIndex = 0;
if (searchOptions.regex) {
const searchRegex = RegExp(searchTerm, 'g');
let foundTerm: RegExpExecArray;
do {
foundTerm = searchRegex.exec(searchString);
if (foundTerm && foundTerm[0].length > 0) {
term = foundTerm[0];
currentIndex = searchRegex.lastIndex - foundTerm[0].length;
this._currentLineMatches.push({term, col: currentIndex, row});
searchRegex.lastIndex -= (term.length - 1); // Handle regex match overlap
}
} while (foundTerm && searchOptions.matchMultiple);
} else {
let nextIndex = 0;
while (currentIndex >= 0) {
currentIndex = searchString.indexOf(searchTerm, nextIndex);
if (currentIndex >= 0) {
this._currentLineMatches.push({term , col: currentIndex, row});
nextIndex = currentIndex + 1;
}
if (!searchOptions.matchMultiple) {
break;
}
}
}

if (this._currentLineMatches.length > 0) {
return this._currentLineMatches.shift();
}
return undefined;
}

private _normalizeMatch(match: ISearchResult): void {
const line = this._terminal._core.buffer.lines.get(match.row);
for (let i = 0; i < match.col; 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) {
match.col -= 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) {
match.col++;
}
}
}
}
68 changes: 68 additions & 0 deletions src/addons/search/search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,5 +243,73 @@ 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('AaazcxzczAaa\r\naaAda');
term.pushWriteData();
const searchOptions = {
regex: false,
wholeWord: false,
caseSensitive: false,
matchMultiple: true
};
const find0 = term.searchHelper.findInLine('aa', 0, searchOptions);
const find1 = term.searchHelper.findInLine('aa', 0, searchOptions);
const find2 = term.searchHelper.findInLine('aa', 0, searchOptions);
const find3 = term.searchHelper.findInLine('aa', 0, searchOptions);
const find4 = term.searchHelper.findInLine('aa', 1, searchOptions);
const find5 = term.searchHelper.findInLine('aa', 1, searchOptions);
expect(find0).eql({col: 0, row: 0, term: 'aa'});
expect(find1).eql({col: 1, row: 0, term: 'aa'});
expect(find2).eql({col: 9, row: 0, term: 'aa'});
expect(find3).eql({col: 10, row: 0, term: 'aa'});
expect(find4).eql({col: 0, row: 1, term: 'aa'});
expect(find5).eql({col: 1, row: 1, term: 'aa'});
});
it('should find multiple matches in line - case sensitive', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('aaaAa\r\nAAa');
term.pushWriteData();
const searchOptions = {
regex: false,
wholeWord: false,
caseSensitive: true,
matchMultiple: true
};
const find0 = term.searchHelper.findInLine('Aa', 0, searchOptions);
const find1 = term.searchHelper.findInLine('AA', 0, searchOptions);
const find2 = term.searchHelper.findInLine('AA', 1, searchOptions);
const find3 = term.searchHelper.findInLine('aa', 1, searchOptions);
expect(find0).eql({col: 3, row: 0, term: 'Aa'});
expect(find1).eql(undefined);
expect(find2).eql({col: 0, row: 1, term: 'AA'});
expect(find3).eql(undefined);
});
it('should find multiple matches in line - use regex', function(): void {
search.apply(<any>MockTerminal);
const term = new MockTerminal({cols: 20, rows: 5});
term.core.write('1234\r\naaaa');
term.pushWriteData();
const searchOptions = {
regex: true,
wholeWord: false,
caseSensitive: false,
matchMultiple: true
};
const find0 = term.searchHelper.findInLine('\\d\\d', 0, searchOptions);
const find1 = term.searchHelper.findInLine('\\d\\d', 0, searchOptions);
const find2 = term.searchHelper.findInLine('\\d\\d', 0, searchOptions);
const find3 = term.searchHelper.findInLine('aa', 1, searchOptions);
const find4 = term.searchHelper.findInLine('aa', 1, searchOptions);
const find5 = term.searchHelper.findInLine('aa', 1, searchOptions);
expect(find0).eql({col: 0, row: 0, term: '12'});
expect(find1).eql({col: 1, row: 0, term: '23'});
expect(find2).eql({col: 2, row: 0, term: '34'});
expect(find3).eql({col: 0, row: 1, term: 'aa'});
expect(find4).eql({col: 1, row: 1, term: 'aa'});
expect(find5).eql({col: 2, row: 1, term: 'aa'});
});
});
});

0 comments on commit 99749c2

Please sign in to comment.