未验证 提交 8bc931bd 编写于 作者: J João Moreno 提交者: GitHub

Merge pull request #53536 from Microsoft/joao/search-progress-events

Use events instead of PPromise for search IPC
......@@ -25,7 +25,7 @@ import { IProgress, IUncachedSearchStats } from 'vs/platform/search/common/searc
import * as extfs from 'vs/base/node/extfs';
import * as flow from 'vs/base/node/flow';
import { IRawFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine, IFolderSearch } from './search';
import { IRawFileMatch, IRawSearch, ISearchEngine, IFolderSearch, ISerializedSearchSuccess } from './search';
import { spawnRipgrepCmd } from './ripgrepFileSearch';
import { rgErrorMsgForDisplay } from './ripgrepTextSearch';
......@@ -721,9 +721,10 @@ export class Engine implements ISearchEngine<IRawFileMatch> {
this.walker = new FileWalker(config);
}
public search(onResult: (result: IRawFileMatch) => 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: ISerializedSearchSuccess) => void): void {
this.walker.walk(this.folderQueries, this.extraFiles, onResult, onProgress, (err: Error, isLimitHit: boolean) => {
done(err, {
type: 'success',
limitHit: isLimitHit,
stats: this.walker.getStats()
});
......
......@@ -11,7 +11,7 @@ import { join, sep } from 'path';
import * as arrays from 'vs/base/common/arrays';
import * as objects from 'vs/base/common/objects';
import * as strings from 'vs/base/common/strings';
import { PPromise, TPromise } from 'vs/base/common/winjs.base';
import { TPromise } from 'vs/base/common/winjs.base';
import { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from 'vs/base/parts/quickopen/common/quickOpenScorer';
import { MAX_FILE_SIZE } from 'vs/platform/files/node/files';
import { ICachedSearchStats, IProgress } from 'vs/platform/search/common/search';
......@@ -19,10 +19,14 @@ import { Engine as FileSearchEngine, FileWalker } from 'vs/workbench/services/se
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 { IFileSearchProgressItem, IRawFileMatch, IRawSearch, IRawSearchService, ISearchEngine, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ITelemetryEvent } from './search';
import { IFileSearchProgressItem, IRawFileMatch, IRawSearch, IRawSearchService, ISearchEngine, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ITelemetryEvent, ISerializedSearchSuccess } from './search';
import { Event, Emitter } from 'vs/base/common/event';
gracefulFs.gracefulify(fs);
type IProgressCallback = (p: ISerializedSearchProgressItem) => void;
type IFileProgressCallback = (p: IFileSearchProgressItem) => void;
export class SearchService implements IRawSearchService {
private static readonly BATCH_SIZE = 512;
......@@ -31,29 +35,52 @@ export class SearchService implements IRawSearchService {
private textSearchWorkerProvider: TextSearchWorkerProvider;
private telemetryPipe: (event: ITelemetryEvent) => void;
private _onTelemetry = new Emitter<ITelemetryEvent>();
readonly onTelemetry: Event<ITelemetryEvent> = this._onTelemetry.event;
public fileSearch(config: IRawSearch, batchSize = SearchService.BATCH_SIZE): Event<ISerializedSearchProgressItem | ISerializedSearchComplete> {
let promise: TPromise<ISerializedSearchSuccess>;
const emitter = new Emitter<ISerializedSearchProgressItem | ISerializedSearchComplete>({
onFirstListenerAdd: () => {
promise = this.doFileSearch(FileSearchEngine, config, p => emitter.fire(p), batchSize)
.then(c => emitter.fire(c), err => emitter.fire({ type: 'error', error: err }));
},
onLastListenerRemove: () => {
promise.cancel();
}
});
public fileSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
return this.doFileSearch(FileSearchEngine, config, SearchService.BATCH_SIZE);
return emitter.event;
}
public textSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
return config.useRipgrep ?
this.ripgrepTextSearch(config) :
this.legacyTextSearch(config);
public textSearch(config: IRawSearch): Event<ISerializedSearchProgressItem | ISerializedSearchComplete> {
let promise: TPromise<ISerializedSearchSuccess>;
const emitter = new Emitter<ISerializedSearchProgressItem | ISerializedSearchComplete>({
onFirstListenerAdd: () => {
promise = (config.useRipgrep ? this.ripgrepTextSearch(config, p => emitter.fire(p)) : this.legacyTextSearch(config, p => emitter.fire(p)))
.then(c => emitter.fire(c), err => emitter.fire({ type: 'error', error: err }));
},
onLastListenerRemove: () => {
promise.cancel();
}
});
return emitter.event;
}
public ripgrepTextSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
private ripgrepTextSearch(config: IRawSearch, progressCallback: IProgressCallback): TPromise<ISerializedSearchSuccess> {
config.maxFilesize = MAX_FILE_SIZE;
let engine = new RipgrepEngine(config);
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
return new TPromise<ISerializedSearchSuccess>((c, e) => {
// 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);
const collector = new BatchedCollector<ISerializedFileMatch>(SearchService.BATCH_SIZE, progressCallback);
engine.search((match) => {
collector.addItem(match, match.numMatches);
}, (message) => {
p(message);
progressCallback(message);
}, (error, stats) => {
collector.flush();
......@@ -68,7 +95,7 @@ export class SearchService implements IRawSearchService {
});
}
public legacyTextSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
private legacyTextSearch(config: IRawSearch, progressCallback: IProgressCallback): TPromise<ISerializedSearchComplete> {
if (!this.textSearchWorkerProvider) {
this.textSearchWorkerProvider = new TextSearchWorkerProvider();
}
......@@ -86,75 +113,75 @@ export class SearchService implements IRawSearchService {
}),
this.textSearchWorkerProvider);
return this.doTextSearch(engine, SearchService.BATCH_SIZE);
return this.doTextSearch(engine, progressCallback, SearchService.BATCH_SIZE);
}
public doFileSearch(EngineClass: { new(config: IRawSearch): ISearchEngine<IRawFileMatch>; }, config: IRawSearch, batchSize?: number): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
doFileSearch(EngineClass: { new(config: IRawSearch): ISearchEngine<IRawFileMatch>; }, config: IRawSearch, progressCallback: IProgressCallback, batchSize?: number): TPromise<ISerializedSearchSuccess> {
const fileProgressCallback: IFileProgressCallback = progress => {
if (Array.isArray(progress)) {
progressCallback(progress.map(m => this.rawMatchToSearchItem(m)));
} else if ((<IRawFileMatch>progress).relativePath) {
progressCallback(this.rawMatchToSearchItem(<IRawFileMatch>progress));
} else {
progressCallback(<IProgress>progress);
}
};
if (config.sortByScore) {
let sortedSearch = this.trySortedSearchFromCache(config);
let sortedSearch = this.trySortedSearchFromCache(config, fileProgressCallback);
if (!sortedSearch) {
const walkerConfig = config.maxResults ? objects.assign({}, config, { maxResults: null }) : config;
const engine = new EngineClass(walkerConfig);
sortedSearch = this.doSortedSearch(engine, config);
sortedSearch = this.doSortedSearch(engine, config, progressCallback, fileProgressCallback);
}
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
return new TPromise<ISerializedSearchSuccess>((c, e) => {
process.nextTick(() => { // allow caller to register progress callback first
sortedSearch.then(([result, rawMatches]) => {
const serializedMatches = rawMatches.map(rawMatch => this.rawMatchToSearchItem(rawMatch));
this.sendProgress(serializedMatches, p, batchSize);
this.sendProgress(serializedMatches, progressCallback, batchSize);
c(result);
}, e, p);
}, e);
});
}, () => {
sortedSearch.cancel();
});
}
let searchPromise: PPromise<void, IFileSearchProgressItem>;
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 => this.rawMatchToSearchItem(m)));
} else if ((<IRawFileMatch>progress).relativePath) {
p(this.rawMatchToSearchItem(<IRawFileMatch>progress));
} else {
p(<IProgress>progress);
}
});
}, () => {
searchPromise.cancel();
});
const engine = new EngineClass(config);
return this.doSearch(engine, fileProgressCallback, batchSize);
}
private rawMatchToSearchItem(match: IRawFileMatch): ISerializedFileMatch {
return { path: match.base ? join(match.base, match.relativePath) : match.relativePath };
}
private doSortedSearch(engine: ISearchEngine<IRawFileMatch>, config: IRawSearch): PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress> {
let searchPromise: PPromise<void, IFileSearchProgressItem>;
let allResultsPromise = new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>((c, e, p) => {
private doSortedSearch(engine: ISearchEngine<IRawFileMatch>, config: IRawSearch, progressCallback: IProgressCallback, fileProgressCallback: IFileProgressCallback): TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]> {
let searchPromise: TPromise<void>;
const emitter = new Emitter<IFileSearchProgressItem>();
let allResultsPromise = new TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]>((c, e) => {
let results: IRawFileMatch[] = [];
searchPromise = this.doSearch(engine, -1)
const innerProgressCallback: IFileProgressCallback = progress => {
if (Array.isArray(progress)) {
results = progress;
} else {
fileProgressCallback(progress);
emitter.fire(progress);
}
};
searchPromise = this.doSearch(engine, innerProgressCallback, -1)
.then(result => {
c([result, results]);
if (this.telemetryPipe) {
// __GDPR__TODO__ classify event
this.telemetryPipe({
eventName: 'fileSearch',
data: result.stats
});
}
}, e, progress => {
if (Array.isArray(progress)) {
results = progress;
} else {
p(progress);
}
});
// __GDPR__TODO__ classify event
this._onTelemetry.fire({
eventName: 'fileSearch',
data: result.stats
});
}, e);
}, () => {
searchPromise.cancel();
});
......@@ -162,7 +189,10 @@ export class SearchService implements IRawSearchService {
let cache: Cache;
if (config.cacheKey) {
cache = this.getOrCreateCache(config.cacheKey);
cache.resultsToSearchCache[config.filePattern] = allResultsPromise;
cache.resultsToSearchCache[config.filePattern] = {
promise: allResultsPromise,
event: emitter.event
};
allResultsPromise.then(null, err => {
delete cache.resultsToSearchCache[config.filePattern];
});
......@@ -170,7 +200,7 @@ export class SearchService implements IRawSearchService {
}
let chained: TPromise<void>;
return new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress>((c, e, p) => {
return new TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]>((c, e) => {
chained = allResultsPromise.then(([result, results]) => {
const scorerCache: ScorerCache = cache ? cache.scorerCache : Object.create(null);
const unsortedResultTime = Date.now();
......@@ -179,14 +209,15 @@ export class SearchService implements IRawSearchService {
const sortedResultTime = Date.now();
c([{
type: 'success',
stats: objects.assign({}, result.stats, {
unsortedResultTime,
sortedResultTime
}),
limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults
}, sortedResults]);
} as ISerializedSearchSuccess, sortedResults]);
});
}, e, p);
}, e);
}, () => {
chained.cancel();
});
......@@ -200,17 +231,17 @@ export class SearchService implements IRawSearchService {
return this.caches[cacheKey] = new Cache();
}
private trySortedSearchFromCache(config: IRawSearch): PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress> {
private trySortedSearchFromCache(config: IRawSearch, progressCallback: IFileProgressCallback): TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]> {
const cache = config.cacheKey && this.caches[config.cacheKey];
if (!cache) {
return undefined;
}
const cacheLookupStartTime = Date.now();
const cached = this.getResultsFromCache(cache, config.filePattern);
const cached = this.getResultsFromCache(cache, config.filePattern, progressCallback);
if (cached) {
let chained: TPromise<void>;
return new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress>((c, e, p) => {
return new TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]>((c, e) => {
chained = cached.then(([result, results, cacheStats]) => {
const cacheLookupResultTime = Date.now();
return this.sortResults(config, results, cache.scorerCache)
......@@ -234,13 +265,14 @@ export class SearchService implements IRawSearchService {
}
c([
{
type: 'success',
limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults,
stats: stats
},
} as ISerializedSearchSuccess,
sortedResults
]);
});
}, e, p);
}, e);
}, () => {
chained.cancel();
});
......@@ -259,7 +291,7 @@ export class SearchService implements IRawSearchService {
return arrays.topAsync(results, compare, config.maxResults, 10000);
}
private sendProgress(results: ISerializedFileMatch[], progressCb: (batch: ISerializedFileMatch[]) => void, batchSize: number) {
private sendProgress(results: ISerializedFileMatch[], progressCb: IProgressCallback, batchSize: number) {
if (batchSize && batchSize > 0) {
for (let i = 0; i < results.length; i += batchSize) {
progressCb(results.slice(i, i + batchSize));
......@@ -269,10 +301,10 @@ export class SearchService implements IRawSearchService {
}
}
private getResultsFromCache(cache: Cache, searchValue: string): PPromise<[ISerializedSearchComplete, IRawFileMatch[], CacheStats], IProgress> {
private getResultsFromCache(cache: Cache, searchValue: string, progressCallback: IFileProgressCallback): TPromise<[ISerializedSearchSuccess, IRawFileMatch[], CacheStats]> {
// Find cache entries by prefix of search value
const hasPathSep = searchValue.indexOf(sep) >= 0;
let cached: PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>;
let cachedRow: CacheRow;
let wasResolved: boolean;
for (let previousSearch in cache.resultsToSearchCache) {
......@@ -282,20 +314,25 @@ export class SearchService implements IRawSearchService {
continue; // since a path character widens the search for potential more matches, require it in previous search too
}
const c = cache.resultsToSearchCache[previousSearch];
c.then(() => { wasResolved = false; });
const row = cache.resultsToSearchCache[previousSearch];
row.promise.then(() => { wasResolved = false; });
wasResolved = true;
cached = this.preventCancellation(c);
cachedRow = {
promise: this.preventCancellation(row.promise),
event: row.event
};
break;
}
}
if (!cached) {
if (!cachedRow) {
return null;
}
return new PPromise<[ISerializedSearchComplete, IRawFileMatch[], CacheStats], IProgress>((c, e, p) => {
cached.then(([complete, cachedEntries]) => {
const listener = cachedRow.event(progressCallback);
return new TPromise<[ISerializedSearchSuccess, IRawFileMatch[], CacheStats]>((c, e) => {
cachedRow.promise.then(([complete, cachedEntries]) => {
const cacheFilterStartTime = Date.now();
// Pattern match on results
......@@ -317,21 +354,22 @@ export class SearchService implements IRawSearchService {
cacheFilterStartTime: cacheFilterStartTime,
cacheFilterResultCount: cachedEntries.length
}]);
}, e, p);
}, e);
}, () => {
cached.cancel();
cachedRow.promise.cancel();
listener.dispose();
});
}
private doTextSearch(engine: TextSearchEngine, batchSize: number): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
private doTextSearch(engine: TextSearchEngine, progressCallback: IProgressCallback, batchSize: number): TPromise<ISerializedSearchSuccess> {
return new TPromise<ISerializedSearchSuccess>((c, e) => {
// Use BatchedCollector to get new results to the frontend every 2s at least, until 50 results have been returned
const collector = new BatchedCollector<ISerializedFileMatch>(batchSize, p);
const collector = new BatchedCollector<ISerializedFileMatch>(batchSize, progressCallback);
engine.search((matches) => {
const totalMatches = matches.reduce((acc, m) => acc + m.numMatches, 0);
collector.addItems(matches, totalMatches);
}, (progress) => {
p(progress);
progressCallback(progress);
}, (error, stats) => {
collector.flush();
......@@ -346,28 +384,28 @@ export class SearchService implements IRawSearchService {
});
}
private doSearch(engine: ISearchEngine<IRawFileMatch>, batchSize?: number): PPromise<ISerializedSearchComplete, IFileSearchProgressItem> {
return new PPromise<ISerializedSearchComplete, IFileSearchProgressItem>((c, e, p) => {
private doSearch(engine: ISearchEngine<IRawFileMatch>, progressCallback: IFileProgressCallback, batchSize?: number): TPromise<ISerializedSearchSuccess> {
return new TPromise<ISerializedSearchSuccess>((c, e) => {
let batch: IRawFileMatch[] = [];
engine.search((match) => {
if (match) {
if (batchSize) {
batch.push(match);
if (batchSize > 0 && batch.length >= batchSize) {
p(batch);
progressCallback(batch);
batch = [];
}
} else {
p(match);
progressCallback(match);
}
}
}, (progress) => {
process.nextTick(() => {
p(progress);
progressCallback(progress);
});
}, (error, stats) => {
if (batch.length) {
p(batch);
progressCallback(batch);
}
if (error) {
e(error);
......@@ -385,19 +423,11 @@ export class SearchService implements IRawSearchService {
return TPromise.as(undefined);
}
public fetchTelemetry(): PPromise<void, ITelemetryEvent> {
return new PPromise((c, e, p) => {
this.telemetryPipe = p;
}, () => {
this.telemetryPipe = null;
});
}
private preventCancellation<C, P>(promise: PPromise<C, P>): PPromise<C, P> {
return new PPromise<C, P>((c, e, p) => {
private preventCancellation<C, P>(promise: TPromise<C>): TPromise<C> {
return new TPromise<C>((c, e) => {
// Allow for piled up cancellations to come through first.
process.nextTick(() => {
promise.then(c, e, p);
promise.then(c, e);
});
}, () => {
// Do not propagate.
......@@ -405,9 +435,14 @@ export class SearchService implements IRawSearchService {
}
}
interface CacheRow {
promise: TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]>;
event: Event<IFileSearchProgressItem>;
}
class Cache {
public resultsToSearchCache: { [searchValue: string]: PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>; } = Object.create(null);
public resultsToSearchCache: { [searchValue: string]: CacheRow; } = Object.create(null);
public scorerCache: ScorerCache = Object.create(null);
}
......
......@@ -18,7 +18,7 @@ import * as encoding from 'vs/base/node/encoding';
import * as extfs from 'vs/base/node/extfs';
import { IProgress } from 'vs/platform/search/common/search';
import { rgPath } from 'vscode-ripgrep';
import { FileMatch, IFolderSearch, IRawSearch, ISerializedFileMatch, ISerializedSearchComplete, LineMatch } from './search';
import { FileMatch, IFolderSearch, IRawSearch, ISerializedFileMatch, LineMatch, ISerializedSearchSuccess } from './search';
// If vscode-ripgrep is in an .asar file, then the binary is unpacked.
const rgDiskPath = rgPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked');
......@@ -44,10 +44,11 @@ export class RipgrepEngine {
}
// 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, onMessage: (message: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
search(onResult: (match: ISerializedFileMatch) => void, onMessage: (message: IProgress) => void, done: (error: Error, complete: ISerializedSearchSuccess) => void): void {
if (!this.config.folderQueries.length && !this.config.extraFiles.length) {
process.removeListener('exit', this.killRgProcFn);
done(null, {
type: 'success',
limitHit: false,
stats: null
});
......@@ -94,6 +95,7 @@ export class RipgrepEngine {
this.cancel();
process.removeListener('exit', this.killRgProcFn);
done(null, {
type: 'success',
limitHit: true,
stats: null
});
......@@ -124,11 +126,13 @@ export class RipgrepEngine {
process.removeListener('exit', this.killRgProcFn);
if (stderr && !gotData && (displayMsg = rgErrorMsgForDisplay(stderr))) {
done(new Error(displayMsg), {
type: 'success',
limitHit: false,
stats: null
});
} else {
done(null, {
type: 'success',
limitHit: false,
stats: null
});
......
......@@ -5,10 +5,11 @@
'use strict';
import { PPromise, TPromise } from 'vs/base/common/winjs.base';
import { TPromise } from 'vs/base/common/winjs.base';
import { IExpression } from 'vs/base/common/glob';
import { IProgress, ILineMatch, IPatternInfo, ISearchStats } from 'vs/platform/search/common/search';
import { ITelemetryData } from 'vs/platform/telemetry/common/telemetry';
import { Event } from 'vs/base/common/event';
export interface IFolderSearch {
folder: string;
......@@ -41,10 +42,10 @@ export interface ITelemetryEvent {
}
export interface IRawSearchService {
fileSearch(search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
textSearch(search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
fileSearch(search: IRawSearch): Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
textSearch(search: IRawSearch): Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
clearCache(cacheKey: string): TPromise<void>;
fetchTelemetry(): PPromise<void, ITelemetryEvent>;
readonly onTelemetry: Event<ITelemetryEvent>;
}
export interface IRawFileMatch {
......@@ -55,15 +56,37 @@ export interface IRawFileMatch {
}
export interface ISearchEngine<T> {
search: (onResult: (matches: T) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void) => void;
search: (onResult: (matches: T) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchSuccess) => void) => void;
cancel: () => void;
}
export interface ISerializedSearchComplete {
export interface ISerializedSearchSuccess {
type: 'success';
limitHit: boolean;
stats: ISearchStats;
}
export interface ISerializedSearchError {
type: 'error';
error: any;
}
export type ISerializedSearchComplete = ISerializedSearchSuccess | ISerializedSearchError;
export function isSerializedSearchComplete(arg: ISerializedSearchProgressItem | ISerializedSearchComplete): arg is ISerializedSearchComplete {
if ((arg as any).type === 'error') {
return true;
} else if ((arg as any).type === 'success') {
return true;
} else {
return false;
}
}
export function isSerializedSearchSuccess(arg: ISerializedSearchComplete): arg is ISerializedSearchSuccess {
return arg.type === 'success';
}
export interface ISerializedFileMatch {
path: string;
lineMatches?: ILineMatch[];
......
......@@ -5,16 +5,16 @@
'use strict';
import { PPromise, TPromise } from 'vs/base/common/winjs.base';
import { TPromise } from 'vs/base/common/winjs.base';
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
import { IRawSearchService, IRawSearch, ISerializedSearchComplete, ISerializedSearchProgressItem, ITelemetryEvent } from './search';
import { Event } from 'vs/base/common/event';
export interface ISearchChannel extends IChannel {
call(command: 'fileSearch', search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
call(command: 'textSearch', search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
listen(event: 'telemetry'): Event<ITelemetryEvent>;
listen(event: 'fileSearch', search: IRawSearch): Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
listen(event: 'textSearch', search: IRawSearch): Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
call(command: 'clearCache', cacheKey: string): TPromise<void>;
call(command: 'fetchTelemetry'): PPromise<void, ITelemetryEvent>;
call(command: string, arg: any): TPromise<any>;
}
......@@ -22,38 +22,38 @@ export class SearchChannel implements ISearchChannel {
constructor(private service: IRawSearchService) { }
listen<T>(event: string, arg?: any): Event<T> {
throw new Error('No events');
listen<T>(event: string, arg?: any): Event<any> {
switch (event) {
case 'telemetry': return this.service.onTelemetry;
case 'fileSearch': return this.service.fileSearch(arg);
case 'textSearch': return this.service.textSearch(arg);
}
throw new Error('Event not found');
}
call(command: string, arg?: any): TPromise<any> {
switch (command) {
case 'fileSearch': return this.service.fileSearch(arg);
case 'textSearch': return this.service.textSearch(arg);
case 'clearCache': return this.service.clearCache(arg);
case 'fetchTelemetry': return this.service.fetchTelemetry();
}
return undefined;
throw new Error('Call not found');
}
}
export class SearchChannelClient implements IRawSearchService {
get onTelemetry(): Event<ITelemetryEvent> { return this.channel.listen('telemetry'); }
constructor(private channel: ISearchChannel) { }
fileSearch(search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
return this.channel.call('fileSearch', search);
fileSearch(search: IRawSearch): Event<ISerializedSearchProgressItem | ISerializedSearchComplete> {
return this.channel.listen('fileSearch', search);
}
textSearch(search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
return this.channel.call('textSearch', search);
textSearch(search: IRawSearch): Event<ISerializedSearchProgressItem | ISerializedSearchComplete> {
return this.channel.listen('textSearch', search);
}
clearCache(cacheKey: string): TPromise<void> {
return this.channel.call('clearCache', cacheKey);
}
fetchTelemetry(): PPromise<void, ITelemetryEvent> {
return this.channel.call('fetchTelemetry');
}
}
\ No newline at end of file
......@@ -15,7 +15,7 @@ import { IProgress, LineMatch, FileMatch, ISearchComplete, ISearchProgressItem,
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IRawSearch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedFileMatch, IRawSearchService, ITelemetryEvent } from './search';
import { IRawSearch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedFileMatch, IRawSearchService, ITelemetryEvent, isSerializedSearchComplete, isSerializedSearchSuccess, ISerializedSearchSuccess } from './search';
import { ISearchChannel, SearchChannelClient } from './searchIpc';
import { IEnvironmentService, IDebugParams } from 'vs/platform/environment/common/environment';
import { ResourceMap } from 'vs/base/common/map';
......@@ -26,6 +26,7 @@ import { Schemas } from 'vs/base/common/network';
import * as pfs from 'vs/base/node/pfs';
import { ILogService } from 'vs/platform/log/common/log';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { Event } from 'vs/base/common/event';
export class SearchService implements ISearchService {
public _serviceBrand: any;
......@@ -320,14 +321,14 @@ export class DiskSearch implements ISearchResultProvider {
const existingFolders = folderQueries.filter((q, index) => exists[index]);
const rawSearch = this.rawSearchQuery(query, existingFolders);
let request: PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
let event: Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
if (query.type === QueryType.File) {
request = this.raw.fileSearch(rawSearch);
event = this.raw.fileSearch(rawSearch);
} else {
request = this.raw.textSearch(rawSearch);
event = this.raw.textSearch(rawSearch);
}
return DiskSearch.collectResults(request);
return DiskSearch.collectResultsFromEvent(event);
});
}
......@@ -372,7 +373,28 @@ export class DiskSearch implements ISearchResultProvider {
return rawSearch;
}
public static collectResults(request: PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>): PPromise<ISearchComplete, ISearchProgressItem> {
public static collectResultsFromEvent(event: Event<ISerializedSearchProgressItem | ISerializedSearchComplete>): PPromise<ISearchComplete, ISearchProgressItem> {
const promise = new PPromise<ISerializedSearchSuccess, ISerializedSearchProgressItem>((c, e, p) => {
setTimeout(() => {
const listener = event(ev => {
if (isSerializedSearchComplete(ev)) {
if (isSerializedSearchSuccess(ev)) {
c(ev);
} else {
e(ev.error);
}
listener.dispose();
} else {
p(ev);
}
});
}, 0);
});
return DiskSearch.collectResults(promise);
}
public static collectResults(request: PPromise<ISerializedSearchSuccess, ISerializedSearchProgressItem>): PPromise<ISearchComplete, ISearchProgressItem> {
let result: IFileMatch[] = [];
return new PPromise<ISearchComplete, ISearchProgressItem>((c, e, p) => {
request.done((complete) => {
......@@ -420,6 +442,8 @@ export class DiskSearch implements ISearchResultProvider {
}
public fetchTelemetry(): PPromise<void, ITelemetryEvent> {
return this.raw.fetchTelemetry();
return new PPromise<void, ITelemetryEvent>((c, e, p) => {
this.raw.onTelemetry(p);
});
}
}
......@@ -11,7 +11,7 @@ import { onUnexpectedError } from 'vs/base/common/errors';
import { IProgress } from 'vs/platform/search/common/search';
import { FileWalker } from 'vs/workbench/services/search/node/fileSearch';
import { ISerializedFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine } from './search';
import { ISerializedFileMatch, IRawSearch, ISearchEngine, ISerializedSearchSuccess } from './search';
import { ISearchWorker } from './worker/searchWorkerIpc';
import { ITextSearchWorkerProvider } from './textSearchWorkerProvider';
......@@ -60,7 +60,7 @@ export class Engine implements ISearchEngine<ISerializedFileMatch[]> {
});
}
search(onResult: (match: ISerializedFileMatch[]) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
search(onResult: (match: ISerializedFileMatch[]) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchSuccess) => void): void {
this.workers = this.workerProvider.getWorkers();
this.initializeWorkers();
......@@ -86,6 +86,7 @@ export class Engine implements ISearchEngine<ISerializedFileMatch[]> {
if (!this.isDone && this.processedBytes === this.totalBytes && this.walkerIsDone) {
this.isDone = true;
done(this.walkerError, {
type: 'success',
limitHit: this.limitReached,
stats: this.walker.getStats()
});
......
......@@ -9,9 +9,11 @@ import * as assert from 'assert';
import * as path from 'path';
import { IProgress, IUncachedSearchStats } from 'vs/platform/search/common/search';
import { ISearchEngine, IRawSearch, IRawFileMatch, ISerializedFileMatch, ISerializedSearchComplete, IFolderSearch } from 'vs/workbench/services/search/node/search';
import { ISearchEngine, IRawSearch, IRawFileMatch, ISerializedFileMatch, IFolderSearch, ISerializedSearchSuccess, ISerializedSearchProgressItem, 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';
import { Emitter, Event } from 'vs/base/common/event';
import { TPromise } from 'vs/base/common/winjs.base';
const TEST_FOLDER_QUERIES = [
{ folder: path.normalize('/some/where') }
......@@ -44,12 +46,13 @@ class TestSearchEngine implements ISearchEngine<IRawFileMatch> {
TestSearchEngine.last = this;
}
public search(onResult: (match: IRawFileMatch) => 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: ISerializedSearchSuccess) => void): void {
const self = this;
(function next() {
process.nextTick(() => {
if (self.isCanceled) {
done(null, {
type: 'success',
limitHit: false,
stats: stats
});
......@@ -58,6 +61,7 @@ class TestSearchEngine implements ISearchEngine<IRawFileMatch> {
const result = self.result();
if (!result) {
done(null, {
type: 'success',
limitHit: false,
stats: stats
});
......@@ -101,17 +105,17 @@ suite('SearchService', () => {
const service = new RawSearchService();
let results = 0;
return service.doFileSearch(Engine, rawSearch)
.then(() => {
assert.strictEqual(results, 5);
}, null, value => {
if (!Array.isArray(value)) {
assert.deepStrictEqual(value, match);
results++;
} else {
assert.fail(JSON.stringify(value));
}
});
const cb: (p: ISerializedSearchProgressItem) => void = value => {
if (!Array.isArray(value)) {
assert.deepStrictEqual(value, match);
results++;
} else {
assert.fail(JSON.stringify(value));
}
};
return service.doFileSearch(Engine, rawSearch, cb)
.then(() => assert.strictEqual(results, 5));
});
test('Batch results', function () {
......@@ -121,19 +125,20 @@ suite('SearchService', () => {
const service = new RawSearchService();
const results = [];
return service.doFileSearch(Engine, rawSearch, 10)
.then(() => {
assert.deepStrictEqual(results, [10, 10, 5]);
}, null, value => {
if (Array.isArray(value)) {
value.forEach(m => {
assert.deepStrictEqual(m, match);
});
results.push(value.length);
} else {
assert.fail(JSON.stringify(value));
}
});
const cb: (p: ISerializedSearchProgressItem) => void = value => {
if (Array.isArray(value)) {
value.forEach(m => {
assert.deepStrictEqual(m, match);
});
results.push(value.length);
} else {
assert.fail(JSON.stringify(value));
}
};
return service.doFileSearch(Engine, rawSearch, cb, 10).then(() => {
assert.deepStrictEqual(results, [10, 10, 5]);
});
});
test('Collect batched results', function () {
......@@ -143,8 +148,24 @@ suite('SearchService', () => {
const Engine = TestSearchEngine.bind(null, () => i-- && rawMatch);
const service = new RawSearchService();
function fileSearch(config: IRawSearch, batchSize: number): Event<ISerializedSearchProgressItem | ISerializedSearchComplete> {
let promise: TPromise<ISerializedSearchSuccess>;
const emitter = new Emitter<ISerializedSearchProgressItem | ISerializedSearchComplete>({
onFirstListenerAdd: () => {
promise = service.doFileSearch(Engine, config, p => emitter.fire(p), batchSize)
.then(c => emitter.fire(c), err => emitter.fire({ type: 'error', error: err }));
},
onLastListenerRemove: () => {
promise.cancel();
}
});
return emitter.event;
}
const progressResults = [];
return DiskSearch.collectResults(service.doFileSearch(Engine, rawSearch, 10))
return DiskSearch.collectResultsFromEvent(fileSearch(rawSearch, 10))
.then(result => {
assert.strictEqual(result.results.length, 25, 'Result');
assert.strictEqual(progressResults.length, 25, 'Progress');
......@@ -167,7 +188,7 @@ suite('SearchService', () => {
},
};
return DiskSearch.collectResults(service.fileSearch(query))
return DiskSearch.collectResultsFromEvent(service.fileSearch(query))
.then(result => {
assert.strictEqual(result.results.length, 1, 'Result');
});
......@@ -186,7 +207,7 @@ suite('SearchService', () => {
},
};
return DiskSearch.collectResults(service.fileSearch(query))
return DiskSearch.collectResultsFromEvent(service.fileSearch(query))
.then(result => {
assert.strictEqual(result.results.length, 0, 'Result');
assert.ok(result.limitHit);
......@@ -206,20 +227,22 @@ suite('SearchService', () => {
const service = new RawSearchService();
const results = [];
const cb = value => {
if (Array.isArray(value)) {
results.push(...value.map(v => v.path));
} else {
assert.fail(JSON.stringify(value));
}
};
return service.doFileSearch(Engine, {
folderQueries: TEST_FOLDER_QUERIES,
filePattern: 'bb',
sortByScore: true,
maxResults: 2
}, 1).then(() => {
}, cb, 1).then(() => {
assert.notStrictEqual(typeof TestSearchEngine.last.config.maxResults, 'number');
assert.deepStrictEqual(results, [path.normalize('/some/where/bbc'), path.normalize('/some/where/bab')]);
}, null, value => {
if (Array.isArray(value)) {
results.push(...value.map(v => v.path));
} else {
assert.fail(JSON.stringify(value));
}
});
});
......@@ -230,23 +253,24 @@ suite('SearchService', () => {
const service = new RawSearchService();
const results = [];
const cb = value => {
if (Array.isArray(value)) {
value.forEach(m => {
assert.deepStrictEqual(m, match);
});
results.push(value.length);
} else {
assert.fail(JSON.stringify(value));
}
};
return service.doFileSearch(Engine, {
folderQueries: TEST_FOLDER_QUERIES,
filePattern: 'a',
sortByScore: true,
maxResults: 23
}, 10)
}, cb, 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(JSON.stringify(value));
}
});
});
......@@ -263,37 +287,39 @@ suite('SearchService', () => {
const service = new RawSearchService();
const results = [];
const cb = value => {
if (Array.isArray(value)) {
results.push(...value.map(v => v.path));
} else {
assert.fail(JSON.stringify(value));
}
};
return service.doFileSearch(Engine, {
folderQueries: TEST_FOLDER_QUERIES,
filePattern: 'b',
sortByScore: true,
cacheKey: 'x'
}, -1).then(complete => {
}, cb, -1).then(complete => {
assert.strictEqual(complete.stats.fromCache, false);
assert.deepStrictEqual(results, [path.normalize('/some/where/bcb'), path.normalize('/some/where/bbc'), path.normalize('/some/where/aab')]);
}, null, value => {
if (Array.isArray(value)) {
results.push(...value.map(v => v.path));
} else {
assert.fail(JSON.stringify(value));
}
}).then(() => {
const results = [];
const cb = value => {
if (Array.isArray(value)) {
results.push(...value.map(v => v.path));
} else {
assert.fail(JSON.stringify(value));
}
};
return service.doFileSearch(Engine, {
folderQueries: TEST_FOLDER_QUERIES,
filePattern: 'bc',
sortByScore: true,
cacheKey: 'x'
}, -1).then(complete => {
}, cb, -1).then(complete => {
assert.ok(complete.stats.fromCache);
assert.deepStrictEqual(results, [path.normalize('/some/where/bcb'), path.normalize('/some/where/bbc')]);
}, null, value => {
if (Array.isArray(value)) {
results.push(...value.map(v => v.path));
} else {
assert.fail(JSON.stringify(value));
}
});
}, null);
}).then(() => {
return service.clearCache('x');
}).then(() => {
......@@ -304,20 +330,21 @@ suite('SearchService', () => {
size: 3
});
const results = [];
const cb = value => {
if (Array.isArray(value)) {
results.push(...value.map(v => v.path));
} else {
assert.fail(JSON.stringify(value));
}
};
return service.doFileSearch(Engine, {
folderQueries: TEST_FOLDER_QUERIES,
filePattern: 'bc',
sortByScore: true,
cacheKey: 'x'
}, -1).then(complete => {
}, cb, -1).then(complete => {
assert.strictEqual(complete.stats.fromCache, false);
assert.deepStrictEqual(results, [path.normalize('/some/where/bc')]);
}, null, value => {
if (Array.isArray(value)) {
results.push(...value.map(v => v.path));
} else {
assert.fail(JSON.stringify(value));
}
});
});
});
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册