diff --git a/demo/index.html b/demo/index.html index fff77d0989..f21a55d83e 100644 --- a/demo/index.html +++ b/demo/index.html @@ -16,6 +16,7 @@

Actions

+

diff --git a/demo/main.js b/demo/main.js index 70544e38a2..93dda37f77 100644 --- a/demo/main.js +++ b/demo/main.js @@ -83,13 +83,13 @@ function createTerminal() { addDomListener(actionElements.findNext, 'keypress', function (e) { if (e.key === "Enter") { e.preventDefault(); - term.findNext(actionElements.findNext.value); + term.findNext(actionElements.findNext.value, document.getElementById('whole-word').checked); } }); addDomListener(actionElements.findPrevious, 'keypress', function (e) { if (e.key === "Enter") { e.preventDefault(); - term.findPrevious(actionElements.findPrevious.value); + term.findPrevious(actionElements.findPrevious.value, document.getElementById('whole-word').checked); } }); diff --git a/src/addons/search/Interfaces.ts b/src/addons/search/Interfaces.ts index 926e3f36a3..1416158e9f 100644 --- a/src/addons/search/Interfaces.ts +++ b/src/addons/search/Interfaces.ts @@ -17,6 +17,6 @@ export interface ISearchAddonTerminal extends Terminal { } export interface ISearchHelper { - findNext(term: string): boolean; - findPrevious(term: string): boolean; + findNext(term: string, wholeWord: boolean): boolean; + findPrevious(term: string, wholeWord: boolean): boolean; } diff --git a/src/addons/search/SearchHelper.ts b/src/addons/search/SearchHelper.ts index 9b5c06f3be..e0db0e0718 100644 --- a/src/addons/search/SearchHelper.ts +++ b/src/addons/search/SearchHelper.ts @@ -4,6 +4,7 @@ */ import { ISearchHelper, ISearchAddonTerminal } from './Interfaces'; +const nonWordCharacters = ' ~!@#$%^&*()_+`-=[]{}|\;:"\',./<>?'; interface ISearchResult { term: string; @@ -28,7 +29,7 @@ export class SearchHelper implements ISearchHelper { * @param term Tne search term. * @return Whether a result was found. */ - public findNext(term: string): boolean { + public findNext(term: string, wholeWord: boolean = false): boolean { if (!term || term.length === 0) { return false; } @@ -43,7 +44,7 @@ export class SearchHelper implements ISearchHelper { // 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); + result = this._findInLine(term, y, wholeWord); if (result) { break; } @@ -52,7 +53,7 @@ export class SearchHelper implements ISearchHelper { // Search from the top to the current ydisp if (!result) { for (let y = 0; y < startRow; y++) { - result = this._findInLine(term, y); + result = this._findInLine(term, y, wholeWord); if (result) { break; } @@ -69,7 +70,7 @@ export class SearchHelper implements ISearchHelper { * @param term Tne search term. * @return Whether a result was found. */ - public findPrevious(term: string): boolean { + public findPrevious(term: string, wholeWord: boolean = false): boolean { if (!term || term.length === 0) { return false; } @@ -84,7 +85,7 @@ export class SearchHelper implements ISearchHelper { // Search from ydisp + 1 to end for (let y = startRow - 1; y >= 0; y--) { - result = this._findInLine(term, y); + result = this._findInLine(term, y, wholeWord); if (result) { break; } @@ -93,7 +94,7 @@ export class SearchHelper implements ISearchHelper { // 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); + result = this._findInLine(term, y, wholeWord); if (result) { break; } @@ -104,17 +105,32 @@ export class SearchHelper implements ISearchHelper { return this._selectResult(result); } + /** + * A found substring is a whole word if it doesn't have an alphanumeric character directly adjacent to it. + * @param searchIndex starting indext of the potential whole word substring + * @param line entire string in which the potential whole word was found + * @param term the substring that starts at searchIndex + */ + private _isWholeWord(searchIndex: number, line: string, term: string): boolean { + return (((searchIndex === 0) || (nonWordCharacters.indexOf(line[searchIndex - 1]) !== -1)) && + (((searchIndex + term.length) === line.length) || (nonWordCharacters.indexOf(line[searchIndex + term.length]) !== -1))); + } + /** * Searches a line for a search term. * @param term Tne search term. * @param y The line to search. * @return The search result if it was found. */ - private _findInLine(term: string, y: number): ISearchResult { + private _findInLine(term: string, y: number, wholeWord: boolean): ISearchResult { const lowerStringLine = this._terminal._core.buffer.translateBufferLineToString(y, true).toLowerCase(); const lowerTerm = term.toLowerCase(); let searchIndex = lowerStringLine.indexOf(lowerTerm); if (searchIndex >= 0) { + if (wholeWord && !this._isWholeWord(searchIndex, lowerStringLine, term)) { + return; + } + const line = this._terminal._core.buffer.lines.get(y); for (let i = 0; i < searchIndex; i++) { const charData = line.get(i); diff --git a/src/addons/search/search.test.ts b/src/addons/search/search.test.ts index 91ecc4efbd..18ec59caaa 100644 --- a/src/addons/search/search.test.ts +++ b/src/addons/search/search.test.ts @@ -46,4 +46,20 @@ describe('search addon', function(): void { expect(hello1).eql(undefined); expect(hello2).eql({col: 11, row: 2, term: 'Hello'}); }); + it('should respect whole-word search option', function(): void { + search.apply(MockTerminal); + const term = new MockTerminal({cols: 20, rows: 5}); + term.core.write('Hello World\r\nWorld Hello\r\nWorldHelloWorld\r\nHelloWorld\r\nWorldHello'); + term.pushWriteData(); + const hello0 = (term.searchHelper as any)._findInLine('Hello', 0, true); + const hello1 = (term.searchHelper as any)._findInLine('Hello', 1, true); + const hello2 = (term.searchHelper as any)._findInLine('Hello', 2, true); + const hello3 = (term.searchHelper as any)._findInLine('Hello', 3, true); + const hello4 = (term.searchHelper as any)._findInLine('Hello', 4, true); + expect(hello0).eql({col: 0, row: 0, term: 'Hello'}); + expect(hello1).eql({col: 6, row: 1, term: 'Hello'}); + expect(hello2).eql(undefined); + expect(hello3).eql(undefined); + expect(hello4).eql(undefined); + }); }); diff --git a/src/addons/search/search.ts b/src/addons/search/search.ts index a1dd766ffd..391512768a 100644 --- a/src/addons/search/search.ts +++ b/src/addons/search/search.ts @@ -13,12 +13,12 @@ import { ISearchAddonTerminal } from './Interfaces'; * @param term Tne search term. * @return Whether a result was found. */ -export function findNext(terminal: Terminal, term: string): boolean { +export function findNext(terminal: Terminal, term: string, wholeWord: boolean): boolean { const addonTerminal = terminal; if (!addonTerminal.__searchHelper) { addonTerminal.__searchHelper = new SearchHelper(addonTerminal); } - return addonTerminal.__searchHelper.findNext(term); + return addonTerminal.__searchHelper.findNext(term, wholeWord); } /** @@ -27,20 +27,20 @@ export function findNext(terminal: Terminal, term: string): boolean { * @param term Tne search term. * @return Whether a result was found. */ -export function findPrevious(terminal: Terminal, term: string): boolean { +export function findPrevious(terminal: Terminal, term: string, wholeWord: boolean): boolean { const addonTerminal = terminal; if (!addonTerminal.__searchHelper) { addonTerminal.__searchHelper = new SearchHelper(addonTerminal); } - return addonTerminal.__searchHelper.findPrevious(term); + return addonTerminal.__searchHelper.findPrevious(term, wholeWord); } export function apply(terminalConstructor: typeof Terminal): void { - (terminalConstructor.prototype).findNext = function(term: string): boolean { - return findNext(this, term); + (terminalConstructor.prototype).findNext = function(term: string, wholeWord: boolean): boolean { + return findNext(this, term, wholeWord); }; - (terminalConstructor.prototype).findPrevious = function(term: string): boolean { - return findPrevious(this, term); + (terminalConstructor.prototype).findPrevious = function(term: string, wholeWord: boolean): boolean { + return findPrevious(this, term, wholeWord); }; }