src/vs/workbench/services/search/node/searchService.ts (163 lines of code) (raw):
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getPathFromAmdModule } from 'vs/base/common/amd';
import { CancellationToken } from 'vs/base/common/cancellation';
import { canceled } from 'vs/base/common/errors';
import { Event } from 'vs/base/common/event';
import { IDisposable } from 'vs/base/common/lifecycle';
import { URI as uri } from 'vs/base/common/uri';
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
import { Client, IIPCOptions } from 'vs/base/parts/ipc/node/ipc.cp';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IDebugParams, IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { ILogService } from 'vs/platform/log/common/log';
import { FileMatch, IFileMatch, IFileQuery, IProgressMessage, IRawSearchService, ISearchComplete, ISearchConfiguration, ISearchProgressItem, ISearchResultProvider, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, isSerializedSearchComplete, isSerializedSearchSuccess, ITextQuery, ISearchService } from 'vs/workbench/services/search/common/search';
import { SearchChannelClient } from './searchIpc';
import { SearchService } from 'vs/workbench/services/search/common/searchService';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IModelService } from 'vs/editor/common/services/modelService';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
export class LocalSearchService extends SearchService {
constructor(
@IModelService modelService: IModelService,
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
@IEditorService editorService: IEditorService,
@ITelemetryService telemetryService: ITelemetryService,
@ILogService logService: ILogService,
@IExtensionService extensionService: IExtensionService,
@IFileService fileService: IFileService,
@IEnvironmentService readonly environmentService: IEnvironmentService,
@IInstantiationService readonly instantiationService: IInstantiationService
) {
super(modelService, untitledEditorService, editorService, telemetryService, logService, extensionService, fileService);
this.diskSearch = instantiationService.createInstance(DiskSearch, !environmentService.isBuilt || environmentService.verbose, environmentService.debugSearch);
}
}
export class DiskSearch implements ISearchResultProvider {
private raw: IRawSearchService;
constructor(
verboseLogging: boolean,
searchDebug: IDebugParams | undefined,
@ILogService private readonly logService: ILogService,
@IConfigurationService private readonly configService: IConfigurationService,
@IFileService private readonly fileService: IFileService
) {
const timeout = this.configService.getValue<ISearchConfiguration>().search.maintainFileSearchCache ?
Number.MAX_VALUE :
60 * 60 * 1000;
const opts: IIPCOptions = {
serverName: 'Search',
timeout,
args: ['--type=searchService'],
// See https://github.com/Microsoft/vscode/issues/27665
// Pass in fresh execArgv to the forked process such that it doesn't inherit them from `process.execArgv`.
// e.g. Launching the extension host process with `--inspect-brk=xxx` and then forking a process from the extension host
// results in the forked process inheriting `--inspect-brk=xxx`.
freshExecArgv: true,
env: {
AMD_ENTRYPOINT: 'vs/workbench/services/search/node/searchApp',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: verboseLogging
},
useQueue: true
};
if (searchDebug) {
if (searchDebug.break && searchDebug.port) {
opts.debugBrk = searchDebug.port;
} else if (!searchDebug.break && searchDebug.port) {
opts.debug = searchDebug.port;
}
}
const client = new Client(
getPathFromAmdModule(require, 'bootstrap-fork'),
opts);
const channel = getNextTickChannel(client.getChannel('search'));
this.raw = new SearchChannelClient(channel);
}
textSearch(query: ITextQuery, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): Promise<ISearchComplete> {
const folderQueries = query.folderQueries || [];
return Promise.all(folderQueries.map(q => this.fileService.exists(q.folder)))
.then(exists => {
if (token && token.isCancellationRequested) {
throw canceled();
}
query.folderQueries = folderQueries.filter((q, index) => exists[index]);
const event: Event<ISerializedSearchProgressItem | ISerializedSearchComplete> = this.raw.textSearch(query);
return DiskSearch.collectResultsFromEvent(event, onProgress, token);
});
}
fileSearch(query: IFileQuery, token?: CancellationToken): Promise<ISearchComplete> {
const folderQueries = query.folderQueries || [];
return Promise.all(folderQueries.map(q => this.fileService.exists(q.folder)))
.then(exists => {
if (token && token.isCancellationRequested) {
throw canceled();
}
query.folderQueries = folderQueries.filter((q, index) => exists[index]);
let event: Event<ISerializedSearchProgressItem | ISerializedSearchComplete>;
event = this.raw.fileSearch(query);
const onProgress = (p: IProgressMessage) => {
if (p.message) {
// Should only be for logs
this.logService.debug('SearchService#search', p.message);
}
};
return DiskSearch.collectResultsFromEvent(event, onProgress, token);
});
}
/**
* Public for test
*/
static collectResultsFromEvent(event: Event<ISerializedSearchProgressItem | ISerializedSearchComplete>, onProgress?: (p: ISearchProgressItem) => void, token?: CancellationToken): Promise<ISearchComplete> {
let result: IFileMatch[] = [];
let listener: IDisposable;
return new Promise<ISearchComplete>((c, e) => {
if (token) {
token.onCancellationRequested(() => {
if (listener) {
listener.dispose();
}
e(canceled());
});
}
listener = event(ev => {
if (isSerializedSearchComplete(ev)) {
if (isSerializedSearchSuccess(ev)) {
c({
limitHit: ev.limitHit,
results: result,
stats: ev.stats
});
} else {
e(ev.error);
}
listener.dispose();
} else {
// Matches
if (Array.isArray(ev)) {
const fileMatches = ev.map(d => this.createFileMatch(d));
result = result.concat(fileMatches);
if (onProgress) {
fileMatches.forEach(onProgress);
}
}
// Match
else if ((<ISerializedFileMatch>ev).path) {
const fileMatch = this.createFileMatch(<ISerializedFileMatch>ev);
result.push(fileMatch);
if (onProgress) {
onProgress(fileMatch);
}
}
// Progress
else if (onProgress) {
onProgress(<IProgressMessage>ev);
}
}
});
});
}
private static createFileMatch(data: ISerializedFileMatch): FileMatch {
const fileMatch = new FileMatch(uri.file(data.path));
if (data.results) {
// const matches = data.results.filter(resultIsMatch);
fileMatch.results.push(...data.results);
}
return fileMatch;
}
clearCache(cacheKey: string): Promise<void> {
return this.raw.clearCache(cacheKey);
}
}
registerSingleton(ISearchService, LocalSearchService, true);