提交 cc4217cc 编写于 作者: R Rob Lourens

Delete legacy file search - Fix #65337

上级 adf0b196
......@@ -317,7 +317,6 @@ export class OneLineRange extends SearchRange {
export interface ISearchConfigurationProperties {
exclude: glob.IExpression;
useRipgrep: boolean;
useLegacySearch: boolean;
/**
* Use ignore file for file search.
*/
......
......@@ -36,32 +36,32 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { TreeResourceNavigator2, WorkbenchObjectTree } from 'vs/platform/list/browser/listService';
import { INotificationService, Severity } from 'vs/platform/notification/common/notification';
import { IProgressService } from 'vs/platform/progress/common/progress';
import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ISearchHistoryService, ISearchHistoryValues, ISearchProgressItem, ITextQuery, SearchErrorCode, VIEW_ID, IProgress } from 'vs/platform/search/common/search';
import { IPatternInfo, ISearchComplete, ISearchConfiguration, ISearchConfigurationProperties, ISearchHistoryService, ISearchHistoryValues, ITextQuery, SearchErrorCode, VIEW_ID } from 'vs/platform/search/common/search';
import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { diffInserted, diffInsertedOutline, diffRemoved, diffRemovedOutline, editorFindMatchHighlight, editorFindMatchHighlightBorder, listActiveSelectionForeground } from 'vs/platform/theme/common/colorRegistry';
import { ICssStyleCollector, ITheme, IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { OpenFileFolderAction, OpenFolderAction } from 'vs/workbench/browser/actions/workspaceActions';
import { ResourceLabels } from 'vs/workbench/browser/labels';
import { Viewlet } from 'vs/workbench/browser/viewlet';
import { IEditor } from 'vs/workbench/common/editor';
import { IPanel } from 'vs/workbench/common/panel';
import { IViewlet } from 'vs/workbench/common/viewlet';
import { ExcludePatternInputWidget, PatternInputWidget } from 'vs/workbench/parts/search/browser/patternInputWidget';
import { CancelSearchAction, ClearSearchResultsAction, CollapseDeepestExpandedLevelAction, RefreshAction, getKeyboardEventForEditorOpen } from 'vs/workbench/parts/search/browser/searchActions';
import { FileMatchRenderer, FolderMatchRenderer, MatchRenderer, SearchDelegate, SearchAccessibilityProvider } from 'vs/workbench/parts/search/browser/searchResultsView';
import { CancelSearchAction, ClearSearchResultsAction, CollapseDeepestExpandedLevelAction, getKeyboardEventForEditorOpen, RefreshAction } from 'vs/workbench/parts/search/browser/searchActions';
import { FileMatchRenderer, FolderMatchRenderer, MatchRenderer, SearchAccessibilityProvider, SearchDelegate } from 'vs/workbench/parts/search/browser/searchResultsView';
import { ISearchWidgetOptions, SearchWidget } from 'vs/workbench/parts/search/browser/searchWidget';
import * as Constants from 'vs/workbench/parts/search/common/constants';
import { ITextQueryBuilderOptions, QueryBuilder } from 'vs/workbench/parts/search/common/queryBuilder';
import { IReplaceService } from 'vs/workbench/parts/search/common/replace';
import { getOutOfWorkspaceEditorResources } from 'vs/workbench/parts/search/common/search';
import { FileMatch, FileMatchOrMatch, FolderMatch, IChangeEvent, ISearchWorkbenchService, Match, RenderableMatch, SearchModel, SearchResult, searchMatchComparer } from 'vs/workbench/parts/search/common/searchModel';
import { FileMatch, FileMatchOrMatch, FolderMatch, IChangeEvent, ISearchWorkbenchService, Match, RenderableMatch, searchMatchComparer, SearchModel, SearchResult } from 'vs/workbench/parts/search/common/searchModel';
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from 'vs/workbench/services/editor/common/editorService';
import { IEditorGroupsService } from 'vs/workbench/services/group/common/editorGroupsService';
import { IPartService } from 'vs/workbench/services/part/common/partService';
import { IPreferencesService, ISettingsEditorOptions } from 'vs/workbench/services/preferences/common/preferences';
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
import { ResourceLabels } from 'vs/workbench/browser/labels';
const $ = dom.$;
......@@ -1241,13 +1241,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel {
}
private doSearch(query: ITextQuery, options: ITextQueryBuilderOptions, excludePatternText: string, includePatternText: string): Thenable<void> {
// Progress total is 100.0% for more progress bar granularity
const progressTotal = 1000;
let progressWorked = 0;
const progressRunner = query.useRipgrep ?
this.progressService.show(/*infinite=*/true) :
this.progressService.show(progressTotal);
const progressRunner = this.progressService.show(/*infinite=*/true);
this.searchWidget.searchInput.clearMessage();
this.searching = true;
......@@ -1263,12 +1257,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel {
this.searching = false;
// Complete up to 100% as needed
if (completed && !query.useRipgrep) {
progressRunner.worked(progressTotal - progressWorked);
setTimeout(() => progressRunner.done(), 200);
} else {
progressRunner.done();
}
progressRunner.done();
// Do final render, then expand if just 1 file with less than 50 matches
this.onSearchResultsChanged();
......@@ -1370,13 +1359,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel {
this.searchWidget.searchInput.showMessage({ content: e.message, type: MessageType.ERROR });
this.viewModel.searchResult.clear();
if (e.code === SearchErrorCode.unknownEncoding && !this.configurationService.getValue('search.useLegacySearch')) {
this.notificationService.prompt(Severity.Info, nls.localize('otherEncodingWarning', "You can enable \"search.useLegacySearch\" to search non-standard file encodings."),
[{
label: nls.localize('otherEncodingWarning.openSettingsLabel', "Open Settings"),
run: () => this.openSettings('search.useLegacySearch')
}]);
} else if (e.code === SearchErrorCode.regexParseError && !this.configurationService.getValue('search.usePCRE2')) {
if (e.code === SearchErrorCode.regexParseError && !this.configurationService.getValue('search.usePCRE2')) {
this.showPcre2Hint();
}
......@@ -1384,18 +1367,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel {
}
};
let total: number = 0;
let worked: number = 0;
let visibleMatches = 0;
const onProgress = (p: ISearchProgressItem) => {
// Progress
if ((<IProgress>p).total) {
total = (<IProgress>p).total;
}
if ((<IProgress>p).worked) {
worked = (<IProgress>p).worked;
}
};
// Handle UI updates in an interval to show frequent progress and results
const uiRefreshHandle: any = setInterval(() => {
......@@ -1404,30 +1376,6 @@ export class SearchView extends Viewlet implements IViewlet, IPanel {
return;
}
if (!query.useRipgrep) {
// Progress bar update
let fakeProgress = true;
if (total > 0 && worked > 0) {
const ratio = Math.round((worked / total) * progressTotal);
if (ratio > progressWorked) { // never show less progress than what we have already
progressRunner.worked(ratio - progressWorked);
progressWorked = ratio;
fakeProgress = false;
}
}
// Fake progress up to 90%, or when actual progress beats it
const fakeMax = 900;
const fakeMultiplier = 12;
if (fakeProgress && progressWorked < fakeMax) {
// Linearly decrease the rate of fake progress.
// 1 is the smallest allowed amount of progress.
const fakeAmt = Math.round((fakeMax - progressWorked) / fakeMax * fakeMultiplier) || 1;
progressWorked += fakeAmt;
progressRunner.worked(fakeAmt);
}
}
// Search result tree update
const fileCount = this.viewModel.searchResult.fileCount();
if (visibleMatches !== fileCount) {
......@@ -1441,7 +1389,7 @@ export class SearchView extends Viewlet implements IViewlet, IPanel {
this.searchWidget.setReplaceAllActionState(false);
return this.viewModel.search(query, onProgress)
return this.viewModel.search(query)
.then(onComplete, onError);
}
......
......@@ -51,7 +51,6 @@ export interface ICommonQueryBuilderOptions {
maxResults?: number;
maxFileSize?: number;
useRipgrep?: boolean;
disregardIgnoreFiles?: boolean;
disregardGlobalIgnoreFiles?: boolean;
disregardExcludeSettings?: boolean;
......@@ -150,11 +149,6 @@ export class QueryBuilder {
folderResources.map(uri => this.getFolderQueryForRoot(uri, options, excludePattern)))
.filter(query => !!query) as IFolderQuery[];
// const useRipgrep = !folderResources || folderResources.every(folder => {
// const folderConfig = this.configurationService.getValue<ISearchConfiguration>({ resource: folder });
// return !folderConfig.search.useLegacySearch;
// });
const queryProps: ICommonQueryProps<uri> = {
_reason: options._reason,
folderQueries,
......@@ -163,8 +157,7 @@ export class QueryBuilder {
excludePattern: excludePattern.pattern,
includePattern,
maxResults: options.maxResults,
useRipgrep: true
maxResults: options.maxResults
};
// Filter extraFileResources against global include/exclude patterns - they are already expected to not belong to a workspace
......
......@@ -960,7 +960,6 @@ export class SearchModel extends Disposable {
"fileCount": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"options": { "${inline}": [ "${IPatternInfo}" ] },
"duration": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth", "isMeasurement": true },
"useRipgrep": { "classification": "SystemMetaData", "purpose": "FeatureInsight", "isMeasurement": true },
"type" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" },
"scheme" : { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" }
}
......@@ -970,7 +969,6 @@ export class SearchModel extends Disposable {
fileCount: this._searchResult.fileCount(),
options,
duration,
useRipgrep: this._searchQuery.useRipgrep,
type: stats && stats.type,
scheme
});
......
......@@ -618,11 +618,6 @@ configurationRegistry.registerConfiguration({
deprecationMessage: nls.localize('useRipgrepDeprecated', "Deprecated. Consider \"search.usePCRE2\" for advanced regex feature support."),
default: true
},
'search.useLegacySearch': {
type: 'boolean',
description: nls.localize('useLegacySearch', "Controls whether to use the deprecated legacy mode for text and file search. It supports some text encodings that are not supported by the standard ripgrep-based search."),
default: false
},
'search.useIgnoreFiles': {
type: 'boolean',
markdownDescription: nls.localize('useIgnoreFiles', "Controls whether to use `.gitignore` and `.ignore` files when searching for files."),
......
......@@ -75,7 +75,7 @@ export class FileWalker {
constructor(config: IFileQuery, maxFileSize?: number) {
this.config = config;
this.useRipgrep = config.useRipgrep !== false;
this.useRipgrep = true;
this.filePattern = config.filePattern || '';
this.includePattern = config.includePattern && glob.parse(config.includePattern);
this.maxResults = config.maxResults || null;
......
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as gracefulFs from 'graceful-fs';
import { CancellationToken } from 'vs/base/common/cancellation';
import { MAX_FILE_SIZE } from 'vs/platform/files/node/files';
import { ITextQuery, QueryType } from 'vs/platform/search/common/search';
import { FileWalker } from 'vs/workbench/services/search/node/fileSearch';
import { Engine } from 'vs/workbench/services/search/node/legacy/textSearch';
import { TextSearchWorkerProvider } from 'vs/workbench/services/search/node/legacy/textSearchWorkerProvider';
import { BatchedCollector } from 'vs/workbench/services/search/node/textSearchManager';
import { ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess } from '../search';
gracefulFs.gracefulify(fs);
type IProgressCallback = (p: ISerializedSearchProgressItem) => void;
export class LegacyTextSearchService {
private static readonly BATCH_SIZE = 512;
private textSearchWorkerProvider: TextSearchWorkerProvider;
textSearch(config: ITextQuery, progressCallback: IProgressCallback, token?: CancellationToken): Promise<ISerializedSearchComplete> {
if (!this.textSearchWorkerProvider) {
this.textSearchWorkerProvider = new TextSearchWorkerProvider();
}
let engine = new Engine(
config,
new FileWalker({
type: QueryType.File,
folderQueries: config.folderQueries,
extraFileResources: config.extraFileResources,
includePattern: config.includePattern,
excludePattern: config.excludePattern,
useRipgrep: false,
filePattern: ''
}, MAX_FILE_SIZE),
this.textSearchWorkerProvider);
return this.doTextSearch(engine, progressCallback, LegacyTextSearchService.BATCH_SIZE, token);
}
private doTextSearch(engine: Engine, progressCallback: IProgressCallback, batchSize: number, token?: CancellationToken): Promise<ISerializedSearchSuccess> {
if (token) {
token.onCancellationRequested(() => engine.cancel());
}
return new Promise<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, progressCallback);
engine.search((matches) => {
const totalMatches = matches.reduce((acc, m) => acc + m.numMatches!, 0);
collector.addItems(matches, totalMatches);
}, (progress) => {
progressCallback(progress);
}, (error, stats) => {
collector.flush();
if (error) {
e(error);
} else {
c({
type: 'success',
limitHit: stats.limitHit,
stats: null
});
}
});
});
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as glob from 'vs/base/common/glob';
import { IPatternInfo, ITextSearchPreviewOptions } from 'vs/platform/search/common/search';
export interface IFolderSearch {
folder: string;
excludePattern?: glob.IExpression;
includePattern?: glob.IExpression;
fileEncoding?: string;
disregardIgnoreFiles?: boolean;
disregardGlobalIgnoreFiles?: boolean;
}
export interface IRawSearch {
folderQueries: IFolderSearch[];
ignoreSymlinks?: boolean;
extraFiles?: string[];
filePattern?: string;
excludePattern?: glob.IExpression;
includePattern?: glob.IExpression;
contentPattern: IPatternInfo;
maxResults?: number;
exists?: boolean;
sortByScore?: boolean;
cacheKey?: string;
maxFilesize?: number;
useRipgrep?: boolean;
disregardIgnoreFiles?: boolean;
previewOptions?: ITextSearchPreviewOptions;
disregardGlobalIgnoreFiles?: boolean;
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'path';
import { onUnexpectedError } from 'vs/base/common/errors';
import { IProgress, ITextQuery } from 'vs/platform/search/common/search';
import { FileWalker } from 'vs/workbench/services/search/node/fileSearch';
import { ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch } from '../search';
import { ITextSearchWorkerProvider } from './textSearchWorkerProvider';
import { ISearchWorker, ISearchWorkerSearchArgs } from './worker/searchWorkerIpc';
import { IRawSearch } from 'vs/workbench/services/search/node/legacy/search';
export class Engine implements ISearchEngine<ISerializedFileMatch[]> {
private static readonly PROGRESS_FLUSH_CHUNK_SIZE = 50; // optimization: number of files to process before emitting progress event
private config: IRawSearch;
private config2: ITextQuery;
private walker: FileWalker;
private walkerError: Error | null;
private isCanceled = false;
private isDone = false;
private totalBytes = 0;
private processedBytes = 0;
private progressed = 0;
private walkerIsDone = false;
private limitReached = false;
private numResults = 0;
private workerProvider: ITextSearchWorkerProvider;
private workers: ISearchWorker[];
private nextWorker = 0;
constructor(config: ITextQuery, walker: FileWalker, workerProvider: ITextSearchWorkerProvider) {
this.config = makeRawSearch(config);
this.config2 = config;
this.walker = walker;
this.workerProvider = workerProvider;
}
cancel(): void {
this.isCanceled = true;
this.walker.cancel();
this.workers.forEach(w => {
w.cancel()
.then(undefined, onUnexpectedError);
});
}
initializeWorkers(): void {
this.workers.forEach(w => {
w.initialize()
.then(undefined, onUnexpectedError);
});
}
search(onResult: (match: ISerializedFileMatch[]) => void, onProgress: (progress: IProgress) => void, done: (error: Error | null, complete: ISearchEngineSuccess) => void): void {
this.workers = this.workerProvider.getWorkers();
this.initializeWorkers();
const fileEncoding = this.config.folderQueries.length === 1 ?
this.config.folderQueries[0].fileEncoding || 'utf8' :
'utf8';
const progress = () => {
if (++this.progressed % Engine.PROGRESS_FLUSH_CHUNK_SIZE === 0) {
onProgress({ total: this.totalBytes, worked: this.processedBytes }); // buffer progress in chunks to reduce pressure
}
};
const unwind = (processed: number) => {
this.processedBytes += processed;
// Emit progress() unless we got canceled or hit the limit
if (processed && !this.isDone && !this.isCanceled && !this.limitReached) {
progress();
}
// Emit done()
if (!this.isDone && this.processedBytes === this.totalBytes && this.walkerIsDone) {
this.isDone = true;
done(this.walkerError, {
limitHit: this.limitReached,
stats: this.walker.getStats()
});
}
};
const run = (batch: string[], batchBytes: number): void => {
const worker = this.workers[this.nextWorker];
this.nextWorker = (this.nextWorker + 1) % this.workers.length;
const maxResults = this.config.maxResults && (this.config.maxResults - this.numResults);
const searchArgs: ISearchWorkerSearchArgs = { absolutePaths: batch, maxResults, pattern: this.config.contentPattern, fileEncoding, previewOptions: this.config.previewOptions };
worker.search(searchArgs).then(result => {
if (!result || this.limitReached || this.isCanceled) {
return unwind(batchBytes);
}
const matches = result.matches;
onResult(matches);
this.numResults += result.numMatches;
if (this.config.maxResults && this.numResults >= this.config.maxResults) {
// It's possible to go over maxResults like this, but it's much simpler than trying to extract the exact number
// of file matches, line matches, and matches within a line to == maxResults.
this.limitReached = true;
}
unwind(batchBytes);
},
error => {
// An error on the worker's end, not in reading the file, but in processing the batch. Log and continue.
onUnexpectedError(error);
unwind(batchBytes);
});
};
// Walk over the file system
let nextBatch: string[] = [];
let nextBatchBytes = 0;
const batchFlushBytes = 2 ** 20; // 1MB
this.walker.walk(this.config2.folderQueries, this.config2.extraFileResources || [], result => {
let bytes = result.size || 1;
this.totalBytes += bytes;
// If we have reached the limit or we are canceled, ignore it
if (this.limitReached || this.isCanceled) {
return unwind(bytes);
}
// Indicate progress to the outside
progress();
const absolutePath = result.base ? [result.base, result.relativePath].join(path.sep) : result.relativePath;
nextBatch.push(absolutePath);
nextBatchBytes += bytes;
if (nextBatchBytes >= batchFlushBytes) {
run(nextBatch, nextBatchBytes);
nextBatch = [];
nextBatchBytes = 0;
}
},
onProgress,
(error, isLimitHit) => {
this.walkerIsDone = true;
this.walkerError = error;
// Send any remaining paths to a worker, or unwind if we're stopping
if (nextBatch.length) {
if (this.limitReached || this.isCanceled) {
unwind(nextBatchBytes);
} else {
run(nextBatch, nextBatchBytes);
}
} else {
unwind(0);
}
});
}
}
/**
* Exported for tests
*/
export function makeRawSearch(query: ITextQuery): IRawSearch {
let rawSearch: IRawSearch = {
folderQueries: [],
extraFiles: [],
excludePattern: query.excludePattern,
includePattern: query.includePattern,
maxResults: query.maxResults,
useRipgrep: query.useRipgrep,
disregardIgnoreFiles: query.folderQueries.some(fq => fq.disregardIgnoreFiles!),
disregardGlobalIgnoreFiles: query.folderQueries.some(fq => fq.disregardGlobalIgnoreFiles!),
ignoreSymlinks: query.folderQueries.some(fq => fq.ignoreSymlinks!),
previewOptions: query.previewOptions,
contentPattern: query.contentPattern
};
for (const q of query.folderQueries) {
rawSearch.folderQueries.push({
excludePattern: q.excludePattern,
includePattern: q.includePattern,
fileEncoding: q.fileEncoding,
disregardIgnoreFiles: q.disregardIgnoreFiles,
disregardGlobalIgnoreFiles: q.disregardGlobalIgnoreFiles,
folder: q.folder.fsPath
});
}
if (query.extraFileResources) {
for (const r of query.extraFileResources) {
rawSearch.extraFiles!.push(r.fsPath);
}
}
return rawSearch;
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as os from 'os';
import * as ipc from 'vs/base/parts/ipc/node/ipc';
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
import { ISearchWorker, SearchWorkerChannelClient } from './worker/searchWorkerIpc';
import { getPathFromAmdModule } from 'vs/base/common/amd';
export interface ITextSearchWorkerProvider {
getWorkers(): ISearchWorker[];
}
export class TextSearchWorkerProvider implements ITextSearchWorkerProvider {
private workers: ISearchWorker[] = [];
getWorkers(): ISearchWorker[] {
const numWorkers = os.cpus().length;
while (this.workers.length < numWorkers) {
this.createWorker();
}
return this.workers;
}
private createWorker(): void {
let client = new Client(
getPathFromAmdModule(require, 'bootstrap-fork'),
{
serverName: 'Search Worker ' + this.workers.length,
args: ['--type=searchWorker'],
timeout: 30 * 1000,
env: {
AMD_ENTRYPOINT: 'vs/workbench/services/search/node/legacy/worker/searchWorkerApp',
PIPE_LOGGING: 'true',
VERBOSE_LOGGING: process.env.VERBOSE_LOGGING
},
useQueue: true
});
const channel = ipc.getNextTickChannel(client.getChannel('searchWorker'));
const channelClient = new SearchWorkerChannelClient(channel);
this.workers.push(channelClient);
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as fs from 'fs';
import * as gracefulFs from 'graceful-fs';
import { onUnexpectedError } from 'vs/base/common/errors';
import * as strings from 'vs/base/common/strings';
import { bomLength, decode, detectEncodingFromBuffer, encodingExists, UTF16be, UTF16le, UTF8, UTF8_with_bom } from 'vs/base/node/encoding';
import { Range } from 'vs/editor/common/core/range';
import { ITextSearchPreviewOptions, TextSearchMatch } from 'vs/platform/search/common/search';
import { ISearchWorker, ISearchWorkerSearchArgs, ISearchWorkerSearchResult } from './searchWorkerIpc';
import { FileMatch } from 'vs/workbench/services/search/node/search';
gracefulFs.gracefulify(fs);
interface ReadLinesOptions {
bufferLength: number;
encoding: string;
}
const MAX_FILE_ERRORS = 5; // Don't report more than this number of errors, 1 per file, to avoid flooding the log when there's a general issue
let numErrorsLogged = 0;
function onError(error: any): void {
if (numErrorsLogged++ < MAX_FILE_ERRORS) {
onUnexpectedError(error);
}
}
export class SearchWorker implements ISearchWorker {
private currentSearchEngine: SearchWorkerEngine;
initialize(): Promise<void> {
this.currentSearchEngine = new SearchWorkerEngine();
return Promise.resolve<void>(undefined);
}
cancel(): Promise<void> {
// Cancel the current search. It will stop searching and close its open files.
if (this.currentSearchEngine) {
this.currentSearchEngine.cancel();
}
return Promise.resolve<void>(undefined);
}
search(args: ISearchWorkerSearchArgs): Promise<ISearchWorkerSearchResult | null> {
if (!this.currentSearchEngine) {
// Worker timed out during search
this.initialize();
}
return this.currentSearchEngine.searchBatch(args);
}
}
interface IFileSearchResult {
match: FileMatch;
numMatches: number;
limitReached?: boolean;
}
const LF = 0x0A;
const CR = 0x0D;
export class SearchWorkerEngine {
private nextSearch: Promise<any> = Promise.resolve(null);
private isCanceled = false;
/**
* Searches some number of the given paths concurrently, and starts searches in other paths when those complete.
*/
searchBatch(args: ISearchWorkerSearchArgs): Promise<ISearchWorkerSearchResult | null> {
const contentPattern = strings.createRegExp(args.pattern.pattern, !!args.pattern.isRegExp, { matchCase: args.pattern.isCaseSensitive, wholeWord: args.pattern.isWordMatch, multiline: false, global: true });
const fileEncoding = encodingExists(args.fileEncoding) ? args.fileEncoding : UTF8;
return this.nextSearch =
this.nextSearch.then(() => this._searchBatch(args, contentPattern, fileEncoding));
}
private _searchBatch(args: ISearchWorkerSearchArgs, contentPattern: RegExp, fileEncoding: string): Promise<ISearchWorkerSearchResult | null> {
if (this.isCanceled) {
return Promise.resolve(null);
}
return new Promise<ISearchWorkerSearchResult>(batchDone => {
const result: ISearchWorkerSearchResult = {
matches: [],
numMatches: 0,
limitReached: false
};
// Search in the given path, and when it's finished, search in the next path in absolutePaths
const startSearchInFile = (absolutePath: string): Promise<void> => {
return this.searchInFile(absolutePath, contentPattern, fileEncoding, args.maxResults && (args.maxResults - result.numMatches), args.previewOptions).then(fileResult => {
// Finish early if search is canceled
if (this.isCanceled) {
return;
}
if (fileResult) {
result.numMatches += fileResult.numMatches;
result.matches.push(fileResult.match.serialize());
if (fileResult.limitReached) {
// If the limit was reached, terminate early with the results so far and cancel in-progress searches.
this.cancel();
result.limitReached = true;
return batchDone(result);
}
}
}, onError);
};
Promise.all(args.absolutePaths.map(startSearchInFile)).then(() => {
batchDone(result);
});
});
}
cancel(): void {
this.isCanceled = true;
}
private searchInFile(absolutePath: string, contentPattern: RegExp, fileEncoding: string, maxResults?: number, previewOptions?: ITextSearchPreviewOptions): Promise<IFileSearchResult | null> {
let fileMatch: FileMatch | null = null;
let limitReached = false;
let numMatches = 0;
const perLineCallback = (line: string, lineNumber: number) => {
let match = contentPattern.exec(line);
// Record all matches into file result
while (match !== null && match[0].length > 0 && !this.isCanceled && !limitReached) {
if (fileMatch === null) {
fileMatch = new FileMatch(absolutePath);
}
const lineMatch = new TextSearchMatch(line, new Range(lineNumber, match.index, lineNumber, match.index + match[0].length), previewOptions);
fileMatch.addMatch(lineMatch);
numMatches++;
if (maxResults && numMatches >= maxResults) {
limitReached = true;
}
match = contentPattern.exec(line);
}
};
// Read lines buffered to support large files
return this.readlinesAsync(absolutePath, perLineCallback, { bufferLength: 8096, encoding: fileEncoding }).then(
() => fileMatch ? { match: fileMatch, limitReached, numMatches } : null);
}
private readlinesAsync(filename: string, perLineCallback: (line: string, lineNumber: number) => void, options: ReadLinesOptions): Promise<void> {
return new Promise<void>((resolve, reject) => {
fs.open(filename, 'r', null, (error: Error, fd: number) => {
if (error) {
return resolve(undefined);
}
const buffer = Buffer.allocUnsafe(options.bufferLength);
let line = '';
let lineNumber = 0;
let lastBufferHadTrailingCR = false;
const readFile = (isFirstRead: boolean, clb: (error: Error | null) => void): void => {
if (this.isCanceled) {
return clb(null); // return early if canceled or limit reached
}
fs.read(fd, buffer, 0, buffer.length, null, (error: Error, bytesRead: number, buffer: Buffer) => {
const decodeBuffer = (buffer: Buffer, start: number, end: number): string => {
if (options.encoding === UTF8 || options.encoding === UTF8_with_bom) {
return buffer.toString(undefined, start, end); // much faster to use built in toString() when encoding is default
}
return decode(buffer.slice(start, end), options.encoding);
};
const lineFinished = (offset: number): void => {
line += decodeBuffer(buffer, pos, i + offset);
perLineCallback(line, lineNumber);
line = '';
lineNumber++;
pos = i + offset;
};
if (error || bytesRead === 0 || this.isCanceled) {
return clb(error); // return early if canceled or limit reached or no more bytes to read
}
let crlfCharSize = 1;
let crBytes = [CR];
let lfBytes = [LF];
let pos = 0;
let i = 0;
// Detect encoding and mime when this is the beginning of the file
if (isFirstRead) {
const detected = detectEncodingFromBuffer({ buffer, bytesRead }, false);
if (detected.seemsBinary) {
return clb(null); // skip files that seem binary
}
// Check for BOM offset
switch (detected.encoding) {
case UTF8:
pos = i = bomLength(UTF8);
options.encoding = UTF8;
break;
case UTF16be:
pos = i = bomLength(UTF16be);
options.encoding = UTF16be;
break;
case UTF16le:
pos = i = bomLength(UTF16le);
options.encoding = UTF16le;
break;
}
// when we are running with UTF16le/be, LF and CR are encoded as
// two bytes, like 0A 00 (LF) / 0D 00 (CR) for LE or flipped around
// for BE. We need to account for this when splitting the buffer into
// newlines, and when detecting a CRLF combo.
if (options.encoding === UTF16le) {
crlfCharSize = 2;
crBytes = [CR, 0x00];
lfBytes = [LF, 0x00];
} else if (options.encoding === UTF16be) {
crlfCharSize = 2;
crBytes = [0x00, CR];
lfBytes = [0x00, LF];
}
}
if (lastBufferHadTrailingCR) {
if (buffer[i] === lfBytes[0] && (lfBytes.length === 1 || buffer[i + 1] === lfBytes[1])) {
lineFinished(1 * crlfCharSize);
i++;
} else {
lineFinished(0);
}
lastBufferHadTrailingCR = false;
}
/**
* This loop executes for every byte of every file in the workspace - it is highly performance-sensitive!
* Hence the duplication in reading the buffer to avoid a function call. Previously a function call was not
* being inlined by V8.
*/
for (; i < bytesRead; ++i) {
if (buffer[i] === lfBytes[0] && (lfBytes.length === 1 || buffer[i + 1] === lfBytes[1])) {
lineFinished(1 * crlfCharSize);
} else if (buffer[i] === crBytes[0] && (crBytes.length === 1 || buffer[i + 1] === crBytes[1])) { // CR (Carriage Return)
if (i + crlfCharSize === bytesRead) {
lastBufferHadTrailingCR = true;
} else if (buffer[i + crlfCharSize] === lfBytes[0] && (lfBytes.length === 1 || buffer[i + crlfCharSize + 1] === lfBytes[1])) {
lineFinished(2 * crlfCharSize);
i += 2 * crlfCharSize - 1;
} else {
lineFinished(1 * crlfCharSize);
}
}
}
line += decodeBuffer(buffer, pos, bytesRead);
readFile(/*isFirstRead=*/false, clb); // Continue reading
});
};
readFile(/*isFirstRead=*/true, (error: Error) => {
if (error) {
return resolve(undefined);
}
if (line.length) {
perLineCallback(line, lineNumber); // handle last line
}
fs.close(fd, (error: Error) => {
resolve(undefined);
});
});
});
});
}
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
import { SearchWorkerChannel } from './searchWorkerIpc';
import { SearchWorker } from './searchWorker';
const server = new Server('searchWorker');
const worker = new SearchWorker();
const channel = new SearchWorkerChannel(worker);
server.registerChannel('searchWorker', channel);
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IChannel, IServerChannel } from 'vs/base/parts/ipc/node/ipc';
import { IPatternInfo, ITextSearchPreviewOptions } from 'vs/platform/search/common/search';
import { SearchWorker } from './searchWorker';
import { Event } from 'vs/base/common/event';
import { ISerializedFileMatch } from 'vs/workbench/services/search/node/search';
export interface ISearchWorkerSearchArgs {
pattern: IPatternInfo;
fileEncoding: string;
absolutePaths: string[];
maxResults?: number;
previewOptions?: ITextSearchPreviewOptions;
}
export interface ISearchWorkerSearchResult {
matches: ISerializedFileMatch[];
numMatches: number;
limitReached: boolean;
}
export interface ISearchWorker {
initialize(): Promise<void>;
search(args: ISearchWorkerSearchArgs): Promise<ISearchWorkerSearchResult | null>;
cancel(): Promise<void>;
}
export class SearchWorkerChannel implements IServerChannel {
constructor(private worker: SearchWorker) {
}
listen<T>(): Event<T> {
throw new Error('No events');
}
call(_, command: string, arg?: any): Promise<any> {
switch (command) {
case 'initialize': return this.worker.initialize();
case 'search': return this.worker.search(arg);
case 'cancel': return this.worker.cancel();
}
throw new Error(`Call not found: ${command}`);
}
}
export class SearchWorkerChannelClient implements ISearchWorker {
constructor(private channel: IChannel) { }
initialize(): Promise<void> {
return this.channel.call('initialize');
}
search(args: ISearchWorkerSearchArgs): Promise<ISearchWorkerSearchResult> {
return this.channel.call('search', args);
}
cancel(): Promise<void> {
return this.channel.call('cancel');
}
}
......@@ -19,7 +19,6 @@ import { compareItemsByScore, IItemAccessor, prepareQuery, ScorerCache } from 'v
import { MAX_FILE_SIZE } from 'vs/platform/files/node/files';
import { ICachedSearchStats, IFileQuery, IFileSearchStats, IFolderQuery, IProgress, IRawFileQuery, IRawQuery, IRawTextQuery, ITextQuery } from 'vs/platform/search/common/search';
import { Engine as FileSearchEngine } from 'vs/workbench/services/search/node/fileSearch';
import { LegacyTextSearchService } from 'vs/workbench/services/search/node/legacy/rawLegacyTextSearchService';
import { TextSearchEngineAdapter } from 'vs/workbench/services/search/node/textSearchAdapter';
import { IFileSearchProgressItem, IRawFileMatch, IRawSearchService, ISearchEngine, ISearchEngineSuccess, ISerializedFileMatch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedSearchSuccess } from './search';
......@@ -32,7 +31,6 @@ export class SearchService implements IRawSearchService {
private static readonly BATCH_SIZE = 512;
private legacyTextSearchService = new LegacyTextSearchService();
private caches: { [cacheKey: string]: Cache; } = Object.create(null);
fileSearch(config: IRawFileQuery): Event<ISerializedSearchProgressItem | ISerializedSearchComplete> {
......@@ -64,9 +62,7 @@ export class SearchService implements IRawSearchService {
const emitter = new Emitter<ISerializedSearchProgressItem | ISerializedSearchComplete>({
onFirstListenerDidAdd: () => {
promise = createCancelablePromise(token => {
return (rawQuery.useRipgrep ?
this.ripgrepTextSearch(query, p => emitter.fire(p), token) :
this.legacyTextSearchService.textSearch(query, p => emitter.fire(p), token));
return this.ripgrepTextSearch(query, p => emitter.fire(p), token);
});
promise.then(
......
......@@ -9,15 +9,10 @@ import { getPathFromAmdModule } from 'vs/base/common/amd';
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import * as glob from 'vs/base/common/glob';
import { URI } from 'vs/base/common/uri';
import { IFolderQuery, ISearchRange, ITextQuery, ITextSearchMatch, QueryType, ITextSearchContext, deserializeSearchError, SearchErrorCode } from 'vs/platform/search/common/search';
import { LegacyTextSearchService } from 'vs/workbench/services/search/node/legacy/rawLegacyTextSearchService';
import { deserializeSearchError, IFolderQuery, ISearchRange, ITextQuery, ITextSearchContext, ITextSearchMatch, QueryType, SearchErrorCode } from 'vs/platform/search/common/search';
import { ISerializedFileMatch } from 'vs/workbench/services/search/node/search';
import { TextSearchEngineAdapter } from 'vs/workbench/services/search/node/textSearchAdapter';
function countAll(matches: ISerializedFileMatch[]): number {
return matches.reduce((acc, m) => acc + m.numMatches!, 0);
}
const TEST_FIXTURES = path.normalize(getPathFromAmdModule(require, './fixtures'));
const EXAMPLES_FIXTURES = path.join(TEST_FIXTURES, 'examples');
const MORE_FIXTURES = path.join(TEST_FIXTURES, 'more');
......@@ -31,24 +26,7 @@ const MULTIROOT_QUERIES: IFolderQuery[] = [
{ folder: URI.file(MORE_FIXTURES) }
];
function doLegacySearchTest(config: ITextQuery, expectedResultCount: number | Function): Promise<void> {
const engine = new LegacyTextSearchService();
let c = 0;
return engine.textSearch(config, (result) => {
if (result && Array.isArray(result)) {
c += countAll(result);
}
}, null!).then(() => {
if (typeof expectedResultCount === 'function') {
assert(expectedResultCount(c));
} else {
assert.equal(c, expectedResultCount, 'legacy');
}
});
}
function doRipgrepSearchTest(query: ITextQuery, expectedResultCount: number | Function): Promise<ISerializedFileMatch[]> {
function doSearchTest(query: ITextQuery, expectedResultCount: number | Function): Promise<ISerializedFileMatch[]> {
const engine = new TextSearchEngineAdapter(query);
let c = 0;
......@@ -69,11 +47,6 @@ function doRipgrepSearchTest(query: ITextQuery, expectedResultCount: number | Fu
});
}
function doSearchTest(query: ITextQuery, expectedResultCount: number) {
return doLegacySearchTest(query, expectedResultCount)
.then(() => doRipgrepSearchTest(query, expectedResultCount));
}
suite('Search-integration', function () {
this.timeout(1000 * 60); // increase timeout for this suite
......@@ -240,10 +213,7 @@ suite('Search-integration', function () {
maxResults
};
// (Legacy) search can go over the maxResults because it doesn't trim the results from its worker processes to the exact max size.
// But the worst-case scenario should be 2*max-1
return doLegacySearchTest(config, count => count < maxResults * 2)
.then(() => doRipgrepSearchTest(config, maxResults));
return doSearchTest(config, maxResults);
});
test('Text: a (no results)', () => {
......@@ -318,7 +288,7 @@ suite('Search-integration', function () {
contentPattern: { pattern: '' }
};
return doRipgrepSearchTest(config, 1).then(results => {
return doSearchTest(config, 1).then(results => {
const matchRange = (<ITextSearchMatch>results[0].results![0]).ranges;
assert.deepEqual(matchRange, [{
startLineNumber: 0,
......@@ -336,7 +306,7 @@ suite('Search-integration', function () {
contentPattern: { pattern: 'h\\d,', isRegExp: true }
};
return doRipgrepSearchTest(config, 15).then(results => {
return doSearchTest(config, 15).then(results => {
assert.equal(results.length, 3);
assert.equal(results[0].results!.length, 1);
const match = <ITextSearchMatch>results[0].results![0];
......@@ -353,7 +323,7 @@ suite('Search-integration', function () {
afterContext: 2
};
return doRipgrepSearchTest(config, 4).then(results => {
return doSearchTest(config, 4).then(results => {
assert.equal(results.length, 4);
assert.equal((<ITextSearchContext>results[0].results![0]).lineNumber, 25);
assert.equal((<ITextSearchContext>results[0].results![0]).text, ' compiler.addUnit(prog,"input.ts");');
......@@ -378,7 +348,7 @@ suite('Search-integration', function () {
contentPattern: { pattern: 'test' },
};
return doRipgrepSearchTest(config, 0).then(() => {
return doSearchTest(config, 0).then(() => {
throw new Error('expected fail');
}, err => {
const searchError = deserializeSearchError(err.message);
......@@ -394,7 +364,7 @@ suite('Search-integration', function () {
contentPattern: { pattern: ')', isRegExp: true },
};
return doRipgrepSearchTest(config, 0).then(() => {
return doSearchTest(config, 0).then(() => {
throw new Error('expected fail');
}, err => {
const searchError = deserializeSearchError(err.message);
......@@ -413,7 +383,7 @@ suite('Search-integration', function () {
}
};
return doRipgrepSearchTest(config, 0).then(() => {
return doSearchTest(config, 0).then(() => {
throw new Error('expected fail');
}, err => {
const searchError = deserializeSearchError(err.message);
......@@ -429,7 +399,7 @@ suite('Search-integration', function () {
contentPattern: { pattern: 'foo\nbar', isRegExp: true }
};
return doRipgrepSearchTest(config, 0).then(() => {
return doSearchTest(config, 0).then(() => {
throw new Error('expected fail');
}, err => {
const searchError = deserializeSearchError(err.message);
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册