Skip to content

Commit

Permalink
Merge pull request #1826 from nojvek/master
Browse files Browse the repository at this point in the history
 Fix #1660: Search as you type
  • Loading branch information
Tyriar committed Dec 17, 2018
2 parents 4feed2d + 6a5b5c2 commit a53c6f1
Show file tree
Hide file tree
Showing 4 changed files with 93 additions and 42 deletions.
38 changes: 18 additions & 20 deletions demo/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import * as fullscreen from '../lib/addons/fullscreen/fullscreen';
import * as search from '../lib/addons/search/search';
import * as webLinks from '../lib/addons/webLinks/webLinks';
import * as winptyCompat from '../lib/addons/winptyCompat/winptyCompat';
import { ISearchOptions } from '../lib/addons/search/Interfaces';

// Pulling in the module's types relies on the <reference> above, it's looks a
// little weird here as we're importing "this" module
Expand Down Expand Up @@ -50,6 +51,14 @@ function setPadding(): void {
term.fit();
}

function getSearchOptions(): ISearchOptions {
return {
regex: (document.getElementById('regex') as HTMLInputElement).checked,
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
};
}

createTerminal();

const disposeRecreateButtonHandler = () => {
Expand Down Expand Up @@ -97,27 +106,16 @@ function createTerminal(): void {

addDomListener(paddingElement, 'change', setPadding);

addDomListener(actionElements.findNext, 'keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const searchOptions = {
regex: (document.getElementById('regex') as HTMLInputElement).checked,
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
};
term.findNext(actionElements.findNext.value, searchOptions);
}
addDomListener(actionElements.findNext, 'keyup', (e) => {
const searchOptions = getSearchOptions();
searchOptions.incremental = e.key !== `Enter`;
term.findNext(actionElements.findNext.value, searchOptions);
});
addDomListener(actionElements.findPrevious, 'keypress', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
const searchOptions = {
regex: (document.getElementById('regex') as HTMLInputElement).checked,
wholeWord: (document.getElementById('whole-word') as HTMLInputElement).checked,
caseSensitive: (document.getElementById('case-sensitive') as HTMLInputElement).checked
};
term.findPrevious(actionElements.findPrevious.value, searchOptions);
}

addDomListener(actionElements.findPrevious, 'keyup', (e) => {
const searchOptions = getSearchOptions();
searchOptions.incremental = e.key !== `Enter`;
term.findPrevious(actionElements.findPrevious.value, searchOptions);
});

// fit is called within a setTimeout, cols and rows need this.
Expand Down
2 changes: 2 additions & 0 deletions src/addons/search/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ export interface ISearchOptions {
regex?: boolean;
wholeWord?: boolean;
caseSensitive?: boolean;
/** Assume caller implements 'search as you type' where findNext gets called when search input changes */
incremental?: boolean;
}

export interface ISearchResult {
Expand Down
94 changes: 72 additions & 22 deletions src/addons/search/SearchHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,24 @@
*/

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

const NON_WORD_CHARACTERS = ' ~!@#$%^&*()+`-=[]{}|\;:"\',./<>?';
const LINES_CACHE_TIME_TO_LIVE = 15 * 1000; // 15 secs

/**
* A class that knows how to search the terminal and how to display the results.
*/
export class SearchHelper implements ISearchHelper {
/**
* translateBufferLineToStringWithWrap is a fairly expensive call.
* We memoize the calls into an array that has a time based ttl.
* _linesCache is also invalidated when the terminal cursor moves.
*/
private _linesCache: string[] = null;
private _linesCacheTimeoutId = 0;

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
this._destroyLinesCache = this._destroyLinesCache.bind(this);
}

/**
Expand All @@ -24,29 +32,36 @@ export class SearchHelper implements ISearchHelper {
* @return Whether a result was found.
*/
public findNext(term: string, searchOptions?: ISearchOptions): boolean {
const selectionManager = this._terminal._core.selectionManager;
const {incremental} = searchOptions;
let result: ISearchResult;

if (!term || term.length === 0) {
selectionManager.clearSelection();
return false;
}

let result: ISearchResult;

let startRow = this._terminal._core.buffer.ydisp;
if (this._terminal._core.selectionManager.selectionEnd) {

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 = this._terminal._core.selectionManager.selectionEnd[1];
startRow = incremental ? selectionManager.selectionStart[1] : selectionManager.selectionEnd[1];
}
}

// Search from ydisp + 1 to end
for (let y = startRow + 1; y < this._terminal._core.buffer.ybase + this._terminal.rows; y++) {
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);
if (result) {
break;
}
}

// Search from the top to the current ydisp
// Search from the top to the startRow
if (!result) {
for (let y = 0; y < startRow; y++) {
result = this._findInLine(term, y, searchOptions);
Expand All @@ -68,29 +83,35 @@ export class SearchHelper implements ISearchHelper {
* @return Whether a result was found.
*/
public findPrevious(term: string, searchOptions?: ISearchOptions): boolean {
const selectionManager = this._terminal._core.selectionManager;
const {incremental} = searchOptions;
let result: ISearchResult;

if (!term || term.length === 0) {
selectionManager.clearSelection();
return false;
}

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

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

// Search from ydisp + 1 to end
for (let y = startRow - 1; y >= 0; y--) {
this._initLinesCache();

// Search from startRow to top
for (let y = incremental ? startRow : startRow - 1; y >= 0; y--) {
result = this._findInLine(term, y, searchOptions);
if (result) {
break;
}
}

// Search from the top to the current ydisp
// 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);
Expand All @@ -104,15 +125,37 @@ export class SearchHelper implements ISearchHelper {
return this._selectResult(result);
}

/**
* Sets up a line cache with a ttl
*/
private _initLinesCache(): void {
if (!this._linesCache) {
this._linesCache = new Array(this._terminal._core.buffer.length);
this._terminal.on('cursormove', this._destroyLinesCache);
}

window.clearTimeout(this._linesCacheTimeoutId);
this._linesCacheTimeoutId = window.setTimeout(() => this._destroyLinesCache(), LINES_CACHE_TIME_TO_LIVE);
}

private _destroyLinesCache(): void {
this._linesCache = null;
this._terminal.off('cursormove', this._destroyLinesCache);
if (this._linesCacheTimeoutId) {
window.clearTimeout(this._linesCacheTimeoutId);
this._linesCacheTimeoutId = 0;
}
}

/**
* 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)));
return (((searchIndex === 0) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex - 1]) !== -1)) &&
(((searchIndex + term.length) === line.length) || (NON_WORD_CHARACTERS.indexOf(line[searchIndex + term.length]) !== -1)));
}

/**
Expand All @@ -130,7 +173,14 @@ export class SearchHelper implements ISearchHelper {
return;
}

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

const searchStringLine = searchOptions.caseSensitive ? stringLine : stringLine.toLowerCase();
const searchTerm = searchOptions.caseSensitive ? term : term.toLowerCase();
let searchIndex = -1;
Expand Down
1 change: 1 addition & 0 deletions src/addons/search/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
"module": "commonjs",
"target": "es5",
"lib": [
"dom",
"es5"
],
"rootDir": ".",
Expand Down

0 comments on commit a53c6f1

Please sign in to comment.