提交 8a9fce3f 编写于 作者: R Rob Lourens 提交者: GitHub

Merge pull request #22722 from Microsoft/roblou/rgSearch

Add ripgrep support
......@@ -63,6 +63,16 @@
"${workspaceRoot}/out/**/*.js"
]
},
{
"type": "node",
"request": "attach",
"name": "Attach to Search process",
"port": 7890,
"sourceMaps": true,
"outFiles": [
"${workspaceRoot}/out/**/*.js"
]
},
{
"type": "node",
"request": "attach",
......
......@@ -410,9 +410,9 @@
"resolved": "https://registry.npmjs.org/vscode-debugprotocol/-/vscode-debugprotocol-1.17.0.tgz"
},
"vscode-ripgrep": {
"version": "0.0.8",
"from": "vscode-ripgrep@0.0.8",
"resolved": "https://registry.npmjs.org/vscode-ripgrep/-/vscode-ripgrep-0.0.8.tgz"
"version": "0.0.11",
"from": "vscode-ripgrep@0.0.11",
"resolved": "https://registry.npmjs.org/vscode-ripgrep/-/vscode-ripgrep-0.0.11.tgz"
},
"vscode-textmate": {
"version": "3.1.1",
......
declare module 'vscode-ripgrep' {
export const rgPath: string;
}
......@@ -35,6 +35,7 @@ export interface IQueryOptions {
sortByScore?: boolean;
cacheKey?: string;
fileEncoding?: string;
useRipgrep?: boolean;
}
export interface ISearchQuery extends IQueryOptions {
......@@ -127,6 +128,7 @@ export class LineMatch implements ILineMatch {
export interface ISearchConfiguration extends IFilesConfiguration {
search: {
exclude: IExpression;
useRipgrep: boolean;
};
}
......
......@@ -204,6 +204,11 @@ configurationRegistry.registerConfiguration({
]
}
},
'search.useRipgrep': {
'type': 'boolean',
'description': nls.localize('useRipgrep', "Controls whether to use ripgrep in text search"),
'default': false
},
'search.quickOpen.includeSymbols': {
'type': 'boolean',
'description': nls.localize('search.quickOpen.includeSymbols', "Configure to include results from a global symbol search in the file results for Quick Open."),
......
......@@ -990,9 +990,12 @@ export class SearchViewlet extends Viewlet {
// Progress total is 100.0% for more progress bar granularity
let progressTotal = 1000;
let progressRunner = this.progressService.show(progressTotal);
let progressWorked = 0;
let progressRunner = query.useRipgrep ?
this.progressService.show(/*infinite=*/true) :
this.progressService.show(progressTotal);
this.loading = true;
this.searchWidget.searchInput.clearMessage();
this.showEmptyStage();
......@@ -1017,7 +1020,7 @@ export class SearchViewlet extends Viewlet {
isDone = true;
// Complete up to 100% as needed
if (completed) {
if (completed && !query.useRipgrep) {
progressRunner.worked(progressTotal - progressWorked);
setTimeout(() => progressRunner.done(), 200);
} else {
......@@ -1146,26 +1149,28 @@ export class SearchViewlet extends Viewlet {
return;
}
// Progress bar update
let fakeProgress = true;
if (total > 0 && worked > 0) {
let ratio = Math.round((worked / total) * progressTotal);
if (ratio > progressWorked) { // never show less progress than what we have already
progressRunner.worked(ratio - progressWorked);
progressWorked = ratio;
fakeProgress = false;
if (!query.useRipgrep) {
// Progress bar update
let fakeProgress = true;
if (total > 0 && worked > 0) {
let ratio = Math.round((worked / total) * progressTotal);
if (ratio > progressWorked) { // never show less progress than what we have already
progressRunner.worked(ratio - progressWorked);
progressWorked = ratio;
fakeProgress = false;
}
}
}
// Fake progress up to 90%, or when actual progress beats it
const fakeMax = 900;
const fakeMultiplier = 12;
if (fakeProgress && progressWorked < fakeMax) {
// Linearly decrease the rate of fake progress.
// 1 is the smallest allowed amount of progress.
const fakeAmt = Math.round((fakeMax - progressWorked) / fakeMax * fakeMultiplier) || 1;
progressWorked += fakeAmt;
progressRunner.worked(fakeAmt);
// Fake progress up to 90%, or when actual progress beats it
const fakeMax = 900;
const fakeMultiplier = 12;
if (fakeProgress && progressWorked < fakeMax) {
// Linearly decrease the rate of fake progress.
// 1 is the smallest allowed amount of progress.
const fakeAmt = Math.round((fakeMax - progressWorked) / fakeMax * fakeMultiplier) || 1;
progressWorked += fakeAmt;
progressRunner.worked(fakeAmt);
}
}
// Search result tree update
......@@ -1188,7 +1193,7 @@ export class SearchViewlet extends Viewlet {
this.searchWidget.setReplaceAllActionState(false);
// this.replaceService.disposeAllReplacePreviews();
this.viewModel.search(query).done(onComplete, onError, onProgress);
this.viewModel.search(query).done(onComplete, onError, query.useRipgrep ? undefined : onProgress);
}
private updateSearchResultCount(): void {
......
......@@ -86,6 +86,7 @@ export interface IWorkbenchSearchConfiguration extends ISearchConfiguration {
quickOpen: {
includeSymbols: boolean;
},
exclude: glob.IExpression;
exclude: glob.IExpression,
useRipgrep: boolean
};
}
\ No newline at end of file
......@@ -42,7 +42,8 @@ export class QueryBuilder {
sortByScore: options.sortByScore,
cacheKey: options.cacheKey,
fileEncoding: options.fileEncoding,
contentPattern: contentPattern
contentPattern: contentPattern,
useRipgrep: configuration.search.useRipgrep
};
}
}
\ No newline at end of file
......@@ -17,8 +17,9 @@ import objects = require('vs/base/common/objects');
import scorer = require('vs/base/common/scorer');
import strings = require('vs/base/common/strings');
import { PPromise, TPromise } from 'vs/base/common/winjs.base';
import { MAX_FILE_SIZE } from 'vs/platform/files/common/files';
import { FileWalker, Engine as FileSearchEngine } from 'vs/workbench/services/search/node/fileSearch';
import { MAX_FILE_SIZE } from 'vs/platform/files/common/files';
import { RipgrepEngine } from 'vs/workbench/services/search/node/ripgrepTextSearch';
import { Engine as TextSearchEngine } from 'vs/workbench/services/search/node/textSearch';
import { TextSearchWorkerProvider } from 'vs/workbench/services/search/node/textSearchWorkerProvider';
import { IRawSearchService, IRawSearch, IRawFileMatch, ISerializedFileMatch, ISerializedSearchProgressItem, ISerializedSearchComplete, ISearchEngine } from './search';
......@@ -39,6 +40,37 @@ export class SearchService implements IRawSearchService {
}
public textSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
return config.useRipgrep ?
this.ripgrepTextSearch(config) :
this.legacyTextSearch(config);
}
public ripgrepTextSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
config.maxFilesize = MAX_FILE_SIZE;
let engine = new RipgrepEngine(config);
return new PPromise<ISerializedSearchComplete, IRawProgressItem<ISerializedFileMatch>>((c, e, p) => {
// Use BatchedCollector to get new results to the frontend every 2s at least, until 50 results have been returned
const collector = new BatchedCollector<ISerializedFileMatch>(SearchService.BATCH_SIZE, p);
engine.search((match) => {
collector.addItem(match, match.numMatches);
}, (progress) => {
p(progress);
}, (error, stats) => {
collector.flush();
if (error) {
e(error);
} else {
c(stats);
}
});
}, () => {
engine.cancel();
});
}
public legacyTextSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
if (!this.textSearchWorkerProvider) {
this.textSearchWorkerProvider = new TextSearchWorkerProvider();
}
......@@ -399,6 +431,18 @@ class BatchedCollector<T> {
constructor(private maxBatchSize: number, private cb: (items: T | T[]) => void) {
}
addItem(item: T, size: number): void {
if (!item) {
return;
}
if (this.maxBatchSize > 0) {
this.addItemToBatch(item, size);
} else {
this.cb(item);
}
}
addItems(items: T[], size: number): void {
if (!items) {
return;
......@@ -411,9 +455,19 @@ class BatchedCollector<T> {
}
}
private addItemsToBatch(items: T[], size: number): void {
this.batch = this.batch.concat(items);
private addItemToBatch(item: T, size: number): void {
this.batch.push(item);
this.batchSize += size;
this.onUpdate();
}
private addItemsToBatch(item: T[], size: number): void {
this.batch = this.batch.concat(item);
this.batchSize += size;
this.onUpdate();
}
private onUpdate(): void {
if (this.totalNumberCompleted < BatchedCollector.START_BATCH_AFTER_COUNT) {
// Flush because we aren't batching yet
this.flush();
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
'use strict';
import { EventEmitter } from 'events';
import * as cp from 'child_process';
import { rgPath } from 'vscode-ripgrep';
import * as strings from 'vs/base/common/strings';
import * as glob from 'vs/base/common/glob';
import { ILineMatch, IProgress } from 'vs/platform/search/common/search';
import { ISerializedFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine } from './search';
export class RipgrepEngine implements ISearchEngine<ISerializedFileMatch> {
private isDone = false;
private rgProc: cp.ChildProcess;
private postProcessExclusions: glob.SiblingClause[];
private ripgrepParser: RipgrepParser;
constructor(private config: IRawSearch) {
}
cancel(): void {
this.isDone = true;
this.ripgrepParser.cancel();
this.rgProc.kill();
}
// TODO@Rob - make promise-based once the old search is gone, and I don't need them to have matching interfaces anymore
search(onResult: (match: ISerializedFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
if (this.config.rootFolders.length) {
this.searchFolder(this.config.rootFolders[0], onResult, onProgress, done);
} else {
done(null, {
limitHit: false,
stats: null
});
}
}
private searchFolder(rootFolder: string, onResult: (match: ISerializedFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
const rgArgs = getRgArgs(this.config, rootFolder);
this.postProcessExclusions = rgArgs.siblingClauses;
// console.log(`rg ${rgArgs.join(' ')}, cwd: ${rootFolder}`);
this.rgProc = cp.spawn(rgPath, rgArgs.args, { cwd: rootFolder });
this.ripgrepParser = new RipgrepParser(this.config.maxResults);
this.ripgrepParser.on('result', onResult);
this.ripgrepParser.on('hitLimit', () => {
this.cancel();
done(null, {
limitHit: true,
stats: null
});
});
this.rgProc.stdout.on('data', data => {
this.ripgrepParser.handleData(data);
});
this.rgProc.stderr.on('data', data => {
// TODO@rob remove console.logs
console.log('stderr:');
console.log(data.toString());
});
this.rgProc.on('close', code => {
this.rgProc = null;
// console.log(`closed with ${code}`);
if (!this.isDone) {
this.isDone = true;
done(null, {
limitHit: false,
stats: null
});
}
});
}
}
export class RipgrepParser extends EventEmitter {
private static RESULT_REGEX = /^\u001b\[m(\d+)\u001b\[m:(.*)$/;
private static FILE_REGEX = /^\u001b\[m(.+)\u001b\[m$/;
private static MATCH_START_MARKER = '\u001b[m\u001b[31m';
private static MATCH_END_MARKER = '\u001b[m';
private fileMatch: FileMatch;
private remainder: string;
private isDone: boolean;
private numResults = 0;
constructor(private maxResults: number) {
super();
}
public cancel(): void {
this.isDone = true;
}
public handleData(data: string | Buffer): void {
// If the previous data chunk didn't end in a newline, append it to this chunk
const dataStr = this.remainder ?
this.remainder + data.toString() :
data.toString();
const dataLines: string[] = dataStr.split(/\r\n|\n/);
this.remainder = dataLines[dataLines.length - 1] ? dataLines.pop() : null;
for (let l = 0; l < dataLines.length; l++) {
const outputLine = dataLines[l].trim();
if (this.isDone) {
break;
}
let r: RegExpMatchArray;
if (!outputLine) {
if (this.fileMatch) {
this.onResult();
}
} else if (r = outputLine.match(RipgrepParser.RESULT_REGEX)) {
// Line is a result - add to collected results for the current file path
this.handleMatchLine(outputLine, parseInt(r[1]) - 1, r[2]);
} else if (r = outputLine.match(RipgrepParser.FILE_REGEX)) {
// Line is a file path - send all collected results for the previous file path
if (this.fileMatch) {
// TODO@Rob Check fileMatch against other exclude globs
this.onResult();
}
this.fileMatch = new FileMatch(r[1]);
} else {
// Line is malformed
}
}
}
private handleMatchLine(outputLine: string, lineNum: number, text: string): void {
const lineMatch = new LineMatch(text, lineNum);
this.fileMatch.addMatch(lineMatch);
let lastMatchEndPos = 0;
let matchTextStartPos = -1;
// Track positions with color codes subtracted - offsets in the final text preview result
let matchTextStartRealIdx = -1;
let textRealIdx = 0;
const realTextParts: string[] = [];
// todo@Rob Consider just rewriting with a regex. I think perf will be fine.
for (let i = 0; i < text.length - (RipgrepParser.MATCH_END_MARKER.length - 1);) {
if (text.substr(i, RipgrepParser.MATCH_START_MARKER.length) === RipgrepParser.MATCH_START_MARKER) {
// Match start
const chunk = text.slice(lastMatchEndPos, i);
realTextParts.push(chunk);
i += RipgrepParser.MATCH_START_MARKER.length;
matchTextStartPos = i;
matchTextStartRealIdx = textRealIdx;
} else if (text.substr(i, RipgrepParser.MATCH_END_MARKER.length) === RipgrepParser.MATCH_END_MARKER) {
// Match end
const chunk = text.slice(matchTextStartPos, i);
realTextParts.push(chunk);
lineMatch.addMatch(matchTextStartRealIdx, textRealIdx - matchTextStartRealIdx);
matchTextStartPos = -1;
matchTextStartRealIdx = -1;
i += RipgrepParser.MATCH_END_MARKER.length;
lastMatchEndPos = i;
this.numResults++;
// Check hit maxResults limit
if (this.numResults >= this.maxResults) {
// Replace preview with what we have so far, TODO@Rob
lineMatch.preview = realTextParts.join('');
this.cancel();
this.onResult();
this.emit('hitLimit');
}
} else {
i++;
textRealIdx++;
}
}
const chunk = text.slice(lastMatchEndPos);
realTextParts.push(chunk);
// Replace preview with version without color codes
const preview = realTextParts.join('');
lineMatch.preview = preview;
}
private onResult(): void {
this.emit('result', this.fileMatch.serialize());
this.fileMatch = null;
}
}
export class FileMatch implements ISerializedFileMatch {
path: string;
lineMatches: LineMatch[];
constructor(path: string) {
this.path = path;
this.lineMatches = [];
}
addMatch(lineMatch: LineMatch): void {
this.lineMatches.push(lineMatch);
}
isEmpty(): boolean {
return this.lineMatches.length === 0;
}
serialize(): ISerializedFileMatch {
let lineMatches: ILineMatch[] = [];
let numMatches = 0;
for (let i = 0; i < this.lineMatches.length; i++) {
numMatches += this.lineMatches[i].offsetAndLengths.length;
lineMatches.push(this.lineMatches[i].serialize());
}
return {
path: this.path,
lineMatches,
numMatches
};
}
}
export class LineMatch implements ILineMatch {
preview: string;
lineNumber: number;
offsetAndLengths: number[][];
constructor(preview: string, lineNumber: number) {
this.preview = preview.replace(/(\r|\n)*$/, '');
this.lineNumber = lineNumber;
this.offsetAndLengths = [];
}
getText(): string {
return this.preview;
}
getLineNumber(): number {
return this.lineNumber;
}
addMatch(offset: number, length: number): void {
this.offsetAndLengths.push([offset, length]);
}
serialize(): ILineMatch {
const result = {
preview: this.preview,
lineNumber: this.lineNumber,
offsetAndLengths: this.offsetAndLengths
};
return result;
}
}
function globExprsToRgGlobs(patterns: glob.IExpression): { globArgs: string[], siblingClauses: glob.SiblingClause[] } {
const globArgs: string[] = [];
const siblingClauses: glob.SiblingClause[] = [];
Object.keys(patterns)
.forEach(key => {
const value = patterns[key];
if (typeof value === 'boolean' && value) {
// globs added to ripgrep don't match from the root by default, so add a /
if (key.charAt(0) !== '*') {
key = '/' + key;
}
globArgs.push(key);
} else if (value && value.when) {
siblingClauses.push(value);
}
});
return { globArgs, siblingClauses };
}
function getRgArgs(config: IRawSearch, rootFolder: string): { args: string[], siblingClauses: glob.SiblingClause[] } {
const args = ['--heading', '--line-number', '--color', 'ansi', '--colors', 'path:none', '--colors', 'line:none', '--colors', 'match:fg:red', '--colors', 'match:style:nobold'];
args.push(config.contentPattern.isCaseSensitive ? '--case-sensitive' : '--ignore-case');
if (config.includePattern) {
// I don't think includePattern can have siblingClauses
globExprsToRgGlobs(config.includePattern).globArgs.forEach(globArg => {
args.push('-g', globArg);
});
}
let siblingClauses: glob.SiblingClause[] = [];
if (config.excludePattern) {
const rgGlobs = globExprsToRgGlobs(config.excludePattern);
rgGlobs.globArgs
.forEach(rgGlob => args.push('-g', `!${rgGlob}`));
siblingClauses = rgGlobs.siblingClauses;
}
if (config.maxFilesize) {
args.push('--max-filesize', config.maxFilesize + '');
}
if (config.contentPattern.isRegExp) {
if (config.contentPattern.isWordMatch) {
args.push('--word-regexp');
}
args.push('--regexp', config.contentPattern.pattern);
} else {
if (config.contentPattern.isWordMatch) {
args.push('--word-regexp', '--regexp', strings.escapeRegExpCharacters(config.contentPattern.pattern));
} else {
args.push('--fixed-strings', config.contentPattern.pattern);
}
}
// Folder to search
args.push('--', rootFolder);
return { args, siblingClauses };
}
......@@ -21,6 +21,7 @@ export interface IRawSearch {
cacheKey?: string;
maxFilesize?: number;
fileEncoding?: string;
useRipgrep?: boolean;
}
export interface IRawSearchService {
......
......@@ -234,7 +234,8 @@ export class DiskSearch {
includePattern: query.includePattern,
maxResults: query.maxResults,
sortByScore: query.sortByScore,
cacheKey: query.cacheKey
cacheKey: query.cacheKey,
useRipgrep: query.useRipgrep
};
if (query.type === QueryType.Text) {
......
......@@ -8,28 +8,15 @@
import path = require('path');
import assert = require('assert');
import { LineMatch } from 'vs/platform/search/common/search';
import { TPromise } from 'vs/base/common/winjs.base';
import { FileWalker } from 'vs/workbench/services/search/node/fileSearch';
import { ISerializedFileMatch } from 'vs/workbench/services/search/node/search';
import { ISerializedFileMatch, IRawSearch } from 'vs/workbench/services/search/node/search';
import { Engine as TextSearchEngine } from 'vs/workbench/services/search/node/textSearch';
import { RipgrepEngine } from 'vs/workbench/services/search/node/ripgrepTextSearch';
import { TextSearchWorkerProvider } from 'vs/workbench/services/search/node/textSearchWorkerProvider';
function countAll(matches: ISerializedFileMatch[]): number {
return matches.reduce((acc, m) => acc + count(m.lineMatches), 0);
}
function count(lineMatches: LineMatch[]): number {
let count = 0;
if (lineMatches) {
for (let i = 0; i < lineMatches.length; i++) {
let line = lineMatches[i];
let wordMatches = line.offsetAndLengths;
count += wordMatches.length;
}
}
return count;
return matches.reduce((acc, m) => acc + m.numMatches, 0);
}
function rootfolders() {
......@@ -37,114 +24,103 @@ function rootfolders() {
}
const textSearchWorkerProvider = new TextSearchWorkerProvider();
suite('Search-integration', () => {
test('Text: GameOfLife', function (done: () => void) {
let c = 0;
let config = {
rootFolders: rootfolders(),
filePattern: '*.js',
contentPattern: { pattern: 'GameOfLife', modifiers: 'i' }
};
function doLegacySearchTest(config: IRawSearch, expectedResultCount: number | Function): TPromise<void> {
return new TPromise<void>(resolve => {
let engine = new TextSearchEngine(config, new FileWalker(config), textSearchWorkerProvider);
let c = 0;
engine.search((result) => {
if (result) {
c += countAll(result);
}
}, () => { }, (error) => {
assert.ok(!error);
assert.equal(c, 4);
done();
if (typeof expectedResultCount === 'function') {
assert(expectedResultCount(c));
} else {
assert.equal(c, expectedResultCount);
}
resolve(undefined);
});
});
}
test('Text: GameOfLife (RegExp)', function (done: () => void) {
let c = 0;
let config = {
rootFolders: rootfolders(),
filePattern: '*.js',
contentPattern: { pattern: 'Game.?fL\\w?fe', isRegExp: true }
};
let engine = new TextSearchEngine(config, new FileWalker(config), textSearchWorkerProvider);
function doRipgrepSearchTest(config: IRawSearch, expectedResultCount: number): TPromise<void> {
return new TPromise<void>(resolve => {
let engine = new RipgrepEngine(config);
let c = 0;
engine.search((result) => {
if (result) {
c += countAll(result);
c += result.numMatches;
}
}, () => { }, (error) => {
assert.ok(!error);
assert.equal(c, 4);
done();
assert.equal(c, expectedResultCount);
resolve(undefined);
});
});
}
function doSearchTest(config: IRawSearch, expectedResultCount: number, done) {
return doLegacySearchTest(config, expectedResultCount)
// .then(() => doRipgrepSearchTest(config, expectedResultCount))
.then(done, done);
}
suite('Search-integration', () => {
test('Text: GameOfLife', function (done: () => void) {
let config = {
rootFolders: rootfolders(),
filePattern: '*.js',
contentPattern: { pattern: 'GameOfLife', modifiers: 'i' },
};
doSearchTest(config, 4, done);
});
test('Text: GameOfLife (RegExp)', function (done: () => void) {
let config = {
rootFolders: rootfolders(),
filePattern: '*.js',
contentPattern: { pattern: 'Game.?fL\\w?fe', isRegExp: true }
};
doSearchTest(config, 4, done);
});
test('Text: GameOfLife (Word Match, Case Sensitive)', function (done: () => void) {
let c = 0;
let config = {
rootFolders: rootfolders(),
filePattern: '*.js',
contentPattern: { pattern: 'GameOfLife', isWordMatch: true, isCaseSensitive: true }
};
let engine = new TextSearchEngine(config, new FileWalker(config), textSearchWorkerProvider);
engine.search((result) => {
if (result) {
c += countAll(result);
}
}, () => { }, (error) => {
assert.ok(!error);
assert.equal(c, 4);
done();
});
doSearchTest(config, 4, done);
});
test('Text: Helvetica (UTF 16)', function (done: () => void) {
let c = 0;
let config = {
rootFolders: rootfolders(),
filePattern: '*.css',
contentPattern: { pattern: 'Helvetica', modifiers: 'i' }
};
let engine = new TextSearchEngine(config, new FileWalker(config), textSearchWorkerProvider);
engine.search((result) => {
if (result) {
c += countAll(result);
}
}, () => { }, (error) => {
assert.ok(!error);
assert.equal(c, 3);
done();
});
doSearchTest(config, 3, done);
});
test('Text: e', function (done: () => void) {
let c = 0;
let config = {
rootFolders: rootfolders(),
filePattern: '*.*',
contentPattern: { pattern: 'e', modifiers: 'i' }
};
let engine = new TextSearchEngine(config, new FileWalker(config), textSearchWorkerProvider);
engine.search((result) => {
if (result) {
c += countAll(result);
}
}, (result) => { }, (error) => {
assert.ok(!error);
assert.equal(c, 776);
done();
});
doSearchTest(config, 776, done);
});
test('Text: e (with excludes)', function (done: () => void) {
let c = 0;
let config: any = {
rootFolders: rootfolders(),
filePattern: '*.*',
......@@ -152,21 +128,10 @@ suite('Search-integration', () => {
excludePattern: { '**/examples': true }
};
let engine = new TextSearchEngine(config, new FileWalker(config), textSearchWorkerProvider);
engine.search((result) => {
if (result) {
c += countAll(result);
}
}, (result) => { }, (error) => {
assert.ok(!error);
assert.equal(c, 394);
done();
});
doSearchTest(config, 394, done);
});
test('Text: e (with includes)', function (done: () => void) {
let c = 0;
let config: any = {
rootFolders: rootfolders(),
filePattern: '*.*',
......@@ -174,21 +139,10 @@ suite('Search-integration', () => {
includePattern: { '**/examples/**': true }
};
let engine = new TextSearchEngine(config, new FileWalker(config), textSearchWorkerProvider);
engine.search((result) => {
if (result) {
c += countAll(result);
}
}, (result) => { }, (error) => {
assert.ok(!error);
assert.equal(c, 382);
done();
});
doSearchTest(config, 382, done);
});
test('Text: e (with includes and exclude)', function (done: () => void) {
let c = 0;
let config: any = {
rootFolders: rootfolders(),
filePattern: '*.*',
......@@ -197,62 +151,32 @@ suite('Search-integration', () => {
excludePattern: { '**/examples/small.js': true }
};
let engine = new TextSearchEngine(config, new FileWalker(config), textSearchWorkerProvider);
engine.search((result) => {
if (result) {
c += countAll(result);
}
}, (result) => { }, (error) => {
assert.ok(!error);
assert.equal(c, 361);
done();
});
doSearchTest(config, 361, done);
});
test('Text: a (capped)', function (done: () => void) {
let c = 0;
const maxResults = 520;
let config = {
rootFolders: rootfolders(),
filePattern: '*.*',
contentPattern: { pattern: 'a', modifiers: 'i' },
maxResults: 520
maxResults
};
let engine = new TextSearchEngine(config, new FileWalker(config), textSearchWorkerProvider);
engine.search((result) => {
if (result) {
c += countAll(result);
}
}, (result) => { }, (error) => {
assert.ok(!error);
// Search can go over the maxResults because it doesn't trim the results from its worker processes to the exact max size.
// But the worst-case scenario should be 2*max-1
assert.ok(c < 520 * 2);
done();
});
// (Legacy) search can go over the maxResults because it doesn't trim the results from its worker processes to the exact max size.
// But the worst-case scenario should be 2*max-1
return doLegacySearchTest(config, count => count < maxResults * 2)
.then(() => doRipgrepSearchTest(config, maxResults))
.then(done, done);
});
test('Text: a (no results)', function (done: () => void) {
let c = 0;
let config = {
rootFolders: rootfolders(),
filePattern: '*.*',
contentPattern: { pattern: 'ahsogehtdas', modifiers: 'i' }
};
let engine = new TextSearchEngine(config, new FileWalker(config), textSearchWorkerProvider);
engine.search((result) => {
if (result) {
c += countAll(result);
}
}, (result) => { }, (error) => {
assert.ok(!error);
assert.equal(c, 0);
done();
});
doSearchTest(config, 0, done);
});
});
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册