diff --git a/src/vs/platform/search/common/search.ts b/src/vs/platform/search/common/search.ts index a24028d7299b956502270970ade0756beaac5a9f..156c005ec74c5871e465e8132b9ac4c9b940d70e 100644 --- a/src/vs/platform/search/common/search.ts +++ b/src/vs/platform/search/common/search.ts @@ -13,6 +13,7 @@ import { URI as uri, UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; import { IFilesConfiguration } from 'vs/platform/files/common/files'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { CancellationToken } from 'vs/base/common/cancellation'; export const VIEW_ID = 'workbench.view.search'; @@ -24,7 +25,7 @@ export const ISearchService = createDecorator('searchService'); */ export interface ISearchService { _serviceBrand: any; - search(query: ISearchQuery, onProgress?: (result: ISearchProgressItem) => void): TPromise; + search(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): TPromise; extendQuery(query: ISearchQuery): void; clearCache(cacheKey: string): TPromise; registerSearchResultProvider(scheme: string, type: SearchProviderType, provider: ISearchResultProvider): IDisposable; @@ -55,7 +56,7 @@ export enum SearchProviderType { } export interface ISearchResultProvider { - search(query: ISearchQuery, onProgress?: (p: ISearchProgressItem) => void): TPromise; + search(query: ISearchQuery, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): TPromise; clearCache(cacheKey: string): TPromise; } diff --git a/src/vs/workbench/api/electron-browser/mainThreadSearch.ts b/src/vs/workbench/api/electron-browser/mainThreadSearch.ts index 111d34136f1eb96ad860ef718e872f0dd7c2f5a9..3b54bbecb24fa164d8690985e718a639b1fbe088 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSearch.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSearch.ts @@ -13,6 +13,7 @@ import { IFileMatch, IRawFileMatch2, ISearchComplete, ISearchCompleteStats, ISea import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { extHostNamedCustomer } from 'vs/workbench/api/electron-browser/extHostCustomers'; import { ExtHostContext, ExtHostSearchShape, IExtHostContext, MainContext, MainThreadSearchShape } from '../node/extHost.protocol'; +import { CancellationToken } from 'vs/base/common/cancellation'; @extHostNamedCustomer(MainContext.MainThreadSearch) export class MainThreadSearch implements MainThreadSearchShape { @@ -106,34 +107,28 @@ class RemoteSearchProvider implements ISearchResultProvider, IDisposable { dispose(this._registrations); } - search(query: ISearchQuery, onProgress?: (p: ISearchProgressItem) => void): TPromise { - + search(query: ISearchQuery, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): TPromise { if (isFalsyOrEmpty(query.folderQueries)) { return TPromise.as(undefined); } - let outer: TPromise; - - return new TPromise((resolve, reject) => { + const search = new SearchOperation(onProgress); + this._searches.set(search.id, search); - const search = new SearchOperation(onProgress); - this._searches.set(search.id, search); + const searchP = query.type === QueryType.File + ? this._proxy.$provideFileSearchResults(this._handle, search.id, query) + : this._proxy.$provideTextSearchResults(this._handle, search.id, query.contentPattern, query); - outer = query.type === QueryType.File - ? this._proxy.$provideFileSearchResults(this._handle, search.id, query) - : this._proxy.$provideTextSearchResults(this._handle, search.id, query.contentPattern, query); + if (token) { + token.onCancellationRequested(() => searchP.cancel()); + } - outer.then((result: ISearchCompleteStats) => { - this._searches.delete(search.id); - resolve(({ results: values(search.matches), stats: result.stats, limitHit: result.limitHit })); - }, err => { - this._searches.delete(search.id); - reject(err); - }); - }, () => { - if (outer) { - outer.cancel(); - } + return searchP.then((result: ISearchCompleteStats) => { + this._searches.delete(search.id); + return { results: values(search.matches), stats: result.stats, limitHit: result.limitHit }; + }, err => { + this._searches.delete(search.id); + return TPromise.wrapError(err); }); } diff --git a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts index 98a57c725bea992e89860816356cdb8e2a71786e..c813574aba5c37ce8d4cd079a897c614e8e73d0b 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadWorkspace.ts @@ -24,12 +24,13 @@ import { IExtensionService } from 'vs/workbench/services/extensions/common/exten import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkspaceEditingService } from 'vs/workbench/services/workspace/common/workspaceEditing'; import { ExtHostContext, ExtHostWorkspaceShape, IExtHostContext, MainContext, MainThreadWorkspaceShape } from '../node/extHost.protocol'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; @extHostNamedCustomer(MainContext.MainThreadWorkspace) export class MainThreadWorkspace implements MainThreadWorkspaceShape { private readonly _toDispose: IDisposable[] = []; - private readonly _activeSearches: { [id: number]: TPromise } = Object.create(null); + private readonly _activeCancelTokens: { [id: number]: CancellationTokenSource } = Object.create(null); private readonly _proxy: ExtHostWorkspaceShape; constructor( @@ -51,9 +52,9 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { dispose(): void { dispose(this._toDispose); - for (let requestId in this._activeSearches) { - const search = this._activeSearches[requestId]; - search.cancel(); + for (let requestId in this._activeCancelTokens) { + const tokenSource = this._activeCancelTokens[requestId]; + tokenSource.cancel(); } } @@ -157,7 +158,8 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { this._searchService.extendQuery(query); - const search = this._searchService.search(query).then(result => { + const tokenSource = new CancellationTokenSource(); + const search = this._searchService.search(query, tokenSource.token).then(result => { return result.results.map(m => m.resource); }, err => { if (!isPromiseCanceledError(err)) { @@ -166,8 +168,11 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { return undefined; }); - this._activeSearches[requestId] = search; - const onDone = () => delete this._activeSearches[requestId]; + this._activeCancelTokens[requestId] = tokenSource; + const onDone = () => { + tokenSource.dispose(); + delete this._activeCancelTokens[requestId]; + }; search.then(onDone, onDone); return search; @@ -186,13 +191,12 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { } }; - const search = this._searchService.search(query, onProgress).then( + const tokenSource = new CancellationTokenSource(); + const search = this._searchService.search(query, tokenSource.token, onProgress).then( () => { - delete this._activeSearches[requestId]; return null; }, err => { - delete this._activeSearches[requestId]; if (!isPromiseCanceledError(err)) { return TPromise.wrapError(err); } @@ -200,20 +204,27 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { return undefined; }); - this._activeSearches[requestId] = search; + this._activeCancelTokens[requestId] = tokenSource; + const onDone = () => { + tokenSource.dispose(); + delete this._activeCancelTokens[requestId]; + }; + search.then(onDone, onDone); return search; } $checkExists(query: ISearchQuery, requestId: number): TPromise { query.exists = true; - const search = this._searchService.search(query).then( + + const tokenSource = new CancellationTokenSource(); + const search = this._searchService.search(query, tokenSource.token).then( result => { - delete this._activeSearches[requestId]; + delete this._activeCancelTokens[requestId]; return result.limitHit; }, err => { - delete this._activeSearches[requestId]; + delete this._activeCancelTokens[requestId]; if (!isPromiseCanceledError(err)) { return TPromise.wrapError(err); } @@ -221,16 +232,16 @@ export class MainThreadWorkspace implements MainThreadWorkspaceShape { return undefined; }); - this._activeSearches[requestId] = search; + this._activeCancelTokens[requestId] = tokenSource; return search; } $cancelSearch(requestId: number): Thenable { - const search = this._activeSearches[requestId]; - if (search) { - delete this._activeSearches[requestId]; - search.cancel(); + const tokenSource = this._activeCancelTokens[requestId]; + if (tokenSource) { + delete this._activeCancelTokens[requestId]; + tokenSource.cancel(); return TPromise.as(true); } return undefined; diff --git a/src/vs/workbench/api/node/extHostSearch.ts b/src/vs/workbench/api/node/extHostSearch.ts index 6d977baabf15254b528298c61bfd54a9ebea747f..7f9300b8f1b5a856f8232860e8efca1b19e4285f 100644 --- a/src/vs/workbench/api/node/extHostSearch.ts +++ b/src/vs/workbench/api/node/extHostSearch.ts @@ -98,19 +98,15 @@ export class ExtHostSearch implements ExtHostSearchShape { }); } else { const indexProvider = this._fileIndexProvider.get(handle); - if (indexProvider) { - return this._fileIndexSearchManager.fileSearch(query, indexProvider, batch => { - this._proxy.$handleFileMatch(handle, session, batch.map(p => p.resource)); - }).then(null, err => { - if (!isPromiseCanceledError(err)) { - throw err; - } + return this._fileIndexSearchManager.fileSearch(query, indexProvider, batch => { + this._proxy.$handleFileMatch(handle, session, batch.map(p => p.resource)); + }).then(null, err => { + if (!isPromiseCanceledError(err)) { + throw err; + } - return null; - }); - } else { - throw new Error('something went wrong'); - } + return null; + }); } } @@ -300,36 +296,31 @@ class BatchedCollector { class TextSearchEngine { - private activeCancellationTokens = new Set(); private collector: TextSearchResultsCollector; private isLimitHit: boolean; private resultCount = 0; - private isCanceled: boolean; constructor(private pattern: IPatternInfo, private config: ISearchQuery, private provider: vscode.TextSearchProvider, private _extfs: typeof extfs) { } - public cancel(): void { - this.isCanceled = true; - this.activeCancellationTokens.forEach(t => t.cancel()); - this.activeCancellationTokens = new Set(); - } - public search(onProgress: (matches: IFileMatch[]) => void): TPromise { const folderQueries = this.config.folderQueries; + const tokenSource = new CancellationTokenSource(); return new TPromise((resolve, reject) => { this.collector = new TextSearchResultsCollector(onProgress); + let isCanceled = false; const onResult = (match: vscode.TextSearchResult, folderIdx: number) => { - if (this.isCanceled) { + if (isCanceled) { return; } if (this.resultCount >= this.config.maxResults) { this.isLimitHit = true; - this.cancel(); + isCanceled = true; + tokenSource.cancel(); } if (!this.isLimitHit) { @@ -340,8 +331,9 @@ class TextSearchEngine { // For each root folder TPromise.join(folderQueries.map((fq, i) => { - return this.searchInFolder(fq, r => onResult(r, i)); + return this.searchInFolder(fq, r => onResult(r, i), tokenSource.token); })).then(() => { + tokenSource.dispose(); this.collector.flush(); resolve({ limitHit: this.isLimitHit, @@ -350,17 +342,20 @@ class TextSearchEngine { } }); }, (errs: Error[]) => { + tokenSource.dispose(); const errMsg = errs .map(err => toErrorMessage(err)) .filter(msg => !!msg)[0]; reject(new Error(errMsg)); }); + }, () => { + // From IPC + tokenSource.cancel(); }); } - private searchInFolder(folderQuery: IFolderQuery, onResult: (result: vscode.TextSearchResult) => void): TPromise { - let cancellation = new CancellationTokenSource(); + private searchInFolder(folderQuery: IFolderQuery, onResult: (result: vscode.TextSearchResult) => void, token: CancellationToken): TPromise { return new TPromise((resolve, reject) => { const queryTester = new QueryGlobTester(this.config, folderQuery); @@ -385,22 +380,14 @@ class TextSearchEngine { const searchOptions = this.getSearchOptionsForFolder(folderQuery); new TPromise(resolve => process.nextTick(resolve)) .then(() => { - this.activeCancellationTokens.add(cancellation); - return this.provider.provideTextSearchResults(patternInfoToQuery(this.pattern), searchOptions, progress, cancellation.token); + return this.provider.provideTextSearchResults(patternInfoToQuery(this.pattern), searchOptions, progress, token); }) .then(() => { - this.activeCancellationTokens.delete(cancellation); return TPromise.join(testingPs); }) .then( - () => { - cancellation.dispose(); - resolve(null); - }, - err => { - cancellation.dispose(); - reject(err); - }); + () => resolve(null), + reject); }); } diff --git a/src/vs/workbench/parts/search/browser/openFileHandler.ts b/src/vs/workbench/parts/search/browser/openFileHandler.ts index 9e925e3b9b76c2c3cdd3cac18ed7e52047f8b4f0..75c48a3de63e0a886a3049de616c9e785296de14 100644 --- a/src/vs/workbench/parts/search/browser/openFileHandler.ts +++ b/src/vs/workbench/parts/search/browser/openFileHandler.ts @@ -172,8 +172,7 @@ export class OpenFileHandler extends QuickOpenHandler { return TPromise.wrap({ results: [{ resource: result }] }); } - // TODO@Rob support cancellation - return this.searchService.search(this.queryBuilder.file(this.contextService.getWorkspace().folders.map(folder => folder.uri), queryOptions)); + return this.searchService.search(this.queryBuilder.file(this.contextService.getWorkspace().folders.map(folder => folder.uri), queryOptions), token); }).then(complete => { const results: QuickOpenEntry[] = []; for (let i = 0; i < complete.results.length; i++) { diff --git a/src/vs/workbench/parts/search/common/searchModel.ts b/src/vs/workbench/parts/search/common/searchModel.ts index 856678135bbe04c26f976afcd8c4ee5ae48a86ce..5e0e28f19e83a8871bcd176f116ca3c26ee5d532 100644 --- a/src/vs/workbench/parts/search/common/searchModel.ts +++ b/src/vs/workbench/parts/search/common/searchModel.ts @@ -24,6 +24,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { overviewRulerFindMatchForeground } from 'vs/platform/theme/common/colorRegistry'; import { themeColorFromId } from 'vs/platform/theme/common/themeService'; import { IReplaceService } from 'vs/workbench/parts/search/common/replace'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; export class Match { @@ -733,7 +734,7 @@ export class SearchModel extends Disposable { private readonly _onReplaceTermChanged: Emitter = this._register(new Emitter()); public readonly onReplaceTermChanged: Event = this._onReplaceTermChanged.event; - private currentRequest: TPromise; + private currentCancelTokenSource: CancellationTokenSource; constructor(@ISearchService private searchService: ISearchService, @ITelemetryService private telemetryService: ITelemetryService, @IInstantiationService private instantiationService: IInstantiationService) { super(); @@ -778,7 +779,8 @@ export class SearchModel extends Disposable { const progressEmitter = new Emitter(); this._replacePattern = new ReplacePattern(this._replaceString, this._searchQuery.contentPattern); - this.currentRequest = this.searchService.search(this._searchQuery, p => { + const tokenSource = this.currentCancelTokenSource = new CancellationTokenSource(); + const currentRequest = this.searchService.search(this._searchQuery, this.currentCancelTokenSource.token, p => { progressEmitter.fire(); this.onSearchProgress(p); @@ -787,7 +789,10 @@ export class SearchModel extends Disposable { } }); - const onDone = fromPromise(this.currentRequest); + const dispose = () => tokenSource.dispose(); + currentRequest.then(dispose, dispose); + + const onDone = fromPromise(currentRequest); const onFirstRender = anyEvent(onDone, progressEmitter.event); const onFirstRenderStopwatch = stopwatch(onFirstRender); /* __GDPR__ @@ -807,18 +812,14 @@ export class SearchModel extends Disposable { */ onDoneStopwatch(duration => this.telemetryService.publicLog('searchResultsFinished', { duration })); - const currentRequest = this.currentRequest; - this.currentRequest.then( + currentRequest.then( value => this.onSearchCompleted(value, Date.now() - start), e => this.onSearchError(e, Date.now() - start)); - // this.currentRequest may be completed (and nulled) immediately return currentRequest; } private onSearchCompleted(completed: ISearchComplete, duration: number): ISearchComplete { - this.currentRequest = null; - const options: IPatternInfo = objects.assign({}, this._searchQuery.contentPattern); delete options.pattern; @@ -866,9 +867,8 @@ export class SearchModel extends Disposable { } public cancelSearch(): boolean { - if (this.currentRequest) { - this.currentRequest.cancel(); - this.currentRequest = null; + if (this.currentCancelTokenSource) { + this.currentCancelTokenSource.cancel(); return true; } return false; diff --git a/src/vs/workbench/parts/search/test/common/searchModel.test.ts b/src/vs/workbench/parts/search/test/common/searchModel.test.ts index d0227ec3794bced77cb2084a47e78dd005a8be98..3e7e4f5ca129b0230c5ea63c4d1d9a5a0f238106 100644 --- a/src/vs/workbench/parts/search/test/common/searchModel.test.ts +++ b/src/vs/workbench/parts/search/test/common/searchModel.test.ts @@ -20,6 +20,7 @@ import { IFileMatch, IFileSearchStats, IFolderQuery, ISearchComplete, ISearchPro import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils'; import { SearchModel } from 'vs/workbench/parts/search/common/searchModel'; +import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; const nullEvent = new class { @@ -83,7 +84,7 @@ suite('SearchModel', () => { function searchServiceWithResults(results: IFileMatch[], complete: ISearchComplete = null): ISearchService { return { - search(query: ISearchQuery, onProgress: (result: ISearchProgressItem) => void): TPromise { + search(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): TPromise { return new TPromise(resolve => { process.nextTick(() => { results.forEach(onProgress); @@ -96,7 +97,7 @@ suite('SearchModel', () => { function searchServiceWithError(error: Error): ISearchService { return { - search(query: ISearchQuery, onProgress: (result: ISearchProgressItem) => void): TPromise { + search(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): TPromise { return new TPromise((resolve, reject) => { reject(error); }); @@ -104,6 +105,22 @@ suite('SearchModel', () => { }; } + function canceleableSearchService(tokenSource: CancellationTokenSource): ISearchService { + return { + search(query: ISearchQuery, token?: CancellationToken, onProgress?: (result: ISearchProgressItem) => void): TPromise { + if (token) { + token.onCancellationRequested(() => tokenSource.cancel()); + } + + return new TPromise(resolve => { + process.nextTick(() => { + resolve({}); + }); + }); + } + }; + } + test('Search Model: Search adds to results', async () => { let results = [ aRawMatch('file://c:/1', @@ -257,15 +274,15 @@ suite('SearchModel', () => { }); test('Search Model: Previous search is cancelled when new search is called', async () => { - let target = sinon.spy(); - instantiationService.stub(ISearchService, 'search', new DeferredTPromise(target)); - let testObject: SearchModel = instantiationService.createInstance(SearchModel); + const tokenSource = new CancellationTokenSource(); + instantiationService.stub(ISearchService, canceleableSearchService(tokenSource)); + const testObject: SearchModel = instantiationService.createInstance(SearchModel); testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); instantiationService.stub(ISearchService, searchServiceWithResults([])); testObject.search({ contentPattern: { pattern: 'somestring' }, type: 1, folderQueries }); - assert.ok(target.calledOnce); + assert.ok(tokenSource.token.isCancellationRequested); }); test('getReplaceString returns proper replace string for regExpressions', async () => { diff --git a/src/vs/workbench/services/search/node/searchService.ts b/src/vs/workbench/services/search/node/searchService.ts index 46b09f6f8ff35688bf158301f9a891c0f4858287..8d8c55c6a2d858794286fd0e9bf996cda7b26561 100644 --- a/src/vs/workbench/services/search/node/searchService.ts +++ b/src/vs/workbench/services/search/node/searchService.ts @@ -30,6 +30,8 @@ import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/un import { IRawSearch, IRawSearchService, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, isSerializedSearchComplete, isSerializedSearchSuccess } from './search'; import { ISearchChannel, SearchChannelClient } from './searchIpc'; import { Range } from 'vs/editor/common/core/range'; +import { CancellationToken } from 'vs/base/common/cancellation'; +import { canceled } from 'vs/base/common/errors'; export class SearchService extends Disposable implements ISearchService { public _serviceBrand: any; @@ -92,65 +94,62 @@ export class SearchService extends Disposable implements ISearchService { } } - public search(query: ISearchQuery, onProgress?: (item: ISearchProgressItem) => void): TPromise { - let combinedPromise: TPromise; + public search(query: ISearchQuery, token?: CancellationToken, onProgress?: (item: ISearchProgressItem) => void): TPromise { + // Get local results from dirty/untitled + const localResults = this.getLocalResults(query); - return new TPromise((onComplete, onError) => { - - // Get local results from dirty/untitled - const localResults = this.getLocalResults(query); - - if (onProgress) { - localResults.values().filter((res) => !!res).forEach(onProgress); - } + if (onProgress) { + localResults.values().filter((res) => !!res).forEach(onProgress); + } - this.logService.trace('SearchService#search', JSON.stringify(query)); + this.logService.trace('SearchService#search', JSON.stringify(query)); - const onProviderProgress = progress => { - if (progress.resource) { - // Match - if (!localResults.has(progress.resource) && onProgress) { // don't override local results - onProgress(progress); - } - } else if (onProgress) { - // Progress - onProgress(progress); + const onProviderProgress = progress => { + if (progress.resource) { + // Match + if (!localResults.has(progress.resource) && onProgress) { // don't override local results + onProgress(progress); } + } else if (onProgress) { + // Progress + onProgress(progress); + } - if (progress.message) { - this.logService.debug('SearchService#search', progress.message); - } - }; + if (progress.message) { + this.logService.debug('SearchService#search', progress.message); + } + }; - const schemesInQuery = this.getSchemesInQuery(query); + const schemesInQuery = this.getSchemesInQuery(query); - const providerActivations: TPromise[] = [TPromise.wrap(null)]; - schemesInQuery.forEach(scheme => providerActivations.push(this.extensionService.activateByEvent(`onSearch:${scheme}`))); + const providerActivations: TPromise[] = [TPromise.wrap(null)]; + schemesInQuery.forEach(scheme => providerActivations.push(this.extensionService.activateByEvent(`onSearch:${scheme}`))); - const providerPromise = TPromise.join(providerActivations) - .then(() => this.extensionService.whenInstalledExtensionsRegistered()) - .then(() => this.searchWithProviders(query, onProviderProgress)) - .then(completes => { - completes = completes.filter(c => !!c); - if (!completes.length) { - return null; - } + const providerPromise = TPromise.join(providerActivations) + .then(() => this.extensionService.whenInstalledExtensionsRegistered()) + .then(() => this.searchWithProviders(query, onProviderProgress, token)) + .then(completes => { + completes = completes.filter(c => !!c); + if (!completes.length) { + return null; + } - return { - limitHit: completes[0] && completes[0].limitHit, - stats: completes[0].stats, - results: arrays.flatten(completes.map(c => c.results)) - }; - }, errs => { - if (!Array.isArray(errs)) { - errs = [errs]; - } + return { + limitHit: completes[0] && completes[0].limitHit, + stats: completes[0].stats, + results: arrays.flatten(completes.map(c => c.results)) + }; + }, errs => { + if (!Array.isArray(errs)) { + errs = [errs]; + } - errs = errs.filter(e => !!e); - return TPromise.wrapError(errs[0]); - }); + errs = errs.filter(e => !!e); + return TPromise.wrapError(errs[0]); + }); - combinedPromise = providerPromise.then(value => { + return new TPromise((c, e) => { + providerPromise.then(value => { const values = [value]; const result: ISearchComplete = { @@ -177,10 +176,11 @@ export class SearchService extends Disposable implements ISearchService { } return result; + }).then(c, e); + }, () => { + // Need the OpenAnythingHandler to stop trying to cancel this promise, https://github.com/Microsoft/vscode/issues/56137 + }); - }).then(onComplete, onError); - - }, () => combinedPromise && combinedPromise.cancel()); } private getSchemesInQuery(query: ISearchQuery): Set { @@ -196,7 +196,7 @@ export class SearchService extends Disposable implements ISearchService { return schemes; } - private searchWithProviders(query: ISearchQuery, onProviderProgress: (progress: ISearchProgressItem) => void) { + private searchWithProviders(query: ISearchQuery, onProviderProgress: (progress: ISearchProgressItem) => void, token?: CancellationToken) { const e2eSW = StopWatch.create(false); const diskSearchQueries: IFolderQuery[] = []; @@ -219,7 +219,7 @@ export class SearchService extends Disposable implements ISearchService { } }; - searchPs.push(provider.search(oneFolderQuery, onProviderProgress)); + searchPs.push(provider.search(oneFolderQuery, onProviderProgress, token)); } }); @@ -234,7 +234,7 @@ export class SearchService extends Disposable implements ISearchService { extraFileResources: diskSearchExtraFileResources }; - searchPs.push(this.diskSearch.search(diskSearchQuery, onProviderProgress)); + searchPs.push(this.diskSearch.search(diskSearchQuery, onProviderProgress, token)); } return TPromise.join(searchPs).then(completes => { @@ -447,7 +447,7 @@ export class DiskSearch implements ISearchResultProvider { this.raw = new SearchChannelClient(channel); } - public search(query: ISearchQuery, onProgress?: (p: ISearchProgressItem) => void): TPromise { + public search(query: ISearchQuery, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): TPromise { const folderQueries = query.folderQueries || []; return TPromise.join(folderQueries.map(q => q.folder.scheme === Schemas.file && pfs.exists(q.folder.fsPath))) .then(exists => { @@ -461,7 +461,7 @@ export class DiskSearch implements ISearchResultProvider { event = this.raw.textSearch(rawSearch); } - return DiskSearch.collectResultsFromEvent(event, onProgress); + return DiskSearch.collectResultsFromEvent(event, onProgress, token); }); } @@ -507,11 +507,21 @@ export class DiskSearch implements ISearchResultProvider { return rawSearch; } - public static collectResultsFromEvent(event: Event, onProgress?: (p: ISearchProgressItem) => void): TPromise { + public static collectResultsFromEvent(event: Event, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): TPromise { let result: IFileMatch[] = []; let listener: IDisposable; return new TPromise((c, e) => { + if (token) { + token.onCancellationRequested(() => { + if (listener) { + listener.dispose(); + } + + e(canceled()); + }); + } + listener = event(ev => { if (isSerializedSearchComplete(ev)) { if (isSerializedSearchSuccess(ev)) { @@ -551,8 +561,7 @@ export class DiskSearch implements ISearchResultProvider { } } }); - }, - () => listener && listener.dispose()); + }); } private static createFileMatch(data: ISerializedFileMatch): FileMatch {