Skip to content

Commit 376c790

Browse files
committed
implement find multiple matches in line.
start search from current selection. add unit tests.
1 parent 1db1770 commit 376c790

File tree

3 files changed

+126
-37
lines changed

3 files changed

+126
-37
lines changed

src/addons/search/Interfaces.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,13 @@ export interface ISearchOptions {
2525
regex?: boolean;
2626
wholeWord?: boolean;
2727
caseSensitive?: boolean;
28+
reverseSearch?: boolean;
2829
}
2930

30-
export interface ISearchResult {
31-
term: string;
31+
export interface ISearchIndex {
3232
col: number;
3333
row: number;
3434
}
35+
export interface ISearchResult extends ISearchIndex {
36+
term: string;
37+
}

src/addons/search/SearchHelper.ts

Lines changed: 54 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* @license MIT
44
*/
55

6-
import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult } from './Interfaces';
6+
import { ISearchHelper, ISearchAddonTerminal, ISearchOptions, ISearchResult, ISearchIndex } from './Interfaces';
77
const nonWordCharacters = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
88

99
/**
@@ -29,27 +29,30 @@ export class SearchHelper implements ISearchHelper {
2929
}
3030

3131
let result: ISearchResult;
32-
3332
let startRow = this._terminal._core.buffer.ydisp;
33+
let startCol: number = 0;
3434
if (this._terminal._core.selectionManager.selectionEnd) {
3535
// Start from the selection end if there is a selection
3636
if (this._terminal.getSelection().length !== 0) {
3737
startRow = this._terminal._core.selectionManager.selectionEnd[1];
38+
startCol = this._terminal._core.selectionManager.selectionEnd[0];
3839
}
3940
}
4041

4142
// Search from ydisp + 1 to end
42-
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
43-
result = this._findInLine(term, y, searchOptions);
43+
for (let y = startRow; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
44+
result = this._findInLine(term, {row: y, col: startCol}, searchOptions);
4445
if (result) {
4546
break;
4647
}
48+
startCol = 0;
4749
}
4850

4951
// Search from the top to the current ydisp
5052
if (!result) {
5153
for (let y = 0; y < startRow; y++) {
52-
result = this._findInLine(term, y, searchOptions);
54+
startCol = 0;
55+
result = this._findInLine(term, {row: y, col: startCol}, searchOptions);
5356
if (result) {
5457
break;
5558
}
@@ -72,28 +75,35 @@ export class SearchHelper implements ISearchHelper {
7275
return false;
7376
}
7477

75-
let result: ISearchResult;
78+
searchOptions.reverseSearch = true;
7679

80+
let result: ISearchResult;
7781
let startRow = this._terminal._core.buffer.ydisp;
82+
let startCol: number = this._terminal._core.buffer.lines.get(startRow).length;
83+
7884
if (this._terminal._core.selectionManager.selectionStart) {
7985
// Start from the selection end if there is a selection
8086
if (this._terminal.getSelection().length !== 0) {
8187
startRow = this._terminal._core.selectionManager.selectionStart[1];
88+
startCol = this._terminal._core.selectionManager.selectionStart[0];
8289
}
8390
}
8491

8592
// Search from ydisp + 1 to end
86-
for (let y = startRow - 1; y >= 0; y--) {
87-
result = this._findInLine(term, y, searchOptions);
93+
for (let y = startRow; y >= 0; y--) {
94+
result = this._findInLine(term, {row: y, col: startCol}, searchOptions);
8895
if (result) {
8996
break;
9097
}
98+
startCol = y > 0 ? this._terminal._core.buffer.lines.get(y - 1).length : 0;
9199
}
92100

93101
// Search from the top to the current ydisp
94102
if (!result) {
95-
for (let y = this._terminal._core.buffer.ybase + this._terminal.rows - 1; y > startRow; y--) {
96-
result = this._findInLine(term, y, searchOptions);
103+
const searchFrom = this._terminal._core.buffer.ybase + this._terminal.rows - 1;
104+
for (let y = searchFrom; y > startRow; y--) {
105+
startCol = this._terminal._core.buffer.lines.get(y).length;
106+
result = this._findInLine(term, {row: y, col: startCol}, searchOptions);
97107
if (result) {
98108
break;
99109
}
@@ -125,61 +135,73 @@ export class SearchHelper implements ISearchHelper {
125135
* @param searchOptions Search options.
126136
* @return The search result if it was found.
127137
*/
128-
protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult {
129-
if (this._terminal._core.buffer.lines.get(y).isWrapped) {
138+
protected _findInLine(term: string, searchIndex: ISearchIndex, searchOptions: ISearchOptions = {}): ISearchResult {
139+
if (this._terminal._core.buffer.lines.get(searchIndex.row).isWrapped) {
130140
return;
131141
}
132142

133-
const stringLine = this.translateBufferLineToStringWithWrap(y, true);
134-
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
143+
const stringLine = this.translateBufferLineToStringWithWrap(searchIndex.row, true);
135144
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
136-
let searchIndex = -1;
145+
const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
137146

147+
let resultIndex = -1;
138148
if (searchOptions.regex) {
139149
const searchRegex = RegExp(searchTerm, 'g');
140-
const foundTerm = searchRegex.exec(searchStringLine);
141-
if (foundTerm && foundTerm[0].length > 0) {
142-
searchIndex = searchRegex.lastIndex - foundTerm[0].length;
143-
term = foundTerm[0];
150+
let foundTerm: RegExpExecArray;
151+
if (searchOptions.reverseSearch) {
152+
while (foundTerm = searchRegex.exec(searchStringLine.slice(0, searchIndex.col))) {
153+
resultIndex = searchRegex.lastIndex - foundTerm[0].length;
154+
term = foundTerm[0];
155+
searchRegex.lastIndex -= (term.length - 1);
156+
}
157+
} else {
158+
foundTerm = searchRegex.exec(searchStringLine.slice(searchIndex.col));
159+
if (foundTerm && foundTerm[0].length > 0) {
160+
resultIndex = searchIndex.col + (searchRegex.lastIndex - foundTerm[0].length);
161+
term = foundTerm[0];
162+
}
144163
}
145164
} else {
146-
searchIndex = searchStringLine.indexOf(searchTerm);
165+
if (searchOptions.reverseSearch) {
166+
resultIndex = searchStringLine.lastIndexOf(searchTerm, searchIndex.col - searchTerm.length);
167+
} else {
168+
resultIndex = searchStringLine.indexOf(searchTerm, searchIndex.col);
169+
}
147170
}
148171

149-
if (searchIndex >= 0) {
172+
if (resultIndex >= 0) {
150173
// Adjust the row number and search index if needed since a "line" of text can span multiple rows
151-
if (searchIndex >= this._terminal.cols) {
152-
y += Math.floor(searchIndex / this._terminal.cols);
153-
searchIndex = searchIndex % this._terminal.cols;
174+
if (resultIndex >= this._terminal.cols) {
175+
searchIndex.row += Math.floor(resultIndex / this._terminal.cols);
176+
resultIndex = resultIndex % this._terminal.cols;
154177
}
155-
if (searchOptions.wholeWord && !this._isWholeWord(searchIndex, searchStringLine, term)) {
178+
if (searchOptions.wholeWord && !this._isWholeWord(resultIndex, searchStringLine, term)) {
156179
return;
157180
}
158181

159-
const line = this._terminal._core.buffer.lines.get(y);
182+
const line = this._terminal._core.buffer.lines.get(searchIndex.row);
160183

161-
for (let i = 0; i < searchIndex; i++) {
184+
for (let i = 0; i < resultIndex; i++) {
162185
const charData = line.get(i);
163186
// Adjust the searchIndex to normalize emoji into single chars
164187
const char = charData[1/*CHAR_DATA_CHAR_INDEX*/];
165188
if (char.length > 1) {
166-
searchIndex -= char.length - 1;
189+
resultIndex -= char.length - 1;
167190
}
168191
// Adjust the searchIndex for empty characters following wide unicode
169192
// chars (eg. CJK)
170193
const charWidth = charData[2/*CHAR_DATA_WIDTH_INDEX*/];
171194
if (charWidth === 0) {
172-
searchIndex++;
195+
resultIndex++;
173196
}
174197
}
175198
return {
176199
term,
177-
col: searchIndex,
178-
row: y
200+
col: resultIndex,
201+
row: searchIndex.row
179202
};
180203
}
181204
}
182-
183205
/**
184206
* Translates a buffer line to a string, including subsequent lines if they are wraps.
185207
* Wide characters will count as two columns in the resulting string. This

src/addons/search/search.test.ts

Lines changed: 67 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ declare var require: any;
77
import { assert, expect } from 'chai';
88
import * as search from './search';
99
import { SearchHelper } from './SearchHelper';
10-
import { ISearchOptions, ISearchResult } from './Interfaces';
10+
import { ISearchOptions, ISearchResult, ISearchIndex } from './Interfaces';
1111

1212
class MockTerminalPlain {}
1313

@@ -29,8 +29,11 @@ class MockTerminal {
2929
}
3030

3131
class TestSearchHelper extends SearchHelper {
32-
public findInLine(term: string, y: number, searchOptions?: ISearchOptions): ISearchResult {
33-
return this._findInLine(term, y, searchOptions);
32+
public findInLine(term: string, rowNumber: number, searchOptions?: ISearchOptions): ISearchResult {
33+
return this._findInLine(term, {row: rowNumber, col: 0}, searchOptions);
34+
}
35+
public findFromIndex(term: string, searchIndex: ISearchIndex, searchOptions?: ISearchOptions): ISearchResult {
36+
return this._findInLine(term, searchIndex, searchOptions);
3437
}
3538
}
3639

@@ -247,5 +250,66 @@ describe('search addon', () => {
247250
expect(hello4).eql(undefined);
248251
expect(hello5).eql(undefined);
249252
});
253+
it('should find multiple matches in line', function(): void {
254+
search.apply(<any>MockTerminal);
255+
const term = new MockTerminal({cols: 20, rows: 5});
256+
term.core.write('helloooo helloooo\r\naaaAAaaAAA');
257+
term.pushWriteData();
258+
const searchOptions = {
259+
regex: false,
260+
wholeWord: false,
261+
caseSensitive: false
262+
};
263+
const find0 = term.searchHelper.findFromIndex('hello', {row: 0, col: 0}, searchOptions);
264+
const find1 = term.searchHelper.findFromIndex('hello', {row: 0, col: find0.col + find0.term.length}, searchOptions);
265+
const find2 = term.searchHelper.findFromIndex('aaaa', {row: 1, col: 0}, searchOptions);
266+
const find3 = term.searchHelper.findFromIndex('aaaa', {row: 1, col: find2.col + find2.term.length}, searchOptions);
267+
const find4 = term.searchHelper.findFromIndex('aaaa', {row: 1, col: find3.col + find3.term.length}, searchOptions);
268+
expect(find0).eql({col: 0, row: 0, term: 'hello'});
269+
expect(find1).eql({col: 9, row: 0, term: 'hello'});
270+
expect(find2).eql({col: 0, row: 1, term: 'aaaa'});
271+
expect(find3).eql({col: 4, row: 1, term: 'aaaa'});
272+
expect(find4).eql(undefined);
273+
});
274+
it('should find multiple matches in line - reverse search', function(): void {
275+
search.apply(<any>MockTerminal);
276+
const term = new MockTerminal({cols: 20, rows: 5});
277+
term.core.write('it is what it is');
278+
term.pushWriteData();
279+
const searchOptions = {
280+
regex: false,
281+
wholeWord: false,
282+
caseSensitive: false,
283+
reverseSearch: true
284+
};
285+
const find0 = term.searchHelper.findFromIndex('is', {row: 0, col: 16}, searchOptions);
286+
const find1 = term.searchHelper.findFromIndex('is', {row: 0, col: find0.col}, searchOptions);
287+
const find2 = term.searchHelper.findFromIndex('it', {row: 0, col: 16}, searchOptions);
288+
const find3 = term.searchHelper.findFromIndex('it', {row: 0, col: find2.col}, searchOptions);
289+
expect(find0).eql({col: 14, row: 0, term: 'is'});
290+
expect(find1).eql({col: 3, row: 0, term: 'is'});
291+
expect(find2).eql({col: 11, row: 0, term: 'it'});
292+
expect(find3).eql({col: 0, row: 0, term: 'it'});
293+
});
294+
it('should find multiple matches in line - reverse search with regex', function(): void {
295+
search.apply(<any>MockTerminal);
296+
const term = new MockTerminal({cols: 20, rows: 5});
297+
term.core.write('zzzABCzzzzABCABC');
298+
term.pushWriteData();
299+
const searchOptions = {
300+
regex: true,
301+
wholeWord: false,
302+
caseSensitive: true,
303+
reverseSearch: true
304+
};
305+
const find0 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: 16}, searchOptions);
306+
const find1 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find0.col}, searchOptions);
307+
const find2 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find1.col}, searchOptions);
308+
const find3 = term.searchHelper.findFromIndex('[A-Z]{3}', {row: 0, col: find2.col}, searchOptions);
309+
expect(find0).eql({col: 13, row: 0, term: 'ABC'});
310+
expect(find1).eql({col: 10, row: 0, term: 'ABC'});
311+
expect(find2).eql({col: 3, row: 0, term: 'ABC'});
312+
expect(find3).eql(undefined);
313+
});
250314
});
251315
});

0 commit comments

Comments
 (0)