Merge pull request #1662 from alexr00/alexr00/searchWrapped
Added support for searching accross wrapped lines
This commit is contained in:
+6
-6
@@ -8,12 +8,12 @@
|
|||||||
/// <reference path="../typings/xterm.d.ts"/>
|
/// <reference path="../typings/xterm.d.ts"/>
|
||||||
|
|
||||||
import { Terminal } from '../lib/public/Terminal';
|
import { Terminal } from '../lib/public/Terminal';
|
||||||
import * as attach from '../build/addons/attach/attach';
|
import * as attach from '../lib/addons/attach/attach';
|
||||||
import * as fit from '../build/addons/fit/fit';
|
import * as fit from '../lib/addons/fit/fit';
|
||||||
import * as fullscreen from '../build/addons/fullscreen/fullscreen';
|
import * as fullscreen from '../lib/addons/fullscreen/fullscreen';
|
||||||
import * as search from '../build/addons/search/search';
|
import * as search from '../lib/addons/search/search';
|
||||||
import * as webLinks from '../build/addons/webLinks/webLinks';
|
import * as webLinks from '../lib/addons/webLinks/webLinks';
|
||||||
import * as winptyCompat from '../build/addons/winptyCompat/winptyCompat';
|
import * as winptyCompat from '../lib/addons/winptyCompat/winptyCompat';
|
||||||
|
|
||||||
// Pulling in the module's types relies on the <reference> above, it's looks a
|
// Pulling in the module's types relies on the <reference> above, it's looks a
|
||||||
// little weird here as we're importing "this" module
|
// little weird here as we're importing "this" module
|
||||||
|
|||||||
@@ -100,28 +100,41 @@ export class SearchHelper implements ISearchHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Searches a line for a search term.
|
* Searches a line for a search term. Takes the provided terminal line and searches the text line, which may contain
|
||||||
* @param term The search term.
|
* subsequent terminal lines if the text is wrapped. If the provided line number is part of a wrapped text line that
|
||||||
|
* started on an earlier line then it is skipped since it will be properly searched when the terminal line that the
|
||||||
|
* text starts on is searched.
|
||||||
|
* @param term Tne search term.
|
||||||
* @param y The line to search.
|
* @param y The line to search.
|
||||||
* @param searchOptions Search options.
|
* @param searchOptions Search options.
|
||||||
* @return The search result if it was found.
|
* @return The search result if it was found.
|
||||||
*/
|
*/
|
||||||
protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult {
|
protected _findInLine(term: string, y: number, searchOptions: ISearchOptions = {}): ISearchResult {
|
||||||
const lowerStringLine = this._terminal._core.buffer.translateBufferLineToString(y, true).toLowerCase();
|
if (this._terminal._core.buffer.lines.get(y).isWrapped) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const lowerStringLine = this.translateBufferLineToStringWithWrap(y, true).toLowerCase();
|
||||||
const lowerTerm = term.toLowerCase();
|
const lowerTerm = term.toLowerCase();
|
||||||
let searchIndex = -1;
|
let searchIndex = -1;
|
||||||
if (searchOptions.regex) {
|
if (searchOptions.regex) {
|
||||||
const searchRegex = RegExp(lowerTerm, 'g');
|
const searchRegex = RegExp(lowerTerm, 'g');
|
||||||
const foundTerm = searchRegex.exec(lowerStringLine);
|
const foundTerm = searchRegex.exec(lowerStringLine);
|
||||||
if (foundTerm) {
|
if (foundTerm && foundTerm[0].length > 0) {
|
||||||
searchIndex = searchRegex.lastIndex - foundTerm[0].length;
|
searchIndex = searchRegex.lastIndex - foundTerm[0].length;
|
||||||
term = foundTerm[0];
|
term = foundTerm[0];
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
searchIndex = lowerStringLine.indexOf(lowerTerm);
|
searchIndex = lowerStringLine.indexOf(lowerTerm);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (searchIndex >= 0) {
|
if (searchIndex >= 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;
|
||||||
|
}
|
||||||
const line = this._terminal._core.buffer.lines.get(y);
|
const line = this._terminal._core.buffer.lines.get(y);
|
||||||
|
|
||||||
for (let i = 0; i < searchIndex; i++) {
|
for (let i = 0; i < searchIndex; i++) {
|
||||||
const charData = line.get(i);
|
const charData = line.get(i);
|
||||||
// Adjust the searchIndex to normalize emoji into single chars
|
// Adjust the searchIndex to normalize emoji into single chars
|
||||||
@@ -144,6 +157,28 @@ export class SearchHelper implements ISearchHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* function is useful for getting the actual text underneath the raw selection
|
||||||
|
* position.
|
||||||
|
* @param line The line being translated.
|
||||||
|
* @param trimRight Whether to trim whitespace to the right.
|
||||||
|
*/
|
||||||
|
public translateBufferLineToStringWithWrap(lineIndex: number, trimRight: boolean): string {
|
||||||
|
let lineString = '';
|
||||||
|
let lineWrapsToNext: boolean;
|
||||||
|
|
||||||
|
do {
|
||||||
|
lineString += this._terminal._core.buffer.translateBufferLineToString(lineIndex, true);
|
||||||
|
lineIndex++;
|
||||||
|
const nextLine = this._terminal._core.buffer.lines.get(lineIndex);
|
||||||
|
lineWrapsToNext = nextLine ? this._terminal._core.buffer.lines.get(lineIndex).isWrapped : false;
|
||||||
|
} while (lineWrapsToNext);
|
||||||
|
|
||||||
|
return lineString;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects and scrolls to a result.
|
* Selects and scrolls to a result.
|
||||||
* @param result The result to select.
|
* @param result The result to select.
|
||||||
|
|||||||
@@ -14,9 +14,11 @@ class MockTerminalPlain {}
|
|||||||
class MockTerminal {
|
class MockTerminal {
|
||||||
private _core: any;
|
private _core: any;
|
||||||
public searchHelper: TestSearchHelper;
|
public searchHelper: TestSearchHelper;
|
||||||
|
public cols: number;
|
||||||
constructor(options: any) {
|
constructor(options: any) {
|
||||||
this._core = new (require('../../../lib/Terminal').Terminal)(options);
|
this._core = new (require('../../../lib/Terminal').Terminal)(options);
|
||||||
this.searchHelper = new TestSearchHelper(this as any);
|
this.searchHelper = new TestSearchHelper(this as any);
|
||||||
|
this.cols = options.cols;
|
||||||
}
|
}
|
||||||
get core(): any {
|
get core(): any {
|
||||||
return this._core;
|
return this._core;
|
||||||
@@ -32,7 +34,7 @@ class TestSearchHelper extends SearchHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('search addon', function(): void {
|
describe('search addon', () => {
|
||||||
describe('apply', () => {
|
describe('apply', () => {
|
||||||
it('should register findNext and findPrevious', () => {
|
it('should register findNext and findPrevious', () => {
|
||||||
search.apply(<any>MockTerminalPlain);
|
search.apply(<any>MockTerminalPlain);
|
||||||
@@ -40,45 +42,80 @@ describe('search addon', function(): void {
|
|||||||
assert.equal(typeof (<any>MockTerminalPlain).prototype.findPrevious, 'function');
|
assert.equal(typeof (<any>MockTerminalPlain).prototype.findPrevious, 'function');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('Searchhelper - should find correct position', function(): void {
|
describe('find', () => {
|
||||||
search.apply(<any>MockTerminal);
|
it('Searchhelper - should find correct position', () => {
|
||||||
const term = new MockTerminal({cols: 20, rows: 3});
|
search.apply(<any>MockTerminal);
|
||||||
term.core.write('Hello World\r\ntest\n123....hello');
|
const term = new MockTerminal({cols: 20, rows: 3});
|
||||||
term.pushWriteData();
|
term.core.write('Hello World\r\ntest\n123....hello');
|
||||||
const hello0 = term.searchHelper.findInLine('Hello', 0);
|
term.pushWriteData();
|
||||||
const hello1 = term.searchHelper.findInLine('Hello', 1);
|
const hello0 = term.searchHelper.findInLine('Hello', 0);
|
||||||
const hello2 = term.searchHelper.findInLine('Hello', 2);
|
const hello1 = term.searchHelper.findInLine('Hello', 1);
|
||||||
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
const hello2 = term.searchHelper.findInLine('Hello', 2);
|
||||||
expect(hello1).eql(undefined);
|
expect(hello0).eql({col: 0, row: 0, term: 'Hello'});
|
||||||
expect(hello2).eql({col: 11, row: 2, term: 'Hello'});
|
expect(hello1).eql(undefined);
|
||||||
});
|
expect(hello2).eql({col: 11, row: 2, term: 'Hello'});
|
||||||
it('should respect search regex', function(): void {
|
});
|
||||||
search.apply(<any>MockTerminal);
|
it('should find search term accross line wrap', () => {
|
||||||
const term = new MockTerminal({cols: 10, rows: 4});
|
search.apply(<any>MockTerminal);
|
||||||
term.core.write('abcdefghijklmnopqrstuvwxyz\r\n~/dev ');
|
const term = new MockTerminal({cols: 10, rows: 5});
|
||||||
/*
|
term.core.write('texttextHellotext\r\n');
|
||||||
abcdefghij
|
term.core.write('texttexttextHellotext');
|
||||||
klmnopqrst
|
term.pushWriteData();
|
||||||
uvwxyz
|
/*
|
||||||
~/dev
|
texttextHe
|
||||||
*/
|
llotext
|
||||||
term.pushWriteData();
|
texttextte
|
||||||
const searchOptions = {
|
xtHellotex
|
||||||
regex: true,
|
t
|
||||||
wholeWord: false,
|
*/
|
||||||
caseSensitive: false
|
|
||||||
};
|
const hello0 = (term.searchHelper as any)._findInLine('Hello', 0);
|
||||||
const hello0 = term.searchHelper.findInLine('dee*', 0, searchOptions);
|
const hello1 = (term.searchHelper as any)._findInLine('Hello', 1);
|
||||||
term.searchHelper.findInLine('jkk*', 0, searchOptions);
|
const hello2 = (term.searchHelper as any)._findInLine('Hello', 2);
|
||||||
term.searchHelper.findInLine('mnn*', 1, searchOptions);
|
const hello3 = (term.searchHelper as any)._findInLine('Hello', 3);
|
||||||
const tilda0 = term.searchHelper.findInLine('^~', 3, searchOptions);
|
const llo = (term.searchHelper as any)._findInLine('llo', 1);
|
||||||
const tilda1 = term.searchHelper.findInLine('^[~]', 3, searchOptions);
|
expect(hello0).eql({col: 8, row: 0, term: 'Hello'});
|
||||||
const tilda2 = term.searchHelper.findInLine('^\\~', 3, searchOptions);
|
expect(hello1).eql(undefined);
|
||||||
expect(hello0).eql({col: 3, row: 0, term: 'de'});
|
expect(hello2).eql({col: 2, row: 3, term: 'Hello'});
|
||||||
// TODO: uncomment this test when line wrap search is checked in expect(hello1).eql({col: 9, row: 0, term: 'jk'});
|
expect(hello3).eql(undefined);
|
||||||
// TODO: uncomment this test when line wrap search is checked in expect(hello2).eql(undefined);
|
expect(llo).eql(undefined);
|
||||||
expect(tilda0).eql({col: 0, row: 3, term: '~'});
|
});
|
||||||
expect(tilda1).eql({col: 0, row: 3, term: '~'});
|
it('should respect search regex', () => {
|
||||||
expect(tilda2).eql({col: 0, row: 3, term: '~'});
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 10, rows: 4});
|
||||||
|
term.core.write('abcdefghijklmnopqrstuvwxyz\r\n~/dev ');
|
||||||
|
/*
|
||||||
|
abcdefghij
|
||||||
|
klmnopqrst
|
||||||
|
uvwxyz
|
||||||
|
~/dev
|
||||||
|
*/
|
||||||
|
term.pushWriteData();
|
||||||
|
const searchOptions = {
|
||||||
|
regex: true,
|
||||||
|
wholeWord: false,
|
||||||
|
caseSensitive: false
|
||||||
|
};
|
||||||
|
const hello0 = term.searchHelper.findInLine('dee*', 0, searchOptions);
|
||||||
|
const hello1 = term.searchHelper.findInLine('jkk*', 0, searchOptions);
|
||||||
|
const hello2 = term.searchHelper.findInLine('mnn*', 1, searchOptions);
|
||||||
|
const tilda0 = term.searchHelper.findInLine('^~', 3, searchOptions);
|
||||||
|
const tilda1 = term.searchHelper.findInLine('^[~]', 3, searchOptions);
|
||||||
|
const tilda2 = term.searchHelper.findInLine('^\\~', 3, searchOptions);
|
||||||
|
expect(hello0).eql({col: 3, row: 0, term: 'de'});
|
||||||
|
expect(hello1).eql({col: 9, row: 0, term: 'jk'});
|
||||||
|
expect(hello2).eql(undefined);
|
||||||
|
expect(tilda0).eql({col: 0, row: 3, term: '~'});
|
||||||
|
expect(tilda1).eql({col: 0, row: 3, term: '~'});
|
||||||
|
expect(tilda2).eql({col: 0, row: 3, term: '~'});
|
||||||
|
});
|
||||||
|
it('should not select empty lines', () => {
|
||||||
|
search.apply(<any>MockTerminal);
|
||||||
|
const term = new MockTerminal({cols: 20, rows: 3});
|
||||||
|
term.core.write(' ');
|
||||||
|
term.pushWriteData();
|
||||||
|
const line = term.searchHelper.findInLine('^.*$', 0, { regex: true });
|
||||||
|
expect(line).eql(undefined);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user