/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from 'vs/base/common/cancellation'; import { dispose, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { values } from 'vs/base/common/map'; import { URI, UriComponents } from 'vs/base/common/uri'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { IFileMatch, IFileQuery, IRawFileMatch2, ISearchComplete, ISearchCompleteStats, ISearchProgressItem, ISearchResultProvider, ISearchService, ITextQuery, QueryType, SearchProviderType } from 'vs/workbench/services/search/common/search'; import { ExtHostContext, ExtHostSearchShape, IExtHostContext, MainContext, MainThreadSearchShape } from '../common/extHost.protocol'; @extHostNamedCustomer(MainContext.MainThreadSearch) export class MainThreadSearch implements MainThreadSearchShape { private readonly _proxy: ExtHostSearchShape; private readonly _searchProvider = new Map(); constructor( extHostContext: IExtHostContext, @ISearchService private readonly _searchService: ISearchService, @ITelemetryService private readonly _telemetryService: ITelemetryService ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostSearch); } dispose(): void { this._searchProvider.forEach(value => value.dispose()); this._searchProvider.clear(); } $registerTextSearchProvider(handle: number, scheme: string): void { this._searchProvider.set(handle, new RemoteSearchProvider(this._searchService, SearchProviderType.text, scheme, handle, this._proxy)); } $registerFileSearchProvider(handle: number, scheme: string): void { this._searchProvider.set(handle, new RemoteSearchProvider(this._searchService, SearchProviderType.file, scheme, handle, this._proxy)); } $unregisterProvider(handle: number): void { dispose(this._searchProvider.get(handle)); this._searchProvider.delete(handle); } $handleFileMatch(handle: number, session: number, data: UriComponents[]): void { const provider = this._searchProvider.get(handle); if (!provider) { throw new Error('Got result for unknown provider'); } provider.handleFindMatch(session, data); } $handleTextMatch(handle: number, session: number, data: IRawFileMatch2[]): void { const provider = this._searchProvider.get(handle); if (!provider) { throw new Error('Got result for unknown provider'); } provider.handleFindMatch(session, data); } $handleTelemetry(eventName: string, data: any): void { this._telemetryService.publicLog(eventName, data); } } class SearchOperation { private static _idPool = 0; constructor( readonly progress?: (match: IFileMatch) => any, readonly id: number = ++SearchOperation._idPool, readonly matches = new Map() ) { // } addMatch(match: IFileMatch): void { if (this.matches.has(match.resource.toString())) { // Merge with previous IFileMatches // TODO@rob clean up text/file result types this.matches.get(match.resource.toString())!.results!.push(...match.results!); } else { this.matches.set(match.resource.toString(), match); } if (this.progress) { this.progress(match); } } } class RemoteSearchProvider implements ISearchResultProvider, IDisposable { private readonly _registrations = new DisposableStore(); private readonly _searches = new Map(); constructor( searchService: ISearchService, type: SearchProviderType, private readonly _scheme: string, private readonly _handle: number, private readonly _proxy: ExtHostSearchShape ) { this._registrations.add(searchService.registerSearchResultProvider(this._scheme, type, this)); } dispose(): void { this._registrations.dispose(); } fileSearch(query: IFileQuery, token: CancellationToken = CancellationToken.None): Promise { return this.doSearch(query, undefined, token); } textSearch(query: ITextQuery, onProgress?: (p: ISearchProgressItem) => void, token: CancellationToken = CancellationToken.None): Promise { return this.doSearch(query, onProgress, token); } doSearch(query: ITextQuery | IFileQuery, onProgress?: (p: ISearchProgressItem) => void, token: CancellationToken = CancellationToken.None): Promise { if (!query.folderQueries.length) { throw new Error('Empty folderQueries'); } 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, token) : this._proxy.$provideTextSearchResults(this._handle, search.id, query, token); return Promise.resolve(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 Promise.reject(err); }); } clearCache(cacheKey: string): Promise { return Promise.resolve(this._proxy.$clearCache(cacheKey)); } handleFindMatch(session: number, dataOrUri: Array): void { const searchOp = this._searches.get(session); if (!searchOp) { // ignore... return; } dataOrUri.forEach(result => { if ((result).results) { searchOp.addMatch({ resource: URI.revive((result).resource), results: (result).results }); } else { searchOp.addMatch({ resource: URI.revive(result) }); } }); } }