mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-12 19:18:32 -05:00
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:
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user