Merge from vscode a5cf1da01d5db3d2557132be8d30f89c38019f6c (#8525)

* Merge from vscode a5cf1da01d5db3d2557132be8d30f89c38019f6c

* remove files we don't want

* fix hygiene

* update distro

* update distro

* fix hygiene

* fix strict nulls

* distro

* distro

* fix tests

* fix tests

* add another edit

* fix viewlet icon

* fix azure dialog

* fix some padding

* fix more padding issues
This commit is contained in:
Anthony Dresser
2019-12-04 19:28:22 -08:00
committed by GitHub
parent a8818ab0df
commit f5ce7fb2a5
1507 changed files with 42813 additions and 27370 deletions

View File

@@ -1,372 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'vs/base/common/path';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import * as glob from 'vs/base/common/glob';
import * as resources from 'vs/base/common/resources';
import { StopWatch } from 'vs/base/common/stopwatch';
import { URI } from 'vs/base/common/uri';
import { IFileMatch, IFileSearchProviderStats, IFolderQuery, ISearchCompleteStats, IFileQuery, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search';
import { FileSearchProvider, FileSearchOptions } from 'vs/workbench/services/search/common/searchExtTypes';
export interface IInternalFileMatch {
base: URI;
original?: URI;
relativePath?: string; // Not present for extraFiles or absolute path matches
basename: string;
size?: number;
}
export interface IDirectoryEntry {
base: URI;
relativePath: string;
basename: string;
}
export interface IDirectoryTree {
rootEntries: IDirectoryEntry[];
pathToEntries: { [relativePath: string]: IDirectoryEntry[] };
}
class FileSearchEngine {
private filePattern?: string;
private includePattern?: glob.ParsedExpression;
private maxResults?: number;
private exists?: boolean;
private isLimitHit = false;
private resultCount = 0;
private isCanceled = false;
private activeCancellationTokens: Set<CancellationTokenSource>;
private globalExcludePattern?: glob.ParsedExpression;
constructor(private config: IFileQuery, private provider: FileSearchProvider, private sessionToken?: CancellationToken) {
this.filePattern = config.filePattern;
this.includePattern = config.includePattern && glob.parse(config.includePattern);
this.maxResults = config.maxResults || undefined;
this.exists = config.exists;
this.activeCancellationTokens = new Set<CancellationTokenSource>();
this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern);
}
cancel(): void {
this.isCanceled = true;
this.activeCancellationTokens.forEach(t => t.cancel());
this.activeCancellationTokens = new Set();
}
search(_onResult: (match: IInternalFileMatch) => void): Promise<IInternalSearchComplete> {
const folderQueries = this.config.folderQueries || [];
return new Promise((resolve, reject) => {
const onResult = (match: IInternalFileMatch) => {
this.resultCount++;
_onResult(match);
};
// Support that the file pattern is a full path to a file that exists
if (this.isCanceled) {
return resolve({ limitHit: this.isLimitHit });
}
// For each extra file
if (this.config.extraFileResources) {
this.config.extraFileResources
.forEach(extraFile => {
const extraFileStr = extraFile.toString(); // ?
const basename = path.basename(extraFileStr);
if (this.globalExcludePattern && this.globalExcludePattern(extraFileStr, basename)) {
return; // excluded
}
// File: Check for match on file pattern and include pattern
this.matchFile(onResult, { base: extraFile, basename });
});
}
// For each root folder
Promise.all(folderQueries.map(fq => {
return this.searchInFolder(fq, onResult);
})).then(stats => {
resolve({
limitHit: this.isLimitHit,
stats: stats[0] || undefined // Only looking at single-folder workspace stats...
});
}, (err: Error) => {
reject(new Error(toErrorMessage(err)));
});
});
}
private searchInFolder(fq: IFolderQuery<URI>, onResult: (match: IInternalFileMatch) => void): Promise<IFileSearchProviderStats | null> {
const cancellation = new CancellationTokenSource();
return new Promise((resolve, reject) => {
const options = this.getSearchOptionsForFolder(fq);
const tree = this.initDirectoryTree();
const queryTester = new QueryGlobTester(this.config, fq);
const noSiblingsClauses = !queryTester.hasSiblingExcludeClauses();
let providerSW: StopWatch;
new Promise(_resolve => process.nextTick(_resolve))
.then(() => {
this.activeCancellationTokens.add(cancellation);
providerSW = StopWatch.create();
return this.provider.provideFileSearchResults(
{
pattern: this.config.filePattern || ''
},
options,
cancellation.token);
})
.then(results => {
const providerTime = providerSW.elapsed();
const postProcessSW = StopWatch.create();
if (this.isCanceled && !this.isLimitHit) {
return null;
}
if (results) {
results.forEach(result => {
const relativePath = path.posix.relative(fq.folder.path, result.path);
if (noSiblingsClauses) {
const basename = path.basename(result.path);
this.matchFile(onResult, { base: fq.folder, relativePath, basename });
return;
}
// TODO: Optimize siblings clauses with ripgrep here.
this.addDirectoryEntries(tree, fq.folder, relativePath, onResult);
});
}
this.activeCancellationTokens.delete(cancellation);
if (this.isCanceled && !this.isLimitHit) {
return null;
}
this.matchDirectoryTree(tree, queryTester, onResult);
return <IFileSearchProviderStats>{
providerTime,
postProcessTime: postProcessSW.elapsed()
};
}).then(
stats => {
cancellation.dispose();
resolve(stats);
},
err => {
cancellation.dispose();
reject(err);
});
});
}
private getSearchOptionsForFolder(fq: IFolderQuery<URI>): FileSearchOptions {
const includes = resolvePatternsForProvider(this.config.includePattern, fq.includePattern);
const excludes = resolvePatternsForProvider(this.config.excludePattern, fq.excludePattern);
return {
folder: fq.folder,
excludes,
includes,
useIgnoreFiles: !fq.disregardIgnoreFiles,
useGlobalIgnoreFiles: !fq.disregardGlobalIgnoreFiles,
followSymlinks: !fq.ignoreSymlinks,
maxResults: this.config.maxResults,
session: this.sessionToken
};
}
private initDirectoryTree(): IDirectoryTree {
const tree: IDirectoryTree = {
rootEntries: [],
pathToEntries: Object.create(null)
};
tree.pathToEntries['.'] = tree.rootEntries;
return tree;
}
private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: URI, relativeFile: string, onResult: (result: IInternalFileMatch) => void) {
// Support relative paths to files from a root resource (ignores excludes)
if (relativeFile === this.filePattern) {
const basename = path.basename(this.filePattern);
this.matchFile(onResult, { base: base, relativePath: this.filePattern, basename });
}
function add(relativePath: string) {
const basename = path.basename(relativePath);
const dirname = path.dirname(relativePath);
let entries = pathToEntries[dirname];
if (!entries) {
entries = pathToEntries[dirname] = [];
add(dirname);
}
entries.push({
base,
relativePath,
basename
});
}
add(relativeFile);
}
private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, queryTester: QueryGlobTester, onResult: (result: IInternalFileMatch) => void) {
const self = this;
const filePattern = this.filePattern;
function matchDirectory(entries: IDirectoryEntry[]) {
const hasSibling = glob.hasSiblingFn(() => entries.map(entry => entry.basename));
for (let i = 0, n = entries.length; i < n; i++) {
const entry = entries[i];
const { relativePath, basename } = entry;
// Check exclude pattern
// If the user searches for the exact file name, we adjust the glob matching
// to ignore filtering by siblings because the user seems to know what she
// is searching for and we want to include the result in that case anyway
if (!queryTester.includedInQuerySync(relativePath, basename, filePattern !== basename ? hasSibling : undefined)) {
continue;
}
const sub = pathToEntries[relativePath];
if (sub) {
matchDirectory(sub);
} else {
if (relativePath === filePattern) {
continue; // ignore file if its path matches with the file pattern because that is already matched above
}
self.matchFile(onResult, entry);
}
if (self.isLimitHit) {
break;
}
}
}
matchDirectory(rootEntries);
}
private matchFile(onResult: (result: IInternalFileMatch) => void, candidate: IInternalFileMatch): void {
if (!this.includePattern || (candidate.relativePath && this.includePattern(candidate.relativePath, candidate.basename))) {
if (this.exists || (this.maxResults && this.resultCount >= this.maxResults)) {
this.isLimitHit = true;
this.cancel();
}
if (!this.isLimitHit) {
onResult(candidate);
}
}
}
}
interface IInternalSearchComplete {
limitHit: boolean;
stats?: IFileSearchProviderStats;
}
export class FileSearchManager {
private static readonly BATCH_SIZE = 512;
private readonly sessions = new Map<string, CancellationTokenSource>();
fileSearch(config: IFileQuery, provider: FileSearchProvider, onBatch: (matches: IFileMatch[]) => void, token: CancellationToken): Promise<ISearchCompleteStats> {
const sessionTokenSource = this.getSessionTokenSource(config.cacheKey);
const engine = new FileSearchEngine(config, provider, sessionTokenSource && sessionTokenSource.token);
let resultCount = 0;
const onInternalResult = (batch: IInternalFileMatch[]) => {
resultCount += batch.length;
onBatch(batch.map(m => this.rawMatchToSearchItem(m)));
};
return this.doSearch(engine, FileSearchManager.BATCH_SIZE, onInternalResult, token).then(
result => {
return <ISearchCompleteStats>{
limitHit: result.limitHit,
stats: {
fromCache: false,
type: 'fileSearchProvider',
resultCount,
detailStats: result.stats
}
};
});
}
clearCache(cacheKey: string): void {
const sessionTokenSource = this.getSessionTokenSource(cacheKey);
if (sessionTokenSource) {
sessionTokenSource.cancel();
}
}
private getSessionTokenSource(cacheKey: string | undefined): CancellationTokenSource | undefined {
if (!cacheKey) {
return undefined;
}
if (!this.sessions.has(cacheKey)) {
this.sessions.set(cacheKey, new CancellationTokenSource());
}
return this.sessions.get(cacheKey);
}
private rawMatchToSearchItem(match: IInternalFileMatch): IFileMatch {
if (match.relativePath) {
return {
resource: resources.joinPath(match.base, match.relativePath)
};
} else {
// extraFileResources
return {
resource: match.base
};
}
}
private doSearch(engine: FileSearchEngine, batchSize: number, onResultBatch: (matches: IInternalFileMatch[]) => void, token: CancellationToken): Promise<IInternalSearchComplete> {
token.onCancellationRequested(() => {
engine.cancel();
});
const _onResult = (match: IInternalFileMatch) => {
if (match) {
batch.push(match);
if (batchSize > 0 && batch.length >= batchSize) {
onResultBatch(batch);
batch = [];
}
}
};
let batch: IInternalFileMatch[] = [];
return engine.search(_onResult).then(result => {
if (batch.length) {
onResultBatch(batch);
}
return result;
}, error => {
if (batch.length) {
onResultBatch(batch);
}
return Promise.reject(error);
});
}
}

View File

@@ -277,7 +277,7 @@ export class SearchService implements IRawSearchService {
for (const previousSearch in cache.resultsToSearchCache) {
// If we narrow down, we might be able to reuse the cached results
if (strings.startsWith(searchValue, previousSearch)) {
if (hasPathSep && previousSearch.indexOf(sep) < 0) {
if (hasPathSep && previousSearch.indexOf(sep) < 0 && previousSearch !== '') {
continue; // since a path character widens the search for potential more matches, require it in previous search too
}
@@ -383,7 +383,7 @@ export class SearchService implements IRawSearchService {
cancel() {
// Do nothing
}
then(resolve: any, reject: any) {
then<TResult1 = C, TResult2 = never>(resolve?: ((value: C) => TResult1 | Promise<TResult1>) | undefined | null, reject?: ((reason: any) => TResult2 | Promise<TResult2>) | undefined | null): Promise<TResult1 | TResult2> {
return promise.then(resolve, reject);
}
catch(reject?: any) {

View File

@@ -79,15 +79,15 @@ export class RipgrepTextSearchEngine {
cancel();
});
rgProc.stdout.on('data', data => {
rgProc.stdout!.on('data', data => {
ripgrepParser.handleData(data);
});
let gotData = false;
rgProc.stdout.once('data', () => gotData = true);
rgProc.stdout!.once('data', () => gotData = true);
let stderr = '';
rgProc.stderr.on('data', data => {
rgProc.stderr!.on('data', data => {
const message = data.toString();
this.outputChannel.appendLine(message);
stderr += message;

View File

@@ -21,16 +21,17 @@ 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 { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService';
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';
import { parseSearchPort } from 'vs/platform/environment/node/environmentService';
export class LocalSearchService extends SearchService {
constructor(
@IModelService modelService: IModelService,
@IUntitledEditorService untitledEditorService: IUntitledEditorService,
@IUntitledTextEditorService untitledTextEditorService: IUntitledTextEditorService,
@IEditorService editorService: IEditorService,
@ITelemetryService telemetryService: ITelemetryService,
@ILogService logService: ILogService,
@@ -39,10 +40,10 @@ export class LocalSearchService extends SearchService {
@IWorkbenchEnvironmentService readonly environmentService: IWorkbenchEnvironmentService,
@IInstantiationService readonly instantiationService: IInstantiationService
) {
super(modelService, untitledEditorService, editorService, telemetryService, logService, extensionService, fileService);
super(modelService, untitledTextEditorService, editorService, telemetryService, logService, extensionService, fileService);
this.diskSearch = instantiationService.createInstance(DiskSearch, !environmentService.isBuilt || environmentService.verbose, environmentService.debugSearch);
this.diskSearch = instantiationService.createInstance(DiskSearch, !environmentService.isBuilt || environmentService.verbose, parseSearchPort(environmentService.args, environmentService.isBuilt));
}
}

View File

@@ -7,12 +7,11 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import * as pfs from 'vs/base/node/pfs';
import { IFileMatch, IProgressMessage, ITextQuery, ITextSearchStats, ITextSearchMatch, ISerializedFileMatch, ISerializedSearchSuccess } from 'vs/workbench/services/search/common/search';
import { RipgrepTextSearchEngine } from 'vs/workbench/services/search/node/ripgrepTextSearchEngine';
import { TextSearchManager } from 'vs/workbench/services/search/node/textSearchManager';
import { NativeTextSearchManager } from 'vs/workbench/services/search/node/textSearchManager';
export class TextSearchEngineAdapter {
constructor(private query: ITextQuery) {
}
constructor(private query: ITextQuery) { }
search(token: CancellationToken, onResult: (matches: ISerializedFileMatch[]) => void, onMessage: (message: IProgressMessage) => void): Promise<ISerializedSearchSuccess> {
if ((!this.query.folderQueries || !this.query.folderQueries.length) && (!this.query.extraFileResources || !this.query.extraFileResources.length)) {
@@ -30,7 +29,7 @@ export class TextSearchEngineAdapter {
onMessage({ message: msg });
}
};
const textSearchManager = new TextSearchManager(this.query, new RipgrepTextSearchEngine(pretendOutputChannel), pfs);
const textSearchManager = new NativeTextSearchManager(this.query, new RipgrepTextSearchEngine(pretendOutputChannel), pfs);
return new Promise((resolve, reject) => {
return textSearchManager
.search(

View File

@@ -3,352 +3,18 @@
* Licensed under the Source EULA. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as path from 'vs/base/common/path';
import { mapArrayOrNot } from 'vs/base/common/arrays';
import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import * as resources from 'vs/base/common/resources';
import * as glob from 'vs/base/common/glob';
import { URI } from 'vs/base/common/uri';
import { toCanonicalName } from 'vs/base/node/encoding';
import * as pfs from 'vs/base/node/pfs';
import { IExtendedExtensionSearchOptions, IFileMatch, IFolderQuery, IPatternInfo, ISearchCompleteStats, ITextQuery, ITextSearchContext, ITextSearchMatch, ITextSearchResult, QueryGlobTester, resolvePatternsForProvider } from 'vs/workbench/services/search/common/search';
import { TextSearchProvider, TextSearchResult, TextSearchMatch, TextSearchComplete, Range, TextSearchOptions, TextSearchQuery } from 'vs/workbench/services/search/common/searchExtTypes';
import { ITextQuery } from 'vs/workbench/services/search/common/search';
import { TextSearchProvider } from 'vs/workbench/services/search/common/searchExtTypes';
import { TextSearchManager } from 'vs/workbench/services/search/common/textSearchManager';
export class TextSearchManager {
export class NativeTextSearchManager extends TextSearchManager {
private collector: TextSearchResultsCollector | null = null;
private isLimitHit = false;
private resultCount = 0;
constructor(private query: ITextQuery, private provider: TextSearchProvider, private _pfs: typeof pfs = pfs) {
}
search(onProgress: (matches: IFileMatch[]) => void, token: CancellationToken): Promise<ISearchCompleteStats> {
const folderQueries = this.query.folderQueries || [];
const tokenSource = new CancellationTokenSource();
token.onCancellationRequested(() => tokenSource.cancel());
return new Promise<ISearchCompleteStats>((resolve, reject) => {
this.collector = new TextSearchResultsCollector(onProgress);
let isCanceled = false;
const onResult = (result: TextSearchResult, folderIdx: number) => {
if (isCanceled) {
return;
}
if (!this.isLimitHit) {
const resultSize = this.resultSize(result);
if (extensionResultIsMatch(result) && typeof this.query.maxResults === 'number' && this.resultCount + resultSize > this.query.maxResults) {
this.isLimitHit = true;
isCanceled = true;
tokenSource.cancel();
result = this.trimResultToSize(result, this.query.maxResults - this.resultCount);
}
const newResultSize = this.resultSize(result);
this.resultCount += newResultSize;
if (newResultSize > 0) {
this.collector!.add(result, folderIdx);
}
}
};
// For each root folder
Promise.all(folderQueries.map((fq, i) => {
return this.searchInFolder(fq, r => onResult(r, i), tokenSource.token);
})).then(results => {
tokenSource.dispose();
this.collector!.flush();
const someFolderHitLImit = results.some(result => !!result && !!result.limitHit);
resolve({
limitHit: this.isLimitHit || someFolderHitLImit,
stats: {
type: 'textSearchProvider'
}
});
}, (err: Error) => {
tokenSource.dispose();
const errMsg = toErrorMessage(err);
reject(new Error(errMsg));
});
constructor(query: ITextQuery, provider: TextSearchProvider, _pfs: typeof pfs = pfs) {
super(query, provider, {
readdir: resource => _pfs.readdir(resource.fsPath),
toCanonicalName: name => toCanonicalName(name)
});
}
private resultSize(result: TextSearchResult): number {
const match = <TextSearchMatch>result;
return Array.isArray(match.ranges) ?
match.ranges.length :
1;
}
private trimResultToSize(result: TextSearchMatch, size: number): TextSearchMatch {
const rangesArr = Array.isArray(result.ranges) ? result.ranges : [result.ranges];
const matchesArr = Array.isArray(result.preview.matches) ? result.preview.matches : [result.preview.matches];
return {
ranges: rangesArr.slice(0, size),
preview: {
matches: matchesArr.slice(0, size),
text: result.preview.text
},
uri: result.uri
};
}
private searchInFolder(folderQuery: IFolderQuery<URI>, onResult: (result: TextSearchResult) => void, token: CancellationToken): Promise<TextSearchComplete | null | undefined> {
const queryTester = new QueryGlobTester(this.query, folderQuery);
const testingPs: Promise<void>[] = [];
const progress = {
report: (result: TextSearchResult) => {
if (!this.validateProviderResult(result)) {
return;
}
const hasSibling = folderQuery.folder.scheme === 'file' ?
glob.hasSiblingPromiseFn(() => {
return this.readdir(path.dirname(result.uri.fsPath));
}) :
undefined;
const relativePath = path.relative(folderQuery.folder.fsPath, result.uri.fsPath);
testingPs.push(
queryTester.includedInQuery(relativePath, path.basename(relativePath), hasSibling)
.then(included => {
if (included) {
onResult(result);
}
}));
}
};
const searchOptions = this.getSearchOptionsForFolder(folderQuery);
return new Promise(resolve => process.nextTick(resolve))
.then(() => this.provider.provideTextSearchResults(patternInfoToQuery(this.query.contentPattern), searchOptions, progress, token))
.then(result => {
return Promise.all(testingPs)
.then(() => result);
});
}
private validateProviderResult(result: TextSearchResult): boolean {
if (extensionResultIsMatch(result)) {
if (Array.isArray(result.ranges)) {
if (!Array.isArray(result.preview.matches)) {
console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same type.');
return false;
}
if ((<Range[]>result.preview.matches).length !== result.ranges.length) {
console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same length.');
return false;
}
} else {
if (Array.isArray(result.preview.matches)) {
console.warn('INVALID - A text search provider match\'s`ranges` and`matches` properties must have the same length.');
return false;
}
}
}
return true;
}
private readdir(dirname: string): Promise<string[]> {
return this._pfs.readdir(dirname);
}
private getSearchOptionsForFolder(fq: IFolderQuery<URI>): TextSearchOptions {
const includes = resolvePatternsForProvider(this.query.includePattern, fq.includePattern);
const excludes = resolvePatternsForProvider(this.query.excludePattern, fq.excludePattern);
const options = <TextSearchOptions>{
folder: URI.from(fq.folder),
excludes,
includes,
useIgnoreFiles: !fq.disregardIgnoreFiles,
useGlobalIgnoreFiles: !fq.disregardGlobalIgnoreFiles,
followSymlinks: !fq.ignoreSymlinks,
encoding: fq.fileEncoding && toCanonicalName(fq.fileEncoding),
maxFileSize: this.query.maxFileSize,
maxResults: this.query.maxResults,
previewOptions: this.query.previewOptions,
afterContext: this.query.afterContext,
beforeContext: this.query.beforeContext
};
(<IExtendedExtensionSearchOptions>options).usePCRE2 = this.query.usePCRE2;
return options;
}
}
function patternInfoToQuery(patternInfo: IPatternInfo): TextSearchQuery {
return <TextSearchQuery>{
isCaseSensitive: patternInfo.isCaseSensitive || false,
isRegExp: patternInfo.isRegExp || false,
isWordMatch: patternInfo.isWordMatch || false,
isMultiline: patternInfo.isMultiline || false,
pattern: patternInfo.pattern
};
}
export class TextSearchResultsCollector {
private _batchedCollector: BatchedCollector<IFileMatch>;
private _currentFolderIdx: number = -1;
private _currentUri: URI | undefined;
private _currentFileMatch: IFileMatch | null = null;
constructor(private _onResult: (result: IFileMatch[]) => void) {
this._batchedCollector = new BatchedCollector<IFileMatch>(512, items => this.sendItems(items));
}
add(data: TextSearchResult, folderIdx: number): void {
// Collects TextSearchResults into IInternalFileMatches and collates using BatchedCollector.
// This is efficient for ripgrep which sends results back one file at a time. It wouldn't be efficient for other search
// providers that send results in random order. We could do this step afterwards instead.
if (this._currentFileMatch && (this._currentFolderIdx !== folderIdx || !resources.isEqual(this._currentUri, data.uri))) {
this.pushToCollector();
this._currentFileMatch = null;
}
if (!this._currentFileMatch) {
this._currentFolderIdx = folderIdx;
this._currentFileMatch = {
resource: data.uri,
results: []
};
}
this._currentFileMatch.results!.push(extensionResultToFrontendResult(data));
}
private pushToCollector(): void {
const size = this._currentFileMatch && this._currentFileMatch.results ?
this._currentFileMatch.results.length :
0;
this._batchedCollector.addItem(this._currentFileMatch!, size);
}
flush(): void {
this.pushToCollector();
this._batchedCollector.flush();
}
private sendItems(items: IFileMatch[]): void {
this._onResult(items);
}
}
function extensionResultToFrontendResult(data: TextSearchResult): ITextSearchResult {
// Warning: result from RipgrepTextSearchEH has fake Range. Don't depend on any other props beyond these...
if (extensionResultIsMatch(data)) {
return <ITextSearchMatch>{
preview: {
matches: mapArrayOrNot(data.preview.matches, m => ({
startLineNumber: m.start.line,
startColumn: m.start.character,
endLineNumber: m.end.line,
endColumn: m.end.character
})),
text: data.preview.text
},
ranges: mapArrayOrNot(data.ranges, r => ({
startLineNumber: r.start.line,
startColumn: r.start.character,
endLineNumber: r.end.line,
endColumn: r.end.character
}))
};
} else {
return <ITextSearchContext>{
text: data.text,
lineNumber: data.lineNumber
};
}
}
export function extensionResultIsMatch(data: TextSearchResult): data is TextSearchMatch {
return !!(<TextSearchMatch>data).preview;
}
/**
* Collects items that have a size - before the cumulative size of collected items reaches START_BATCH_AFTER_COUNT, the callback is called for every
* set of items collected.
* But after that point, the callback is called with batches of maxBatchSize.
* If the batch isn't filled within some time, the callback is also called.
*/
export class BatchedCollector<T> {
private static readonly TIMEOUT = 4000;
// After START_BATCH_AFTER_COUNT items have been collected, stop flushing on timeout
private static readonly START_BATCH_AFTER_COUNT = 50;
private totalNumberCompleted = 0;
private batch: T[] = [];
private batchSize = 0;
private timeoutHandle: any;
constructor(private maxBatchSize: number, private cb: (items: T[]) => void) {
}
addItem(item: T, size: number): void {
if (!item) {
return;
}
this.addItemToBatch(item, size);
}
addItems(items: T[], size: number): void {
if (!items) {
return;
}
this.addItemsToBatch(items, size);
}
private addItemToBatch(item: T, size: number): void {
this.batch.push(item);
this.batchSize += size;
this.onUpdate();
}
private addItemsToBatch(item: T[], size: number): void {
this.batch = this.batch.concat(item);
this.batchSize += size;
this.onUpdate();
}
private onUpdate(): void {
if (this.totalNumberCompleted < BatchedCollector.START_BATCH_AFTER_COUNT) {
// Flush because we aren't batching yet
this.flush();
} else if (this.batchSize >= this.maxBatchSize) {
// Flush because the batch is full
this.flush();
} else if (!this.timeoutHandle) {
// No timeout running, start a timeout to flush
this.timeoutHandle = setTimeout(() => {
this.flush();
}, BatchedCollector.TIMEOUT);
}
}
flush(): void {
if (this.batchSize) {
this.totalNumberCompleted += this.batchSize;
this.cb(this.batch);
this.batch = [];
this.batchSize = 0;
if (this.timeoutHandle) {
clearTimeout(this.timeoutHandle);
this.timeoutHandle = 0;
}
}
}
}