diff --git a/src/vs/workbench/services/search/node/fileSearch.ts b/src/vs/workbench/services/search/node/fileSearch.ts index 1182c79e5061a..dcfb3c3db8ea2 100644 --- a/src/vs/workbench/services/search/node/fileSearch.ts +++ b/src/vs/workbench/services/search/node/fileSearch.ts @@ -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 { 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() }); diff --git a/src/vs/workbench/services/search/node/rawSearchService.ts b/src/vs/workbench/services/search/node/rawSearchService.ts index fd40541b99cf2..d267e0cd3a0cc 100644 --- a/src/vs/workbench/services/search/node/rawSearchService.ts +++ b/src/vs/workbench/services/search/node/rawSearchService.ts @@ -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(); + readonly onTelemetry: Event = this._onTelemetry.event; + + public fileSearch(config: IRawSearch, batchSize = SearchService.BATCH_SIZE): Event { + let promise: TPromise; + + const emitter = new Emitter({ + 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 { - return this.doFileSearch(FileSearchEngine, config, SearchService.BATCH_SIZE); + return emitter.event; } - public textSearch(config: IRawSearch): PPromise { - return config.useRipgrep ? - this.ripgrepTextSearch(config) : - this.legacyTextSearch(config); + public textSearch(config: IRawSearch): Event { + let promise: TPromise; + + const emitter = new Emitter({ + 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 { + private ripgrepTextSearch(config: IRawSearch, progressCallback: IProgressCallback): TPromise { config.maxFilesize = MAX_FILE_SIZE; let engine = new RipgrepEngine(config); - return new PPromise((c, e, p) => { + return new TPromise((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(SearchService.BATCH_SIZE, p); + const collector = new BatchedCollector(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 { + private legacyTextSearch(config: IRawSearch, progressCallback: IProgressCallback): TPromise { 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; }, config: IRawSearch, batchSize?: number): PPromise { + doFileSearch(EngineClass: { new(config: IRawSearch): ISearchEngine; }, config: IRawSearch, progressCallback: IProgressCallback, batchSize?: number): TPromise { + const fileProgressCallback: IFileProgressCallback = progress => { + if (Array.isArray(progress)) { + progressCallback(progress.map(m => this.rawMatchToSearchItem(m))); + } else if ((progress).relativePath) { + progressCallback(this.rawMatchToSearchItem(progress)); + } else { + progressCallback(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((c, e, p) => { + return new TPromise((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; - return new PPromise((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 ((progress).relativePath) { - p(this.rawMatchToSearchItem(progress)); - } else { - p(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, config: IRawSearch): PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress> { - let searchPromise: PPromise; - let allResultsPromise = new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>((c, e, p) => { + private doSortedSearch(engine: ISearchEngine, config: IRawSearch, progressCallback: IProgressCallback, fileProgressCallback: IFileProgressCallback): TPromise<[ISerializedSearchSuccess, IRawFileMatch[]]> { + let searchPromise: TPromise; + const emitter = new Emitter(); + + 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; - 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; - 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 { - return new PPromise((c, e, p) => { + private doTextSearch(engine: TextSearchEngine, progressCallback: IProgressCallback, batchSize: number): TPromise { + return new TPromise((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(batchSize, p); + const collector = new BatchedCollector(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, batchSize?: number): PPromise { - return new PPromise((c, e, p) => { + private doSearch(engine: ISearchEngine, progressCallback: IFileProgressCallback, batchSize?: number): TPromise { + return new TPromise((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 { - return new PPromise((c, e, p) => { - this.telemetryPipe = p; - }, () => { - this.telemetryPipe = null; - }); - } - - private preventCancellation(promise: PPromise): PPromise { - return new PPromise((c, e, p) => { + private preventCancellation(promise: TPromise): TPromise { + return new TPromise((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; +} + 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); } diff --git a/src/vs/workbench/services/search/node/ripgrepTextSearch.ts b/src/vs/workbench/services/search/node/ripgrepTextSearch.ts index c42d0659d9378..81898c0880dd4 100644 --- a/src/vs/workbench/services/search/node/ripgrepTextSearch.ts +++ b/src/vs/workbench/services/search/node/ripgrepTextSearch.ts @@ -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 }); diff --git a/src/vs/workbench/services/search/node/search.ts b/src/vs/workbench/services/search/node/search.ts index a1061c52d22b6..7cf5124dd454d 100644 --- a/src/vs/workbench/services/search/node/search.ts +++ b/src/vs/workbench/services/search/node/search.ts @@ -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; - textSearch(search: IRawSearch): PPromise; + fileSearch(search: IRawSearch): Event; + textSearch(search: IRawSearch): Event; clearCache(cacheKey: string): TPromise; - fetchTelemetry(): PPromise; + readonly onTelemetry: Event; } export interface IRawFileMatch { @@ -55,15 +56,37 @@ export interface IRawFileMatch { } export interface ISearchEngine { - 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[]; diff --git a/src/vs/workbench/services/search/node/searchIpc.ts b/src/vs/workbench/services/search/node/searchIpc.ts index ffa1d267bb9dd..3450ab3e2fdfd 100644 --- a/src/vs/workbench/services/search/node/searchIpc.ts +++ b/src/vs/workbench/services/search/node/searchIpc.ts @@ -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; - call(command: 'textSearch', search: IRawSearch): PPromise; + listen(event: 'telemetry'): Event; + listen(event: 'fileSearch', search: IRawSearch): Event; + listen(event: 'textSearch', search: IRawSearch): Event; call(command: 'clearCache', cacheKey: string): TPromise; - call(command: 'fetchTelemetry'): PPromise; call(command: string, arg: any): TPromise; } @@ -22,38 +22,38 @@ export class SearchChannel implements ISearchChannel { constructor(private service: IRawSearchService) { } - listen(event: string, arg?: any): Event { - throw new Error('No events'); + listen(event: string, arg?: any): Event { + 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 { 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 { return this.channel.listen('telemetry'); } + constructor(private channel: ISearchChannel) { } - fileSearch(search: IRawSearch): PPromise { - return this.channel.call('fileSearch', search); + fileSearch(search: IRawSearch): Event { + return this.channel.listen('fileSearch', search); } - textSearch(search: IRawSearch): PPromise { - return this.channel.call('textSearch', search); + textSearch(search: IRawSearch): Event { + return this.channel.listen('textSearch', search); } clearCache(cacheKey: string): TPromise { return this.channel.call('clearCache', cacheKey); } - - fetchTelemetry(): PPromise { - return this.channel.call('fetchTelemetry'); - } } \ No newline at end of file diff --git a/src/vs/workbench/services/search/node/searchService.ts b/src/vs/workbench/services/search/node/searchService.ts index cc292c177011a..bdc651e9950b9 100644 --- a/src/vs/workbench/services/search/node/searchService.ts +++ b/src/vs/workbench/services/search/node/searchService.ts @@ -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; + let event: Event; 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): PPromise { + public static collectResultsFromEvent(event: Event): PPromise { + const promise = new PPromise((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): PPromise { let result: IFileMatch[] = []; return new PPromise((c, e, p) => { request.done((complete) => { @@ -420,6 +442,8 @@ export class DiskSearch implements ISearchResultProvider { } public fetchTelemetry(): PPromise { - return this.raw.fetchTelemetry(); + return new PPromise((c, e, p) => { + this.raw.onTelemetry(p); + }); } } diff --git a/src/vs/workbench/services/search/node/textSearch.ts b/src/vs/workbench/services/search/node/textSearch.ts index c2002bb88edaa..e2a14693c1939 100644 --- a/src/vs/workbench/services/search/node/textSearch.ts +++ b/src/vs/workbench/services/search/node/textSearch.ts @@ -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 { }); } - 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 { if (!this.isDone && this.processedBytes === this.totalBytes && this.walkerIsDone) { this.isDone = true; done(this.walkerError, { + type: 'success', limitHit: this.limitReached, stats: this.walker.getStats() }); diff --git a/src/vs/workbench/services/search/test/node/searchService.test.ts b/src/vs/workbench/services/search/test/node/searchService.test.ts index 5d33088e8812d..9ceecbfa74879 100644 --- a/src/vs/workbench/services/search/test/node/searchService.test.ts +++ b/src/vs/workbench/services/search/test/node/searchService.test.ts @@ -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 { 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 { 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 { + let promise: TPromise; + + const emitter = new Emitter({ + 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)); - } }); }); });