#55 Sort and limit file results in search process

This commit is contained in:
Christof Marti 2016-08-04 16:58:36 -07:00
parent 31f8362baf
commit 0f776fe458
16 changed files with 700 additions and 202 deletions

View file

@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import scorer = require('vs/base/common/scorer');
import strings = require('vs/base/common/strings');
const FileNameMatch = /^(.*)\.([^.]*)|([^.]+)$/;
@ -75,4 +76,64 @@ export function compareByPrefix(one: string, other: string, lookFor: string): nu
}
return 0;
}
}
export interface IScorableResourceAccessor<T> {
getLabel(T): string;
getResourcePath(T): string;
}
export function compareByScore<T>(elementA: T, elementB: T, accessor: IScorableResourceAccessor<T>, lookFor: string, lookForNormalizedLower: string, scorerCache?: { [key: string]: number }): number {
const labelA = accessor.getLabel(elementA);
const labelB = accessor.getLabel(elementB);
// treat prefix matches highest in any case
const prefixCompare = compareByPrefix(labelA, labelB, lookFor);
if (prefixCompare) {
return prefixCompare;
}
// Give higher importance to label score
const labelAScore = scorer.score(labelA, lookFor, scorerCache);
const labelBScore = scorer.score(labelB, lookFor, scorerCache);
// Useful for understanding the scoring
// elementA.setPrefix(labelAScore + ' ');
// elementB.setPrefix(labelBScore + ' ');
if (labelAScore !== labelBScore) {
return labelAScore > labelBScore ? -1 : 1;
}
// Score on full resource path comes next (if available)
let resourcePathA = accessor.getResourcePath(elementA);
let resourcePathB = accessor.getResourcePath(elementB);
if (resourcePathA && resourcePathB) {
const resourceAScore = scorer.score(resourcePathA, lookFor, scorerCache);
const resourceBScore = scorer.score(resourcePathB, lookFor, scorerCache);
// Useful for understanding the scoring
// elementA.setPrefix(elementA.getPrefix() + ' ' + resourceAScore + ': ');
// elementB.setPrefix(elementB.getPrefix() + ' ' + resourceBScore + ': ');
if (resourceAScore !== resourceBScore) {
return resourceAScore > resourceBScore ? -1 : 1;
}
}
// At this place, the scores are identical so we check for string lengths and favor shorter ones
if (labelA.length !== labelB.length) {
return labelA.length < labelB.length ? -1 : 1;
}
if (resourcePathA && resourcePathB && resourcePathA.length !== resourcePathB.length) {
return resourcePathA.length < resourcePathB.length ? -1 : 1;
}
// Finally compare by label or resource path
if (labelA === labelB && resourcePathA && resourcePathB) {
return compareAnything(resourcePathA, resourcePathB, lookForNormalizedLower);
}
return compareAnything(labelA, labelB, lookForNormalizedLower);
}

View file

@ -16,12 +16,11 @@ import paths = require('vs/base/common/paths');
import {IQuickNavigateConfiguration, IModel, IDataSource, IFilter, IAccessiblityProvider, IRenderer, IRunner, Mode} from 'vs/base/parts/quickopen/common/quickOpen';
import {IActionProvider} from 'vs/base/parts/tree/browser/actionsRenderer';
import {Action, IAction, IActionRunner} from 'vs/base/common/actions';
import {compareAnything, compareByPrefix} from 'vs/base/common/comparers';
import {compareAnything, compareByScore as doCompareByScore} from 'vs/base/common/comparers';
import {ActionBar, IActionItem} from 'vs/base/browser/ui/actionbar/actionbar';
import {LegacyRenderer, ILegacyTemplateData} from 'vs/base/parts/tree/browser/treeDefaults';
import {HighlightedLabel} from 'vs/base/browser/ui/highlightedlabel/highlightedLabel';
import DOM = require('vs/base/browser/dom');
import scorer = require('vs/base/common/scorer');
export interface IContext {
event: any;
@ -35,6 +34,18 @@ export interface IHighlight {
let IDS = 0;
class EntryAccessor {
public static getLabel(entry: QuickOpenEntry) {
return entry.getLabel();
}
public static getResourcePath(entry: QuickOpenEntry) {
const resource = entry.getResource();
return resource && resource.fsPath;
}
}
export class QuickOpenEntry {
private id: string;
private labelHighlights: IHighlight[];
@ -183,58 +194,7 @@ export class QuickOpenEntry {
}
public static compareByScore(elementA: QuickOpenEntry, elementB: QuickOpenEntry, lookFor: string, lookForNormalizedLower: string, scorerCache?: { [key: string]: number }): number {
const labelA = elementA.getLabel();
const labelB = elementB.getLabel();
// treat prefix matches highest in any case
const prefixCompare = compareByPrefix(labelA, labelB, lookFor);
if (prefixCompare) {
return prefixCompare;
}
// Give higher importance to label score
const labelAScore = scorer.score(labelA, lookFor, scorerCache);
const labelBScore = scorer.score(labelB, lookFor, scorerCache);
// Useful for understanding the scoring
// elementA.setPrefix(labelAScore + ' ');
// elementB.setPrefix(labelBScore + ' ');
if (labelAScore !== labelBScore) {
return labelAScore > labelBScore ? -1 : 1;
}
// Score on full resource path comes next (if available)
let resourceA = elementA.getResource();
let resourceB = elementB.getResource();
if (resourceA && resourceB) {
const resourceAScore = scorer.score(resourceA.fsPath, lookFor, scorerCache);
const resourceBScore = scorer.score(resourceB.fsPath, lookFor, scorerCache);
// Useful for understanding the scoring
// elementA.setPrefix(elementA.getPrefix() + ' ' + resourceAScore + ': ');
// elementB.setPrefix(elementB.getPrefix() + ' ' + resourceBScore + ': ');
if (resourceAScore !== resourceBScore) {
return resourceAScore > resourceBScore ? -1 : 1;
}
}
// At this place, the scores are identical so we check for string lengths and favor shorter ones
if (labelA.length !== labelB.length) {
return labelA.length < labelB.length ? -1 : 1;
}
if (resourceA && resourceB && resourceA.fsPath.length !== resourceB.fsPath.length) {
return resourceA.fsPath.length < resourceB.fsPath.length ? -1 : 1;
}
// Finally compare by label or resource path
if (labelA === labelB && resourceA && resourceB) {
return compareAnything(resourceA.fsPath, resourceB.fsPath, lookForNormalizedLower);
}
return compareAnything(labelA, labelB, lookForNormalizedLower);
return doCompareByScore(elementA, elementB, EntryAccessor, lookFor, lookForNormalizedLower, scorerCache);
}
/**

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import {PPromise} from 'vs/base/common/winjs.base';
import {PPromise, TPromise} from 'vs/base/common/winjs.base';
import uri from 'vs/base/common/uri';
import glob = require('vs/base/common/glob');
import {IFilesConfiguration} from 'vs/platform/files/common/files';
@ -19,6 +19,7 @@ export const ISearchService = createDecorator<ISearchService>(ID);
export interface ISearchService {
_serviceBrand: any;
search(query: ISearchQuery): PPromise<ISearchComplete, ISearchProgressItem>;
clearCache(cacheKey: string): TPromise<void>;
}
export interface IQueryOptions {
@ -28,6 +29,8 @@ export interface IQueryOptions {
excludePattern?: glob.IExpression;
includePattern?: glob.IExpression;
maxResults?: number;
sortByScore?: boolean;
cacheKey?: string;
fileEncoding?: string;
}
@ -75,6 +78,19 @@ export interface ISearchComplete {
}
export interface ISearchStats {
fromCache: boolean;
resultCount: number;
unsortedResultTime?: number;
sortedResultTime?: number;
}
export interface ICachedSearchStats extends ISearchStats {
cacheLookupStartTime: number;
cacheLookupResultTime: number;
cacheEntryCount: number;
}
export interface IUncachedSearchStats extends ISearchStats {
fileWalkStartTime: number;
fileWalkResultTime: number;
directoriesWalked: number;

View file

@ -16,6 +16,7 @@ import scorer = require('vs/base/common/scorer');
import paths = require('vs/base/common/paths');
import labels = require('vs/base/common/labels');
import strings = require('vs/base/common/strings');
import uuid = require('vs/base/common/uuid');
import {IRange} from 'vs/editor/common/editorCommon';
import {IAutoFocus} from 'vs/base/parts/quickopen/common/quickOpen';
import {QuickOpenEntry, QuickOpenModel} from 'vs/base/parts/quickopen/browser/quickOpenModel';
@ -26,35 +27,52 @@ import * as openSymbolHandler from 'vs/workbench/parts/search/browser/openSymbol
/* tslint:enable:no-unused-variable */
import {IMessageService, Severity} from 'vs/platform/message/common/message';
import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation';
import {ISearchStats} from 'vs/platform/search/common/search';
import {ISearchStats, ICachedSearchStats, IUncachedSearchStats} from 'vs/platform/search/common/search';
import {ITelemetryService} from 'vs/platform/telemetry/common/telemetry';
import {IWorkspaceContextService} from 'vs/workbench/services/workspace/common/contextService';
import {IConfigurationService} from 'vs/platform/configuration/common/configuration';
const objects_assign: <T, U>(destination: T, source: U) => T & U = objects.assign;
interface ISearchWithRange {
search: string;
range: IRange;
}
interface ITimerEventData {
fromCache: boolean;
searchLength: number;
unsortedResultDuration: number;
sortedResultDuration: number;
numberOfResultEntries: number;
fileWalkStartDuration?: number;
fileWalkResultDuration?: number;
directoriesWalked?: number;
filesWalked?: number;
resultCount: number;
symbols: {
fromCache: boolean;
};
files: {
fromCache: boolean;
unsortedResultDuration: number;
sortedResultDuration: number;
resultCount: number;
} & ({
fileWalkStartDuration: number;
fileWalkResultDuration: number;
directoriesWalked: number;
filesWalked: number;
} | {
cacheLookupStartDuration: number;
cacheLookupResultDuration: number;
cacheEntryCount: number;
});
}
interface ITelemetryData {
fromCache: boolean;
searchLength: number;
searchStats?: ISearchStats;
unsortedResultTime: number;
sortedResultTime: number;
numberOfResultEntries: number;
resultCount: number;
symbols: {
fromCache: boolean;
};
files: ISearchStats;
}
// OpenSymbolHandler is used from an extension and must be in the main bundle file so it can load
@ -71,11 +89,12 @@ export class OpenAnythingHandler extends QuickOpenHandler {
private openSymbolHandler: OpenSymbolHandler;
private openFileHandler: OpenFileHandler;
private resultsToSearchCache: { [searchValue: string]: QuickOpenEntry[]; };
private symbolResultsToSearchCache: { [searchValue: string]: QuickOpenEntry[]; };
private delayer: ThrottledDelayer<QuickOpenModel>;
private pendingSearch: TPromise<QuickOpenModel>;
private isClosed: boolean;
private scorerCache: { [key: string]: number };
private cacheKey: string;
constructor(
@IMessageService private messageService: IMessageService,
@ -96,8 +115,9 @@ export class OpenAnythingHandler extends QuickOpenHandler {
skipSorting: true // we sort combined with file results
});
this.resultsToSearchCache = Object.create(null);
this.symbolResultsToSearchCache = Object.create(null);
this.scorerCache = Object.create(null);
this.cacheKey = uuid.generateUuid();
this.delayer = new ThrottledDelayer<QuickOpenModel>(OpenAnythingHandler.SEARCH_DELAY);
}
@ -129,13 +149,7 @@ export class OpenAnythingHandler extends QuickOpenHandler {
}
// Check Cache first
let cachedResults = this.getResultsFromCache(searchValue, searchWithRange ? searchWithRange.range : null);
if (cachedResults) {
const [viewResults, telemetry] = cachedResults;
timerEvent.data = this.createTimerEventData(startTime, telemetry);
timerEvent.stop();
return TPromise.as(new QuickOpenModel(viewResults));
}
let cachedSymbolResults = this.getSymbolResultsFromCache(searchValue, !!searchWithRange);
// The throttler needs a factory for its promises
let promiseFactory = () => {
@ -144,7 +158,9 @@ export class OpenAnythingHandler extends QuickOpenHandler {
// Symbol Results (unless a range is specified)
let resultPromises: TPromise<QuickOpenModel>[] = [];
if (!searchWithRange) {
if (cachedSymbolResults) {
resultPromises.push(TPromise.as(new QuickOpenModel(cachedSymbolResults)));
} else if (!searchWithRange) {
let symbolSearchTimeoutPromiseFn: (timeout: number) => TPromise<QuickOpenModel> = (timeout) => {
return TPromise.timeout(timeout).then(() => {
@ -172,7 +188,7 @@ export class OpenAnythingHandler extends QuickOpenHandler {
}
// File Results
resultPromises.push(this.openFileHandler.getResultsWithStats(searchValue).then(([results, stats]) => {
resultPromises.push(this.openFileHandler.getResultsWithStats(searchValue, this.cacheKey, OpenAnythingHandler.MAX_DISPLAYED_RESULTS).then(([results, stats]) => {
receivedFileResults = true;
searchStats = stats;
@ -190,10 +206,11 @@ export class OpenAnythingHandler extends QuickOpenHandler {
}
// Combine symbol results and file results
let result = [...results[0].entries, ...results[1].entries];
const symbolResults = results[0].entries;
let result = [...symbolResults, ...results[1].entries];
// Cache for fast lookup
this.resultsToSearchCache[searchValue] = result;
// // Cache for fast lookup
this.symbolResultsToSearchCache[searchValue] = symbolResults;
// Sort
const normalizedSearchValue = strings.stripWildcards(searchValue).toLowerCase();
@ -211,12 +228,14 @@ export class OpenAnythingHandler extends QuickOpenHandler {
});
timerEvent.data = this.createTimerEventData(startTime, {
fromCache: false,
searchLength: searchValue.length,
searchStats: searchStats,
unsortedResultTime,
sortedResultTime,
numberOfResultEntries: result.length
resultCount: result.length,
symbols: {
fromCache: !!cachedSymbolResults
},
files: searchStats
});
timerEvent.stop();
return TPromise.as<QuickOpenModel>(new QuickOpenModel(viewResults));
@ -228,8 +247,8 @@ export class OpenAnythingHandler extends QuickOpenHandler {
return this.pendingSearch;
};
// Trigger through delayer to prevent accumulation while the user is typing
return this.delayer.trigger(promiseFactory);
// Trigger through delayer to prevent accumulation while the user is typing (except when expecting results to come from cache)
return cachedSymbolResults ? promiseFactory() : this.delayer.trigger(promiseFactory);
}
private extractRange(value: string): ISearchWithRange {
@ -280,14 +299,14 @@ export class OpenAnythingHandler extends QuickOpenHandler {
return null;
}
private getResultsFromCache(searchValue: string, range: IRange = null): [QuickOpenEntry[], ITelemetryData] {
private getSymbolResultsFromCache(searchValue: string, hasRange: boolean): QuickOpenEntry[] {
if (paths.isAbsolute(searchValue)) {
return null; // bypass cache if user looks up an absolute path where matching goes directly on disk
}
// Find cache entries by prefix of search value
let cachedEntries: QuickOpenEntry[];
for (let previousSearch in this.resultsToSearchCache) {
for (let previousSearch in this.symbolResultsToSearchCache) {
// If we narrow down, we might be able to reuse the cached results
if (searchValue.indexOf(previousSearch) === 0) {
@ -295,7 +314,7 @@ export class OpenAnythingHandler extends QuickOpenHandler {
continue; // since a path character widens the search for potential more matches, require it in previous search too
}
cachedEntries = this.resultsToSearchCache[previousSearch];
cachedEntries = this.symbolResultsToSearchCache[previousSearch];
break;
}
}
@ -304,17 +323,16 @@ export class OpenAnythingHandler extends QuickOpenHandler {
return null;
}
if (hasRange) {
return [];
}
// Pattern match on results and adjust highlights
let results: QuickOpenEntry[] = [];
const normalizedSearchValueLowercase = strings.stripWildcards(searchValue).toLowerCase();
for (let i = 0; i < cachedEntries.length; i++) {
let entry = cachedEntries[i];
// Check for file entries if range is used
if (range && !(entry instanceof FileEntry)) {
continue;
}
// Check if this entry is a match for the search value
const resource = entry.getResource(); // can be null for symbol results!
let targetToMatch = resource ? labels.getPathLabel(resource, this.contextService) : entry.getLabel();
@ -324,29 +342,8 @@ export class OpenAnythingHandler extends QuickOpenHandler {
results.push(entry);
}
const unsortedResultTime = Date.now();
// Sort
const compare = (elementA, elementB) => QuickOpenEntry.compareByScore(elementA, elementB, searchValue, normalizedSearchValueLowercase, this.scorerCache);
const viewResults = arrays.top(results, compare, OpenAnythingHandler.MAX_DISPLAYED_RESULTS);
const sortedResultTime = Date.now();
// Apply range and highlights
viewResults.forEach(entry => {
if (entry instanceof FileEntry) {
entry.setRange(range);
}
const {labelHighlights, descriptionHighlights} = QuickOpenEntry.highlight(entry, searchValue, true /* fuzzy highlight */);
entry.setHighlights(labelHighlights, descriptionHighlights);
});
return [viewResults, {
fromCache: true,
searchLength: searchValue.length,
unsortedResultTime,
sortedResultTime,
numberOfResultEntries: results.length
}];
return results;
}
public getGroupLabel(): string {
@ -366,8 +363,10 @@ export class OpenAnythingHandler extends QuickOpenHandler {
this.cancelPendingSearch();
// Clear Cache
this.resultsToSearchCache = Object.create(null);
this.symbolResultsToSearchCache = Object.create(null);
this.scorerCache = Object.create(null);
this.openFileHandler.clearCache(this.cacheKey);
this.cacheKey = uuid.generateUuid();
// Propagate
this.openSymbolHandler.onClose(canceled);
@ -382,19 +381,30 @@ export class OpenAnythingHandler extends QuickOpenHandler {
}
private createTimerEventData(startTime: number, telemetry: ITelemetryData): ITimerEventData {
const data: ITimerEventData = {
fromCache: telemetry.fromCache,
const stats = telemetry.files;
const cached = stats as ICachedSearchStats;
const uncached = stats as IUncachedSearchStats;
return {
searchLength: telemetry.searchLength,
unsortedResultDuration: telemetry.unsortedResultTime - startTime,
sortedResultDuration: telemetry.sortedResultTime - startTime,
numberOfResultEntries: telemetry.numberOfResultEntries
resultCount: telemetry.resultCount,
symbols: telemetry.symbols,
files: objects_assign({
fromCache: stats.fromCache,
unsortedResultDuration: stats.unsortedResultTime - startTime,
sortedResultDuration: stats.sortedResultTime - startTime,
resultCount: stats.resultCount
}, stats.fromCache ? {
cacheLookupStartDuration: cached.cacheLookupStartTime - startTime,
cacheLookupResultDuration: cached.cacheLookupResultTime - startTime,
cacheEntryCount: cached.cacheEntryCount
} : {
fileWalkStartDuration: uncached.fileWalkStartTime - startTime,
fileWalkResultDuration: uncached.fileWalkResultTime - startTime,
directoriesWalked: uncached.directoriesWalked,
filesWalked: uncached.filesWalked
})
};
const stats = telemetry.searchStats;
return stats ? objects.assign(data, {
fileWalkStartDuration: stats.fileWalkStartTime - startTime,
fileWalkResultDuration: stats.fileWalkResultTime - startTime,
directoriesWalked: stats.directoriesWalked,
filesWalked: stats.filesWalked
}) : data;
}
}

View file

@ -104,7 +104,7 @@ export class OpenFileHandler extends QuickOpenHandler {
.then(result => result[0]);
}
public getResultsWithStats(searchValue: string): TPromise<[QuickOpenModel, ISearchStats]> {
public getResultsWithStats(searchValue: string, cacheKey?: string, maxSortedResults?: number): TPromise<[QuickOpenModel, ISearchStats]> {
searchValue = searchValue.trim();
let promise: TPromise<[QuickOpenEntry[], ISearchStats]>;
@ -112,18 +112,23 @@ export class OpenFileHandler extends QuickOpenHandler {
if (!searchValue) {
promise = TPromise.as(<[QuickOpenEntry[], ISearchStats]>[[], undefined]);
} else {
promise = this.doFindResults(searchValue);
promise = this.doFindResults(searchValue, cacheKey, maxSortedResults);
}
return promise.then(result => [new QuickOpenModel(result[0]), result[1]]);
}
private doFindResults(searchValue: string): TPromise<[QuickOpenEntry[], ISearchStats]> {
private doFindResults(searchValue: string, cacheKey?: string, maxSortedResults?: number): TPromise<[QuickOpenEntry[], ISearchStats]> {
const query: IQueryOptions = {
folderResources: this.contextService.getWorkspace() ? [this.contextService.getWorkspace().resource] : [],
extraFileResources: getOutOfWorkspaceEditorResources(this.editorGroupService, this.contextService),
filePattern: searchValue
filePattern: searchValue,
cacheKey: cacheKey
};
if (maxSortedResults) {
query.maxResults = maxSortedResults;
query.sortByScore = true;
}
return this.searchService.search(this.queryBuilder.file(query)).then((complete) => {
let results: QuickOpenEntry[] = [];
@ -140,6 +145,10 @@ export class OpenFileHandler extends QuickOpenHandler {
});
}
public clearCache(cacheKey: string): TPromise<void> {
return this.searchService.clearCache(cacheKey);
}
public getGroupLabel(): string {
return nls.localize('searchResults', "search results");
}

View file

@ -59,6 +59,8 @@ export class QueryBuilder {
excludePattern: options.excludePattern,
includePattern: options.includePattern,
maxResults: options.maxResults,
sortByScore: options.sortByScore,
cacheKey: options.cacheKey,
fileEncoding: options.fileEncoding,
contentPattern: contentPattern
};

View file

@ -14,7 +14,7 @@ import { SearchModel } from 'vs/workbench/parts/search/common/searchModel';
import URI from 'vs/base/common/uri';
import {IFileMatch, ILineMatch} from 'vs/platform/search/common/search';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { ISearchService, ISearchComplete, ISearchProgressItem, ISearchStats } from 'vs/platform/search/common/search';
import { ISearchService, ISearchComplete, ISearchProgressItem, IUncachedSearchStats } from 'vs/platform/search/common/search';
import { Range } from 'vs/editor/common/core/range';
import { createMockModelService } from 'vs/test/utils/servicesTestUtils';
import { IModelService } from 'vs/editor/common/services/modelService';
@ -24,7 +24,9 @@ suite('SearchModel', () => {
let instantiationService: TestInstantiationService;
let restoreStubs;
const testSearchStats: ISearchStats = {
const testSearchStats: IUncachedSearchStats = {
fromCache: false,
resultCount: 4,
fileWalkStartTime: 0,
fileWalkResultTime: 1,
directoriesWalked: 2,

View file

@ -13,11 +13,11 @@ import arrays = require('vs/base/common/arrays');
import strings = require('vs/base/common/strings');
import types = require('vs/base/common/types');
import glob = require('vs/base/common/glob');
import {IProgress, ISearchStats} from 'vs/platform/search/common/search';
import {IProgress, IUncachedSearchStats} from 'vs/platform/search/common/search';
import extfs = require('vs/base/node/extfs');
import flow = require('vs/base/node/flow');
import {ISerializedFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine} from './search';
import {IRawFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine} from './search';
export class FileWalker {
private config: IRawSearch;
@ -59,7 +59,7 @@ export class FileWalker {
this.isCanceled = true;
}
public walk(rootFolders: string[], extraFiles: string[], onResult: (result: ISerializedFileMatch, size: number) => void, done: (error: Error, isLimitHit: boolean) => void): void {
public walk(rootFolders: string[], extraFiles: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error, isLimitHit: boolean) => void): void {
this.fileWalkStartTime = Date.now();
// Support that the file pattern is a full path to a file that exists
@ -70,7 +70,12 @@ export class FileWalker {
// Report result from file pattern if matching
if (exists) {
onResult({ path: this.filePattern }, size);
this.resultCount++;
onResult({
absolutePath: this.filePattern,
pathLabel: this.filePattern,
size
});
// Optimization: a match on an absolute path is a good result and we do not
// continue walking the entire root paths array for other matches because
@ -106,7 +111,12 @@ export class FileWalker {
// Report result from file pattern if matching
if (match) {
onResult({ path: match }, size);
this.resultCount++;
onResult({
absolutePath: match,
pathLabel: this.filePattern,
size
});
}
return this.doWalk(paths.normalize(absolutePath), '', files, onResult, perEntryCallback);
@ -118,12 +128,14 @@ export class FileWalker {
});
}
public getStats(): ISearchStats {
public getStats(): IUncachedSearchStats {
return {
fromCache: false,
fileWalkStartTime: this.fileWalkStartTime,
fileWalkResultTime: Date.now(),
directoriesWalked: this.directoriesWalked,
filesWalked: this.filesWalked
filesWalked: this.filesWalked,
resultCount: this.resultCount
};
}
@ -149,7 +161,7 @@ export class FileWalker {
});
}
private doWalk(absolutePath: string, relativeParentPathWithSlashes: string, files: string[], onResult: (result: ISerializedFileMatch, size: number) => void, done: (error: Error, result: any) => void): void {
private doWalk(absolutePath: string, relativeParentPathWithSlashes: string, files: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error, result: any) => void): void {
// Execute tasks on each file in parallel to optimize throughput
flow.parallel(files, (file: string, clb: (error: Error) => void): void => {
@ -242,7 +254,7 @@ export class FileWalker {
});
}
private matchFile(onResult: (result: ISerializedFileMatch, size: number) => void, absolutePath: string, relativePathWithSlashes: string, size?: number): void {
private matchFile(onResult: (result: IRawFileMatch) => void, absolutePath: string, relativePathWithSlashes: string, size?: number): void {
if (this.isFilePatternMatch(relativePathWithSlashes) && (!this.includePattern || glob.match(this.includePattern, relativePathWithSlashes))) {
this.resultCount++;
@ -252,8 +264,10 @@ export class FileWalker {
if (!this.isLimitHit) {
onResult({
path: absolutePath
}, size);
absolutePath,
pathLabel: relativePathWithSlashes,
size
});
}
}
}
@ -296,7 +310,7 @@ export class FileWalker {
}
}
export class Engine implements ISearchEngine {
export class Engine implements ISearchEngine<IRawFileMatch> {
private rootFolders: string[];
private extraFiles: string[];
private walker: FileWalker;
@ -308,7 +322,7 @@ export class Engine implements ISearchEngine {
this.walker = new FileWalker(config);
}
public search(onResult: (result: ISerializedFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
this.walker.walk(this.rootFolders, this.extraFiles, onResult, (err: Error, isLimitHit: boolean) => {
done(err, {
limitHit: isLimitHit,

View file

@ -10,20 +10,29 @@ import fs = require('fs');
import gracefulFs = require('graceful-fs');
gracefulFs.gracefulify(fs);
import {PPromise} from 'vs/base/common/winjs.base';
import arrays = require('vs/base/common/arrays');
import {compareByScore} from 'vs/base/common/comparers';
import objects = require('vs/base/common/objects');
import paths = require('vs/base/common/paths');
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 {Engine as TextSearchEngine} from 'vs/workbench/services/search/node/textSearch';
import {IRawSearchService, IRawSearch, ISerializedSearchProgressItem, ISerializedSearchComplete, ISearchEngine} from './search';
import {IRawSearchService, IRawSearch, IRawFileMatch, ISerializedFileMatch, ISerializedSearchProgressItem, ISerializedSearchComplete, ISearchEngine} from './search';
import {ICachedSearchStats, IProgress} from 'vs/platform/search/common/search';
export type IRawProgressItem<T> = T | T[] | IProgress;
export class SearchService implements IRawSearchService {
private static BATCH_SIZE = 500;
private static BATCH_SIZE = 512;
private caches: { [cacheKey: string]: Cache; } = Object.create(null);
public fileSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
let engine = new FileSearchEngine(config);
return this.doSearch(engine, SearchService.BATCH_SIZE);
return this.doFileSearch(FileSearchEngine, config, SearchService.BATCH_SIZE);
}
public textSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
@ -39,8 +48,194 @@ export class SearchService implements IRawSearchService {
return this.doSearch(engine, SearchService.BATCH_SIZE);
}
public doSearch(engine: ISearchEngine, batchSize?: number): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
public doFileSearch(EngineClass: { new (config: IRawSearch): ISearchEngine<IRawFileMatch>; }, config: IRawSearch, batchSize?: number): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
if (config.sortByScore) {
const cached = this.trySearchFromCache(config, batchSize);
if (cached) {
return cached;
}
const walkerConfig = config.maxResults ? objects.assign({}, config, { maxResults: null }) : config;
const engine = new EngineClass(walkerConfig);
return this.doSortedSearch(engine, config, batchSize);
}
let searchPromise;
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
const engine = new EngineClass(config);
searchPromise = this.doSearch(engine, batchSize)
.then(c, e, progress => {
if (Array.isArray(progress)) {
p(progress.map(m => ({ path: m.absolutePath })));
} else if ((<IRawFileMatch>progress).absolutePath) {
p({ path: (<IRawFileMatch>progress).absolutePath });
} else {
p(progress);
}
});
}, () => searchPromise.cancel());
}
private doSortedSearch(engine: ISearchEngine<IRawFileMatch>, config: IRawSearch, batchSize?: number): PPromise<ISerializedSearchComplete, IRawProgressItem<IRawFileMatch>> {
let searchPromise;
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
let results: IRawFileMatch[] = [];
let unsortedResultTime: number;
let sortedResultTime: number;
searchPromise = this.doSearch(engine, -1).then(result => {
const maxResults = config.maxResults;
result.limitHit = !!maxResults && results.length > maxResults;
result.stats.unsortedResultTime = unsortedResultTime || Date.now();
result.stats.sortedResultTime = sortedResultTime || Date.now();
c(result);
}, null, progress => {
try {
if (Array.isArray(progress)) {
results = progress;
let scorerCache;
if (config.cacheKey) {
const cache = this.getOrCreateCache(config.cacheKey);
cache.resultsToSearchCache[config.filePattern] = results;
scorerCache = cache.scorerCache;
} else {
scorerCache = Object.create(null);
}
unsortedResultTime = Date.now();
const sortedResults = this.sortResults(config, results, scorerCache);
sortedResultTime = Date.now();
this.sendProgress(sortedResults, p, batchSize);
} else {
p(progress);
}
} catch (err) {
e(err);
}
}).then(null, e);
}, () => searchPromise.cancel());
}
private getOrCreateCache(cacheKey: string): Cache {
const existing = this.caches[cacheKey];
if (existing) {
return existing;
}
return this.caches[cacheKey] = new Cache();
}
private trySearchFromCache(config: IRawSearch, batchSize?: number): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
const cache = config.cacheKey && this.caches[config.cacheKey];
if (!cache) {
return;
}
const cacheLookupStartTime = Date.now();
const cached = this.getResultsFromCache(cache, config.filePattern);
if (cached) {
const cacheLookupResultTime = Date.now();
const [results, cacheEntryCount] = cached;
let clippedResults;
if (config.sortByScore) {
clippedResults = this.sortResults(config, results, cache);
} else if (config.maxResults) {
clippedResults = results.slice(0, config.maxResults);
} else {
clippedResults = results;
}
const sortedResultTime = Date.now();
let canceled = false;
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
process.nextTick(() => { // allow caller to register progress callback first
if (canceled) {
return;
}
this.sendProgress(clippedResults, p, batchSize);
const maxResults = config.maxResults;
const stats: ICachedSearchStats = {
fromCache: true,
cacheLookupStartTime: cacheLookupStartTime,
cacheLookupResultTime: cacheLookupResultTime,
cacheEntryCount: cacheEntryCount,
resultCount: results.length
};
if (config.sortByScore) {
stats.unsortedResultTime = cacheLookupResultTime;
stats.sortedResultTime = sortedResultTime;
}
c({
limitHit: !!maxResults && results.length > maxResults,
stats: stats
});
});
}, () => {
canceled = true;
});
}
}
private sortResults(config: IRawSearch, results: IRawFileMatch[], cache: Cache): ISerializedFileMatch[] {
const filePattern = config.filePattern;
const normalizedSearchValue = strings.stripWildcards(filePattern).toLowerCase();
const compare = (elementA: IRawFileMatch, elementB: IRawFileMatch) => compareByScore(elementA, elementB, FileMatchAccessor, filePattern, normalizedSearchValue, cache.scorerCache);
const filteredWrappers = arrays.top(results, compare, config.maxResults);
return filteredWrappers.map(result => ({ path: result.absolutePath }));
}
private sendProgress(results: ISerializedFileMatch[], progressCb: (batch: ISerializedFileMatch[]) => void, batchSize?: number) {
if (batchSize && batchSize > 0) {
for (let i = 0; i < results.length; i += batchSize) {
progressCb(results.slice(i, i + batchSize));
}
} else {
progressCb(results);
}
}
private getResultsFromCache(cache: Cache, searchValue: string): [IRawFileMatch[], number] {
if (paths.isAbsolute(searchValue)) {
return null; // bypass cache if user looks up an absolute path where matching goes directly on disk
}
// Find cache entries by prefix of search value
const hasPathSep = searchValue.indexOf(paths.nativeSep) >= 0;
let cachedEntries: IRawFileMatch[];
for (let previousSearch in cache.resultsToSearchCache) {
// If we narrow down, we might be able to reuse the cached results
if (strings.startsWith(searchValue, previousSearch)) {
if (hasPathSep && previousSearch.indexOf(paths.nativeSep) < 0) {
continue; // since a path character widens the search for potential more matches, require it in previous search too
}
cachedEntries = cache.resultsToSearchCache[previousSearch];
break;
}
}
if (!cachedEntries) {
return null;
}
// Pattern match on results and adjust highlights
let results: IRawFileMatch[] = [];
const normalizedSearchValue = searchValue.replace(/\\/g, '/'); // Normalize file patterns to forward slashes
const normalizedSearchValueLowercase = strings.stripWildcards(normalizedSearchValue).toLowerCase();
for (let i = 0; i < cachedEntries.length; i++) {
let entry = cachedEntries[i];
// Check if this entry is a match for the search value
if (!scorer.matches(entry.pathLabel, normalizedSearchValueLowercase)) {
continue;
}
results.push(entry);
}
return [results, cachedEntries.length];
}
private doSearch<T>(engine: ISearchEngine<T>, batchSize?: number): PPromise<ISerializedSearchComplete, IRawProgressItem<T>> {
return new PPromise<ISerializedSearchComplete, IRawProgressItem<T>>((c, e, p) => {
let batch = [];
engine.search((match) => {
if (match) {
@ -68,4 +263,34 @@ export class SearchService implements IRawSearchService {
});
}, () => engine.cancel());
}
}
public clearCache(cacheKey: string): TPromise<void> {
delete this.caches[cacheKey];
return TPromise.as(undefined);
}
}
class Cache {
public resultsToSearchCache: { [searchValue: string]: IRawFileMatch[]; } = Object.create(null);
public scorerCache: { [key: string]: number } = Object.create(null);
}
interface IFileMatch extends IRawFileMatch {
label?: string;
}
class FileMatchAccessor {
public static getLabel(match: IFileMatch): string {
if (!match.label) {
match.label = paths.basename(match.absolutePath);
}
return match.label;
}
public static getResourcePath(match: IFileMatch): string {
return match.absolutePath;
}
}

View file

@ -5,7 +5,7 @@
'use strict';
import { PPromise } from 'vs/base/common/winjs.base';
import { PPromise, TPromise } from 'vs/base/common/winjs.base';
import glob = require('vs/base/common/glob');
import { IProgress, ILineMatch, IPatternInfo, ISearchStats } from 'vs/platform/search/common/search';
@ -17,6 +17,8 @@ export interface IRawSearch {
includePattern?: glob.IExpression;
contentPattern?: IPatternInfo;
maxResults?: number;
sortByScore?: boolean;
cacheKey?: string;
maxFilesize?: number;
fileEncoding?: string;
}
@ -24,10 +26,17 @@ export interface IRawSearch {
export interface IRawSearchService {
fileSearch(search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
textSearch(search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
clearCache(cacheKey: string): TPromise<void>;
}
export interface ISearchEngine {
search: (onResult: (match: ISerializedFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void) => void;
export interface IRawFileMatch {
absolutePath: string;
pathLabel: string;
size: number;
}
export interface ISearchEngine<T> {
search: (onResult: (match: T) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void) => void;
cancel: () => void;
}

View file

@ -12,6 +12,7 @@ import { IRawSearchService, IRawSearch, ISerializedSearchComplete, ISerializedSe
export interface ISearchChannel extends IChannel {
call(command: 'fileSearch', search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
call(command: 'textSearch', search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
call(command: 'clearCache', cacheKey: string): TPromise<void>;
call(command: string, arg: any): TPromise<any>;
}
@ -23,6 +24,7 @@ export class SearchChannel implements ISearchChannel {
switch (command) {
case 'fileSearch': return this.service.fileSearch(arg);
case 'textSearch': return this.service.textSearch(arg);
case 'clearCache': return this.service.clearCache(arg);
}
}
}
@ -38,4 +40,8 @@ export class SearchChannelClient implements IRawSearchService {
textSearch(search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
return this.channel.call('textSearch', search);
}
public clearCache(cacheKey: string): TPromise<void> {
return this.channel.call('clearCache', cacheKey);
}
}

View file

@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------------*/
'use strict';
import {PPromise} from 'vs/base/common/winjs.base';
import {PPromise, TPromise} from 'vs/base/common/winjs.base';
import uri from 'vs/base/common/uri';
import glob = require('vs/base/common/glob');
import objects = require('vs/base/common/objects');
@ -189,6 +189,10 @@ export class SearchService implements ISearchService {
return true;
}
public clearCache(cacheKey: string): TPromise<void> {
return this.diskSearch.clearCache(cacheKey);
}
}
export class DiskSearch {
@ -223,7 +227,9 @@ export class DiskSearch {
filePattern: query.filePattern,
excludePattern: query.excludePattern,
includePattern: query.includePattern,
maxResults: query.maxResults
maxResults: query.maxResults,
sortByScore: query.sortByScore,
cacheKey: query.cacheKey
};
if (query.type === QueryType.Text) {
@ -282,4 +288,8 @@ export class DiskSearch {
}
return fileMatch;
}
public clearCache(cacheKey: string): TPromise<void> {
return this.raw.clearCache(cacheKey);
}
}

View file

@ -21,7 +21,7 @@ interface ReadLinesOptions {
encoding: string;
}
export class Engine implements ISearchEngine {
export class Engine implements ISearchEngine<ISerializedFileMatch> {
private static PROGRESS_FLUSH_CHUNK_SIZE = 50; // optimization: number of files to process before emitting progress event
@ -88,8 +88,8 @@ export class Engine implements ISearchEngine {
};
// Walk over the file system
this.walker.walk(this.rootFolders, this.extraFiles, (result, size) => {
size = size ||  1;
this.walker.walk(this.rootFolders, this.extraFiles, result => {
const size = result.size ||  1;
this.total += size;
// If the result is empty or we have reached the limit or we are canceled, ignore it
@ -126,7 +126,7 @@ export class Engine implements ISearchEngine {
}
if (fileMatch === null) {
fileMatch = new FileMatch(result.path);
fileMatch = new FileMatch(result.absolutePath);
}
if (lineMatch === null) {
@ -141,7 +141,7 @@ export class Engine implements ISearchEngine {
};
// Read lines buffered to support large files
this.readlinesAsync(result.path, perLineCallback, { bufferLength: 8096, encoding: this.fileEncoding }, doneCallback);
this.readlinesAsync(result.absolutePath, perLineCallback, { bufferLength: 8096, encoding: this.fileEncoding }, doneCallback);
}, (error, isLimitHit) => {
this.walkerIsDone = true;
this.walkerError = error;

View file

@ -12,6 +12,7 @@ import {join, normalize} from 'vs/base/common/paths';
import {LineMatch} from 'vs/platform/search/common/search';
import {FileWalker, Engine as FileSearchEngine} from 'vs/workbench/services/search/node/fileSearch';
import {IRawFileMatch} from 'vs/workbench/services/search/node/search';
import {Engine as TextSearchEngine} from 'vs/workbench/services/search/node/textSearch';
function count(lineMatches: LineMatch[]): number {
@ -149,7 +150,7 @@ suite('Search', () => {
});
let count = 0;
let res;
let res: IRawFileMatch;
engine.search((result) => {
if (result) {
count++;
@ -158,7 +159,7 @@ suite('Search', () => {
}, () => { }, (error) => {
assert.ok(!error);
assert.equal(count, 1);
assert.ok(path.basename(res.path) === 'site.less');
assert.strictEqual(path.basename(res.absolutePath), 'site.less');
done();
});
});
@ -246,7 +247,7 @@ suite('Search', () => {
});
let count = 0;
let res;
let res: IRawFileMatch;
engine.search((result) => {
if (result) {
count++;
@ -255,7 +256,7 @@ suite('Search', () => {
}, () => { }, (error) => {
assert.ok(!error);
assert.equal(count, 1);
assert.equal(path.basename(res.path), '汉语.txt');
assert.equal(path.basename(res.absolutePath), '汉语.txt');
done();
});
});
@ -286,7 +287,7 @@ suite('Search', () => {
});
let count = 0;
let res;
let res: IRawFileMatch;
engine.search((result) => {
if (result) {
count++;
@ -295,7 +296,7 @@ suite('Search', () => {
}, () => { }, (error) => {
assert.ok(!error);
assert.equal(count, 1);
assert.equal(path.basename(res.path), 'site.css');
assert.equal(path.basename(res.absolutePath), 'site.css');
done();
});
});
@ -308,7 +309,7 @@ suite('Search', () => {
});
let count = 0;
let res;
let res: IRawFileMatch;
engine.search((result) => {
if (result) {
count++;
@ -317,7 +318,7 @@ suite('Search', () => {
}, () => { }, (error) => {
assert.ok(!error);
assert.equal(count, 1);
assert.equal(path.basename(res.path), 'company.js');
assert.equal(path.basename(res.absolutePath), 'company.js');
done();
});
});
@ -334,7 +335,7 @@ suite('Search', () => {
});
let count = 0;
let res;
let res: IRawFileMatch;
engine.search((result) => {
if (result) {
count++;
@ -343,7 +344,7 @@ suite('Search', () => {
}, () => { }, (error) => {
assert.ok(!error);
assert.equal(count, 1);
assert.equal(path.basename(res.path), 'company.js');
assert.equal(path.basename(res.absolutePath), 'company.js');
done();
});
});
@ -361,7 +362,7 @@ suite('Search', () => {
});
let count = 0;
let res;
let res: IRawFileMatch;
engine.search((result) => {
if (result) {
count++;
@ -370,7 +371,7 @@ suite('Search', () => {
}, () => { }, (error) => {
assert.ok(!error);
assert.equal(count, 1);
assert.equal(path.basename(res.path), 'site.css');
assert.equal(path.basename(res.absolutePath), 'site.css');
done();
});
});

View file

@ -7,31 +7,47 @@
import assert = require('assert');
import {IProgress} from 'vs/platform/search/common/search';
import {ISearchEngine, ISerializedFileMatch, ISerializedSearchComplete} from 'vs/workbench/services/search/node/search';
import {IProgress, IUncachedSearchStats} from 'vs/platform/search/common/search';
import {ISearchEngine, IRawSearch, IRawFileMatch, ISerializedFileMatch, ISerializedSearchComplete} from 'vs/workbench/services/search/node/search';
import {SearchService as RawSearchService} from 'vs/workbench/services/search/node/rawSearchService';
import {DiskSearch} from 'vs/workbench/services/search/node/searchService';
class TestSearchEngine implements ISearchEngine {
const stats: IUncachedSearchStats = {
fromCache: false,
resultCount: 4,
fileWalkStartTime: 0,
fileWalkResultTime: 1,
directoriesWalked: 2,
filesWalked: 3
};
constructor(private result: () => ISerializedFileMatch) {
class TestSearchEngine implements ISearchEngine<IRawFileMatch> {
public static last: TestSearchEngine;
private isCanceled = false;
constructor(private result: () => IRawFileMatch, public config?: IRawSearch) {
TestSearchEngine.last = this;
}
public search(onResult: (match: ISerializedFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
public search(onResult: (match: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
const self = this;
(function next() {
process.nextTick(() => {
if (self.isCanceled) {
done(null, {
limitHit: false,
stats: stats
});
return;
}
const result = self.result();
if (!result) {
done(null, {
limitHit: false,
stats: {
fileWalkStartTime: 0,
fileWalkResultTime: 1,
directoriesWalked: 2,
filesWalked: 3
}
stats: stats
});
} else {
onResult(result);
@ -42,24 +58,39 @@ class TestSearchEngine implements ISearchEngine {
}
public cancel(): void {
this.isCanceled = true;
}
}
suite('SearchService', () => {
const rawSearch: IRawSearch = {
rootFolders: ['/some/where'],
filePattern: 'a'
};
const rawMatch: IRawFileMatch = {
absolutePath: '/some/where',
pathLabel: 'where',
size: 123
};
const match: ISerializedFileMatch = {
path: '/some/where'
};
test('Individual results', function () {
const path = '/some/where';
let i = 5;
const engine = new TestSearchEngine(() => i-- && { path });
const Engine = TestSearchEngine.bind(null, () => i-- && rawMatch);
const service = new RawSearchService();
let results = 0;
return service.doSearch(engine)
return service.doFileSearch(Engine, rawSearch)
.then(() => {
assert.strictEqual(results, 5);
}, null, value => {
if (!Array.isArray(value)) {
assert.strictEqual((<any>value).path, path);
assert.deepStrictEqual(value, match);
results++;
} else {
assert.fail(value);
@ -68,19 +99,18 @@ suite('SearchService', () => {
});
test('Batch results', function () {
const path = '/some/where';
let i = 25;
const engine = new TestSearchEngine(() => i-- && { path });
const Engine = TestSearchEngine.bind(null, () => i-- && rawMatch);
const service = new RawSearchService();
const results = [];
return service.doSearch(engine, 10)
return service.doFileSearch(Engine, rawSearch, 10)
.then(() => {
assert.deepStrictEqual(results, [10, 10, 5]);
}, null, value => {
if (Array.isArray(value)) {
value.forEach(match => {
assert.strictEqual(match.path, path);
value.forEach(m => {
assert.deepStrictEqual(m, match);
});
results.push(value.length);
} else {
@ -92,12 +122,12 @@ suite('SearchService', () => {
test('Collect batched results', function () {
const path = '/some/where';
let i = 25;
const engine = new TestSearchEngine(() => i-- && { path });
const Engine = TestSearchEngine.bind(null, () => i-- && rawMatch);
const service = new RawSearchService();
const diskSearch = new DiskSearch(false);
const progressResults = [];
return DiskSearch.collectResults(service.doSearch(engine, 10))
return DiskSearch.collectResults(service.doFileSearch(Engine, rawSearch, 10))
.then(result => {
assert.strictEqual(result.results.length, 25, 'Result');
assert.strictEqual(progressResults.length, 25, 'Progress');
@ -106,4 +136,129 @@ suite('SearchService', () => {
progressResults.push(match);
});
});
test('Sorted results', function () {
const paths = ['bab', 'bbc', 'abb'];
const matches = paths.map(path => ({
absolutePath: `/some/where/${path}`,
pathLabel: path,
size: 3
}));
let i = 0;
const Engine = TestSearchEngine.bind(null, () => matches.shift());
const service = new RawSearchService();
const results = [];
return service.doFileSearch(Engine, {
rootFolders: ['/some/where'],
filePattern: 'bb',
sortByScore: true,
maxResults: 2
}, 1).then(() => {
assert.notStrictEqual(typeof TestSearchEngine.last.config.maxResults, 'number');
assert.deepStrictEqual(results, ['/some/where/bbc', '/some/where/bab']);
}, null, value => {
if (Array.isArray(value)) {
results.push(...value.map(v => v.path));
} else {
assert.fail(value);
}
});
});
test('Sorted result batches', function () {
let i = 25;
const Engine = TestSearchEngine.bind(null, () => i-- && rawMatch);
const service = new RawSearchService();
const results = [];
return service.doFileSearch(Engine, {
rootFolders: ['/some/where'],
filePattern: 'a',
sortByScore: true,
maxResults: 23
}, 10)
.then(() => {
assert.deepStrictEqual(results, [10, 10, 3]);
}, null, value => {
if (Array.isArray(value)) {
value.forEach(m => {
assert.deepStrictEqual(m, match);
});
results.push(value.length);
} else {
assert.fail(value);
}
});
});
test('Cached results', function () {
const paths = ['bcb', 'bbc', 'aab'];
const matches = paths.map(path => ({
absolutePath: `/some/where/${path}`,
pathLabel: path,
size: 3
}));
let i = 0;
const Engine = TestSearchEngine.bind(null, () => matches.shift());
const service = new RawSearchService();
const results = [];
return service.doFileSearch(Engine, {
rootFolders: ['/some/where'],
filePattern: 'b',
sortByScore: true,
cacheKey: 'x'
}, -1).then(complete => {
assert.strictEqual(complete.stats.fromCache, false);
assert.deepStrictEqual(results, ['/some/where/bcb', '/some/where/bbc', '/some/where/aab']);
}, null, value => {
if (Array.isArray(value)) {
results.push(...value.map(v => v.path));
} else {
assert.fail(value);
}
}).then(() => {
const results = [];
return service.doFileSearch(Engine, {
rootFolders: ['/some/where'],
filePattern: 'bc',
sortByScore: true,
cacheKey: 'x'
}, -1).then(complete => {
assert.ok(complete.stats.fromCache);
assert.deepStrictEqual(results, ['/some/where/bcb', '/some/where/bbc']);
}, null, value => {
if (Array.isArray(value)) {
results.push(...value.map(v => v.path));
} else {
assert.fail(value);
}
});
}).then(() => {
return service.clearCache('x');
}).then(() => {
matches.push({
absolutePath: '/some/where/bc',
pathLabel: 'bc',
size: 3
});
const results = [];
return service.doFileSearch(Engine, {
rootFolders: ['/some/where'],
filePattern: 'bc',
sortByScore: true,
cacheKey: 'x'
}, -1).then(complete => {
assert.strictEqual(complete.stats.fromCache, false);
assert.deepStrictEqual(results, ['/some/where/bc']);
}, null, value => {
if (Array.isArray(value)) {
results.push(...value.map(v => v.path));
} else {
assert.fail(value);
}
});
});
});
});

View file

@ -36,7 +36,9 @@ suite('QuickOpen performance', () => {
test('Measure', () => {
const n = 3;
const testWorkspaceArg = minimist(process.argv)['testWorkspace'];
const argv = minimist(process.argv);
const testWorkspaceArg = argv['testWorkspace'];
const verboseResults = argv['verboseResults'];
const testWorkspacePath = testWorkspaceArg ? path.join(process.cwd(), testWorkspaceArg) : __dirname;
const telemetryService = new TestTelemetryService();
@ -69,12 +71,15 @@ suite('QuickOpen performance', () => {
.then((handler: QuickOpenHandler) => {
return handler.getResults('a').then(result => {
const uncachedEvent = popEvent();
assert.ok(!uncachedEvent.data.fromCache);
assert.strictEqual(uncachedEvent.data.symbols.fromCache, false, 'symbols.fromCache');
assert.strictEqual(uncachedEvent.data.files.fromCache, false, 'files.fromCache');
return uncachedEvent;
}).then(uncachedEvent => {
return handler.getResults('ab').then(result => {
const cachedEvent = popEvent();
assert.ok(cachedEvent.data.fromCache);
assert.ok(cachedEvent.data.symbols.fromCache, 'symbolsFromCache');
assert.ok(cachedEvent.data.files.fromCache, 'filesFromCache');
handler.onClose(false);
return [uncachedEvent, cachedEvent];
});
});
@ -90,6 +95,19 @@ suite('QuickOpen performance', () => {
return event;
}
function printResult(data) {
if (verboseResults) {
console.log(JSON.stringify(data, null, ' ') + ',');
} else {
console.log(JSON.stringify({
filesfromCache: data.files.fromCache,
searchLength: data.searchLength,
sortedResultDuration: data.sortedResultDuration,
filesResultCount: data.files.resultCount,
}) + ',');
}
}
return measure() // Warm-up first
.then(() => {
if (testWorkspaceArg) { // Don't measure by default
@ -101,14 +119,14 @@ suite('QuickOpen performance', () => {
}
return measure()
.then(([uncachedEvent, cachedEvent]) => {
console.log(JSON.stringify(uncachedEvent.data) + ',');
printResult(uncachedEvent.data);
cachedEvents.push(cachedEvent);
return iterate();
});
})().then(() => {
console.log();
cachedEvents.forEach(cachedEvent => {
console.log(JSON.stringify(cachedEvent.data) + ',');
printResult(cachedEvent.data);
});
});
}