mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-12 11:08:31 -05:00
SQL Operations Studio Public Preview 1 (0.23) release source code
This commit is contained in:
741
src/vs/workbench/services/search/node/fileSearch.ts
Normal file
741
src/vs/workbench/services/search/node/fileSearch.ts
Normal file
@@ -0,0 +1,741 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as childProcess from 'child_process';
|
||||
import { StringDecoder, NodeStringDecoder } from 'string_decoder';
|
||||
import { toErrorMessage } from 'vs/base/common/errorMessage';
|
||||
import fs = require('fs');
|
||||
import path = require('path');
|
||||
import { isEqualOrParent } from 'vs/base/common/paths';
|
||||
import { Readable } from 'stream';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
|
||||
import scorer = require('vs/base/common/scorer');
|
||||
import objects = require('vs/base/common/objects');
|
||||
import arrays = require('vs/base/common/arrays');
|
||||
import platform = require('vs/base/common/platform');
|
||||
import strings = require('vs/base/common/strings');
|
||||
import types = require('vs/base/common/types');
|
||||
import glob = require('vs/base/common/glob');
|
||||
import { IProgress, IUncachedSearchStats } from 'vs/platform/search/common/search';
|
||||
|
||||
import extfs = require('vs/base/node/extfs');
|
||||
import flow = require('vs/base/node/flow');
|
||||
import { IRawFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine, IFolderSearch } from './search';
|
||||
|
||||
enum Traversal {
|
||||
Node = 1,
|
||||
MacFind,
|
||||
WindowsDir,
|
||||
LinuxFind
|
||||
}
|
||||
|
||||
interface IDirectoryEntry {
|
||||
base: string;
|
||||
relativePath: string;
|
||||
basename: string;
|
||||
}
|
||||
|
||||
interface IDirectoryTree {
|
||||
rootEntries: IDirectoryEntry[];
|
||||
pathToEntries: { [relativePath: string]: IDirectoryEntry[] };
|
||||
}
|
||||
|
||||
export class FileWalker {
|
||||
private config: IRawSearch;
|
||||
private filePattern: string;
|
||||
private normalizedFilePatternLowercase: string;
|
||||
private includePattern: glob.ParsedExpression;
|
||||
private maxResults: number;
|
||||
private maxFilesize: number;
|
||||
private isLimitHit: boolean;
|
||||
private resultCount: number;
|
||||
private isCanceled: boolean;
|
||||
private fileWalkStartTime: number;
|
||||
private directoriesWalked: number;
|
||||
private filesWalked: number;
|
||||
private traversal: Traversal;
|
||||
private errors: string[];
|
||||
private cmdForkStartTime: number;
|
||||
private cmdForkResultTime: number;
|
||||
private cmdResultCount: number;
|
||||
|
||||
private folderExcludePatterns: Map<string, AbsoluteAndRelativeParsedExpression>;
|
||||
private globalExcludePattern: glob.ParsedExpression;
|
||||
|
||||
private walkedPaths: { [path: string]: boolean; };
|
||||
|
||||
constructor(config: IRawSearch) {
|
||||
this.config = config;
|
||||
this.filePattern = config.filePattern;
|
||||
this.includePattern = config.includePattern && glob.parse(config.includePattern);
|
||||
this.maxResults = config.maxResults || null;
|
||||
this.maxFilesize = config.maxFilesize || null;
|
||||
this.walkedPaths = Object.create(null);
|
||||
this.resultCount = 0;
|
||||
this.isLimitHit = false;
|
||||
this.directoriesWalked = 0;
|
||||
this.filesWalked = 0;
|
||||
this.traversal = Traversal.Node;
|
||||
this.errors = [];
|
||||
|
||||
if (this.filePattern) {
|
||||
this.normalizedFilePatternLowercase = strings.stripWildcards(this.filePattern).toLowerCase();
|
||||
}
|
||||
|
||||
this.globalExcludePattern = config.excludePattern && glob.parse(config.excludePattern);
|
||||
this.folderExcludePatterns = new Map<string, AbsoluteAndRelativeParsedExpression>();
|
||||
|
||||
config.folderQueries.forEach(folderQuery => {
|
||||
const folderExcludeExpression: glob.IExpression = objects.assign({}, folderQuery.excludePattern || {}, this.config.excludePattern || {});
|
||||
|
||||
// Add excludes for other root folders
|
||||
config.folderQueries
|
||||
.map(rootFolderQuery => rootFolderQuery.folder)
|
||||
.filter(rootFolder => rootFolder !== folderQuery.folder)
|
||||
.forEach(otherRootFolder => {
|
||||
// Exclude nested root folders
|
||||
if (isEqualOrParent(otherRootFolder, folderQuery.folder)) {
|
||||
folderExcludeExpression[path.relative(folderQuery.folder, otherRootFolder)] = true;
|
||||
}
|
||||
});
|
||||
|
||||
this.folderExcludePatterns.set(folderQuery.folder, new AbsoluteAndRelativeParsedExpression(folderExcludeExpression, folderQuery.folder));
|
||||
});
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.isCanceled = true;
|
||||
}
|
||||
|
||||
public walk(folderQueries: IFolderSearch[], extraFiles: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error, isLimitHit: boolean) => void): void {
|
||||
this.fileWalkStartTime = Date.now();
|
||||
|
||||
// Support that the file pattern is a full path to a file that exists
|
||||
this.checkFilePatternAbsoluteMatch((exists, size) => {
|
||||
if (this.isCanceled) {
|
||||
return done(null, this.isLimitHit);
|
||||
}
|
||||
|
||||
// Report result from file pattern if matching
|
||||
if (exists) {
|
||||
this.resultCount++;
|
||||
onResult({
|
||||
relativePath: this.filePattern,
|
||||
basename: path.basename(this.filePattern),
|
||||
size
|
||||
});
|
||||
|
||||
// Optimization: a match on an absolute path is a good result and we do not
|
||||
// continue walking the entire root paths array for other matches because
|
||||
// it is very unlikely that another file would match on the full absolute path
|
||||
return done(null, this.isLimitHit);
|
||||
}
|
||||
|
||||
// For each extra file
|
||||
if (extraFiles) {
|
||||
extraFiles.forEach(extraFilePath => {
|
||||
const basename = path.basename(extraFilePath);
|
||||
if (this.globalExcludePattern && this.globalExcludePattern(extraFilePath, basename)) {
|
||||
return; // excluded
|
||||
}
|
||||
|
||||
// File: Check for match on file pattern and include pattern
|
||||
this.matchFile(onResult, { relativePath: extraFilePath /* no workspace relative path */, basename });
|
||||
});
|
||||
}
|
||||
|
||||
let traverse = this.nodeJSTraversal;
|
||||
if (!this.maxFilesize) {
|
||||
if (platform.isMacintosh) {
|
||||
this.traversal = Traversal.MacFind;
|
||||
traverse = this.findTraversal;
|
||||
// Disable 'dir' for now (#11181, #11179, #11183, #11182).
|
||||
} /* else if (platform.isWindows) {
|
||||
this.traversal = Traversal.WindowsDir;
|
||||
traverse = this.windowsDirTraversal;
|
||||
} */ else if (platform.isLinux) {
|
||||
this.traversal = Traversal.LinuxFind;
|
||||
traverse = this.findTraversal;
|
||||
}
|
||||
}
|
||||
|
||||
const isNodeTraversal = traverse === this.nodeJSTraversal;
|
||||
if (!isNodeTraversal) {
|
||||
this.cmdForkStartTime = Date.now();
|
||||
}
|
||||
|
||||
// For each root folder
|
||||
flow.parallel<IFolderSearch, void>(folderQueries, (folderQuery: IFolderSearch, rootFolderDone: (err: Error, result: void) => void) => {
|
||||
this.call(traverse, this, folderQuery, onResult, (err?: Error) => {
|
||||
if (err) {
|
||||
if (isNodeTraversal) {
|
||||
rootFolderDone(err, undefined);
|
||||
} else {
|
||||
// fallback
|
||||
const errorMessage = toErrorMessage(err);
|
||||
console.error(errorMessage);
|
||||
this.errors.push(errorMessage);
|
||||
this.nodeJSTraversal(folderQuery, onResult, err => rootFolderDone(err, undefined));
|
||||
}
|
||||
} else {
|
||||
rootFolderDone(undefined, undefined);
|
||||
}
|
||||
});
|
||||
}, (err, result) => {
|
||||
done(err ? err[0] : null, this.isLimitHit);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private call(fun: Function, that: any, ...args: any[]): void {
|
||||
try {
|
||||
fun.apply(that, args);
|
||||
} catch (e) {
|
||||
args[args.length - 1](e);
|
||||
}
|
||||
}
|
||||
|
||||
private findTraversal(folderQuery: IFolderSearch, onResult: (result: IRawFileMatch) => void, cb: (err?: Error) => void): void {
|
||||
const rootFolder = folderQuery.folder;
|
||||
const isMac = platform.isMacintosh;
|
||||
let done = (err?: Error) => {
|
||||
done = () => { };
|
||||
cb(err);
|
||||
};
|
||||
let leftover = '';
|
||||
let first = true;
|
||||
const tree = this.initDirectoryTree();
|
||||
const cmd = this.spawnFindCmd(folderQuery);
|
||||
this.collectStdout(cmd, 'utf8', (err: Error, stdout?: string, last?: boolean) => {
|
||||
if (err) {
|
||||
done(err);
|
||||
return;
|
||||
}
|
||||
|
||||
// Mac: uses NFD unicode form on disk, but we want NFC
|
||||
const normalized = leftover + (isMac ? strings.normalizeNFC(stdout) : stdout);
|
||||
const relativeFiles = normalized.split('\n./');
|
||||
if (first && normalized.length >= 2) {
|
||||
first = false;
|
||||
relativeFiles[0] = relativeFiles[0].trim().substr(2);
|
||||
}
|
||||
|
||||
if (last) {
|
||||
const n = relativeFiles.length;
|
||||
relativeFiles[n - 1] = relativeFiles[n - 1].trim();
|
||||
if (!relativeFiles[n - 1]) {
|
||||
relativeFiles.pop();
|
||||
}
|
||||
} else {
|
||||
leftover = relativeFiles.pop();
|
||||
}
|
||||
|
||||
if (relativeFiles.length && relativeFiles[0].indexOf('\n') !== -1) {
|
||||
done(new Error('Splitting up files failed'));
|
||||
return;
|
||||
}
|
||||
|
||||
this.addDirectoryEntries(tree, rootFolder, relativeFiles, onResult);
|
||||
|
||||
if (last) {
|
||||
this.matchDirectoryTree(tree, rootFolder, onResult);
|
||||
done();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// protected windowsDirTraversal(rootFolder: string, onResult: (result: IRawFileMatch) => void, done: (err?: Error) => void): void {
|
||||
// const cmd = childProcess.spawn('cmd', ['/U', '/c', 'dir', '/s', '/b', '/a-d', rootFolder]);
|
||||
// this.readStdout(cmd, 'ucs2', (err: Error, stdout?: string) => {
|
||||
// if (err) {
|
||||
// done(err);
|
||||
// return;
|
||||
// }
|
||||
|
||||
// const relativeFiles = stdout.split(`\r\n${rootFolder}\\`);
|
||||
// relativeFiles[0] = relativeFiles[0].trim().substr(rootFolder.length + 1);
|
||||
// const n = relativeFiles.length;
|
||||
// relativeFiles[n - 1] = relativeFiles[n - 1].trim();
|
||||
// if (!relativeFiles[n - 1]) {
|
||||
// relativeFiles.pop();
|
||||
// }
|
||||
|
||||
// if (relativeFiles.length && relativeFiles[0].indexOf('\n') !== -1) {
|
||||
// done(new Error('Splitting up files failed'));
|
||||
// return;
|
||||
// }
|
||||
|
||||
// this.matchFiles(rootFolder, relativeFiles, onResult);
|
||||
|
||||
// done();
|
||||
// });
|
||||
// }
|
||||
|
||||
/**
|
||||
* Public for testing.
|
||||
*/
|
||||
public spawnFindCmd(folderQuery: IFolderSearch) {
|
||||
const excludePattern = this.folderExcludePatterns.get(folderQuery.folder);
|
||||
const basenames = excludePattern.getBasenameTerms();
|
||||
const pathTerms = excludePattern.getPathTerms();
|
||||
let args = ['-L', '.'];
|
||||
if (basenames.length || pathTerms.length) {
|
||||
args.push('-not', '(', '(');
|
||||
for (const basename of basenames) {
|
||||
args.push('-name', basename);
|
||||
args.push('-o');
|
||||
}
|
||||
for (const path of pathTerms) {
|
||||
args.push('-path', path);
|
||||
args.push('-o');
|
||||
}
|
||||
args.pop();
|
||||
args.push(')', '-prune', ')');
|
||||
}
|
||||
args.push('-type', 'f');
|
||||
return childProcess.spawn('find', args, { cwd: folderQuery.folder });
|
||||
}
|
||||
|
||||
/**
|
||||
* Public for testing.
|
||||
*/
|
||||
public readStdout(cmd: childProcess.ChildProcess, encoding: string, cb: (err: Error, stdout?: string) => void): void {
|
||||
let all = '';
|
||||
this.collectStdout(cmd, encoding, (err: Error, stdout?: string, last?: boolean) => {
|
||||
if (err) {
|
||||
cb(err);
|
||||
return;
|
||||
}
|
||||
|
||||
all += stdout;
|
||||
if (last) {
|
||||
cb(null, all);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private collectStdout(cmd: childProcess.ChildProcess, encoding: string, cb: (err: Error, stdout?: string, last?: boolean) => void): void {
|
||||
let done = (err: Error, stdout?: string, last?: boolean) => {
|
||||
if (err || last) {
|
||||
done = () => { };
|
||||
this.cmdForkResultTime = Date.now();
|
||||
}
|
||||
cb(err, stdout, last);
|
||||
};
|
||||
|
||||
this.forwardData(cmd.stdout, encoding, done);
|
||||
const stderr = this.collectData(cmd.stderr);
|
||||
|
||||
cmd.on('error', (err: Error) => {
|
||||
done(err);
|
||||
});
|
||||
|
||||
cmd.on('close', (code: number) => {
|
||||
if (code !== 0) {
|
||||
done(new Error(`find failed with error code ${code}: ${this.decodeData(stderr, encoding)}`));
|
||||
} else {
|
||||
done(null, '', true);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private forwardData(stream: Readable, encoding: string, cb: (err: Error, stdout?: string) => void): NodeStringDecoder {
|
||||
const decoder = new StringDecoder(encoding);
|
||||
stream.on('data', (data: Buffer) => {
|
||||
cb(null, decoder.write(data));
|
||||
});
|
||||
return decoder;
|
||||
}
|
||||
|
||||
private collectData(stream: Readable): Buffer[] {
|
||||
const buffers: Buffer[] = [];
|
||||
stream.on('data', (data: Buffer) => {
|
||||
buffers.push(data);
|
||||
});
|
||||
return buffers;
|
||||
}
|
||||
|
||||
private decodeData(buffers: Buffer[], encoding: string): string {
|
||||
const decoder = new StringDecoder(encoding);
|
||||
return buffers.map(buffer => decoder.write(buffer)).join('');
|
||||
}
|
||||
|
||||
private initDirectoryTree(): IDirectoryTree {
|
||||
const tree: IDirectoryTree = {
|
||||
rootEntries: [],
|
||||
pathToEntries: Object.create(null)
|
||||
};
|
||||
tree.pathToEntries['.'] = tree.rootEntries;
|
||||
return tree;
|
||||
}
|
||||
|
||||
private addDirectoryEntries({ pathToEntries }: IDirectoryTree, base: string, relativeFiles: string[], onResult: (result: IRawFileMatch) => void) {
|
||||
this.cmdResultCount += relativeFiles.length;
|
||||
|
||||
// Support relative paths to files from a root resource (ignores excludes)
|
||||
if (relativeFiles.indexOf(this.filePattern) !== -1) {
|
||||
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
|
||||
});
|
||||
}
|
||||
relativeFiles.forEach(add);
|
||||
}
|
||||
|
||||
private matchDirectoryTree({ rootEntries, pathToEntries }: IDirectoryTree, rootFolder: string, onResult: (result: IRawFileMatch) => void) {
|
||||
const self = this;
|
||||
const excludePattern = this.folderExcludePatterns.get(rootFolder);
|
||||
const filePattern = this.filePattern;
|
||||
function matchDirectory(entries: IDirectoryEntry[]) {
|
||||
self.directoriesWalked++;
|
||||
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 (excludePattern.test(relativePath, basename, () => filePattern !== basename ? entries.map(entry => entry.basename) : [])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sub = pathToEntries[relativePath];
|
||||
if (sub) {
|
||||
matchDirectory(sub);
|
||||
} else {
|
||||
self.filesWalked++;
|
||||
if (relativePath === filePattern) {
|
||||
continue; // ignore file if its path matches with the file pattern because that is already matched above
|
||||
}
|
||||
|
||||
self.matchFile(onResult, entry);
|
||||
}
|
||||
};
|
||||
}
|
||||
matchDirectory(rootEntries);
|
||||
}
|
||||
|
||||
private nodeJSTraversal(folderQuery: IFolderSearch, onResult: (result: IRawFileMatch) => void, done: (err?: Error) => void): void {
|
||||
this.directoriesWalked++;
|
||||
extfs.readdir(folderQuery.folder, (error: Error, files: string[]) => {
|
||||
if (error || this.isCanceled || this.isLimitHit) {
|
||||
return done();
|
||||
}
|
||||
|
||||
// Support relative paths to files from a root resource (ignores excludes)
|
||||
return this.checkFilePatternRelativeMatch(folderQuery.folder, (match, size) => {
|
||||
if (this.isCanceled || this.isLimitHit) {
|
||||
return done();
|
||||
}
|
||||
|
||||
// Report result from file pattern if matching
|
||||
if (match) {
|
||||
this.resultCount++;
|
||||
onResult({
|
||||
base: folderQuery.folder,
|
||||
relativePath: this.filePattern,
|
||||
basename: path.basename(this.filePattern),
|
||||
size
|
||||
});
|
||||
}
|
||||
|
||||
return this.doWalk(folderQuery, '', files, onResult, done);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public getStats(): IUncachedSearchStats {
|
||||
return {
|
||||
fromCache: false,
|
||||
traversal: Traversal[this.traversal],
|
||||
errors: this.errors,
|
||||
fileWalkStartTime: this.fileWalkStartTime,
|
||||
fileWalkResultTime: Date.now(),
|
||||
directoriesWalked: this.directoriesWalked,
|
||||
filesWalked: this.filesWalked,
|
||||
resultCount: this.resultCount,
|
||||
cmdForkStartTime: this.cmdForkStartTime,
|
||||
cmdForkResultTime: this.cmdForkResultTime,
|
||||
cmdResultCount: this.cmdResultCount
|
||||
};
|
||||
}
|
||||
|
||||
private checkFilePatternAbsoluteMatch(clb: (exists: boolean, size?: number) => void): void {
|
||||
if (!this.filePattern || !path.isAbsolute(this.filePattern)) {
|
||||
return clb(false);
|
||||
}
|
||||
|
||||
return fs.stat(this.filePattern, (error, stat) => {
|
||||
return clb(!error && !stat.isDirectory(), stat && stat.size); // only existing files
|
||||
});
|
||||
}
|
||||
|
||||
private checkFilePatternRelativeMatch(basePath: string, clb: (matchPath: string, size?: number) => void): void {
|
||||
if (!this.filePattern || path.isAbsolute(this.filePattern)) {
|
||||
return clb(null);
|
||||
}
|
||||
|
||||
const absolutePath = path.join(basePath, this.filePattern);
|
||||
|
||||
return fs.stat(absolutePath, (error, stat) => {
|
||||
return clb(!error && !stat.isDirectory() ? absolutePath : null, stat && stat.size); // only existing files
|
||||
});
|
||||
}
|
||||
|
||||
private doWalk(folderQuery: IFolderSearch, relativeParentPath: string, files: string[], onResult: (result: IRawFileMatch) => void, done: (error: Error) => void): void {
|
||||
const rootFolder = folderQuery.folder;
|
||||
|
||||
// Execute tasks on each file in parallel to optimize throughput
|
||||
flow.parallel(files, (file: string, clb: (error: Error, result: {}) => void): void => {
|
||||
|
||||
// Check canceled
|
||||
if (this.isCanceled || this.isLimitHit) {
|
||||
return clb(null, undefined);
|
||||
}
|
||||
|
||||
// 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
|
||||
let siblings = files;
|
||||
if (this.config.filePattern === file) {
|
||||
siblings = [];
|
||||
}
|
||||
|
||||
// Check exclude pattern
|
||||
let currentRelativePath = relativeParentPath ? [relativeParentPath, file].join(path.sep) : file;
|
||||
if (this.folderExcludePatterns.get(folderQuery.folder).test(currentRelativePath, file, () => siblings)) {
|
||||
return clb(null, undefined);
|
||||
}
|
||||
|
||||
// Use lstat to detect links
|
||||
let currentAbsolutePath = [rootFolder, currentRelativePath].join(path.sep);
|
||||
fs.lstat(currentAbsolutePath, (error, lstat) => {
|
||||
if (error || this.isCanceled || this.isLimitHit) {
|
||||
return clb(null, undefined);
|
||||
}
|
||||
|
||||
// If the path is a link, we must instead use fs.stat() to find out if the
|
||||
// link is a directory or not because lstat will always return the stat of
|
||||
// the link which is always a file.
|
||||
this.statLinkIfNeeded(currentAbsolutePath, lstat, (error, stat) => {
|
||||
if (error || this.isCanceled || this.isLimitHit) {
|
||||
return clb(null, undefined);
|
||||
}
|
||||
|
||||
// Directory: Follow directories
|
||||
if (stat.isDirectory()) {
|
||||
this.directoriesWalked++;
|
||||
|
||||
// to really prevent loops with links we need to resolve the real path of them
|
||||
return this.realPathIfNeeded(currentAbsolutePath, lstat, (error, realpath) => {
|
||||
if (error || this.isCanceled || this.isLimitHit) {
|
||||
return clb(null, undefined);
|
||||
}
|
||||
|
||||
if (this.walkedPaths[realpath]) {
|
||||
return clb(null, undefined); // escape when there are cycles (can happen with symlinks)
|
||||
}
|
||||
|
||||
this.walkedPaths[realpath] = true; // remember as walked
|
||||
|
||||
// Continue walking
|
||||
return extfs.readdir(currentAbsolutePath, (error: Error, children: string[]): void => {
|
||||
if (error || this.isCanceled || this.isLimitHit) {
|
||||
return clb(null, undefined);
|
||||
}
|
||||
|
||||
this.doWalk(folderQuery, currentRelativePath, children, onResult, err => clb(err, undefined));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// File: Check for match on file pattern and include pattern
|
||||
else {
|
||||
this.filesWalked++;
|
||||
if (currentRelativePath === this.filePattern) {
|
||||
return clb(null, undefined); // ignore file if its path matches with the file pattern because checkFilePatternRelativeMatch() takes care of those
|
||||
}
|
||||
|
||||
if (this.maxFilesize && types.isNumber(stat.size) && stat.size > this.maxFilesize) {
|
||||
return clb(null, undefined); // ignore file if max file size is hit
|
||||
}
|
||||
|
||||
this.matchFile(onResult, { base: rootFolder, relativePath: currentRelativePath, basename: file, size: stat.size });
|
||||
}
|
||||
|
||||
// Unwind
|
||||
return clb(null, undefined);
|
||||
});
|
||||
});
|
||||
}, (error: Error[]): void => {
|
||||
if (error) {
|
||||
error = arrays.coalesce(error); // find any error by removing null values first
|
||||
}
|
||||
|
||||
return done(error && error.length > 0 ? error[0] : null);
|
||||
});
|
||||
}
|
||||
|
||||
private matchFile(onResult: (result: IRawFileMatch) => void, candidate: IRawFileMatch): void {
|
||||
if (this.isFilePatternMatch(candidate.relativePath) && (!this.includePattern || this.includePattern(candidate.relativePath, candidate.basename))) {
|
||||
this.resultCount++;
|
||||
|
||||
if (this.maxResults && this.resultCount > this.maxResults) {
|
||||
this.isLimitHit = true;
|
||||
}
|
||||
|
||||
if (!this.isLimitHit) {
|
||||
onResult(candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isFilePatternMatch(path: string): boolean {
|
||||
|
||||
// Check for search pattern
|
||||
if (this.filePattern) {
|
||||
if (this.filePattern === '*') {
|
||||
return true; // support the all-matching wildcard
|
||||
}
|
||||
|
||||
return scorer.matches(path, this.normalizedFilePatternLowercase);
|
||||
}
|
||||
|
||||
// No patterns means we match all
|
||||
return true;
|
||||
}
|
||||
|
||||
private statLinkIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, stat: fs.Stats) => void): void {
|
||||
if (lstat.isSymbolicLink()) {
|
||||
return fs.stat(path, clb); // stat the target the link points to
|
||||
}
|
||||
|
||||
return clb(null, lstat); // not a link, so the stat is already ok for us
|
||||
}
|
||||
|
||||
private realPathIfNeeded(path: string, lstat: fs.Stats, clb: (error: Error, realpath?: string) => void): void {
|
||||
if (lstat.isSymbolicLink()) {
|
||||
return fs.realpath(path, (error, realpath) => {
|
||||
if (error) {
|
||||
return clb(error);
|
||||
}
|
||||
|
||||
return clb(null, realpath);
|
||||
});
|
||||
}
|
||||
|
||||
return clb(null, path);
|
||||
}
|
||||
}
|
||||
|
||||
export class Engine implements ISearchEngine<IRawFileMatch> {
|
||||
private folderQueries: IFolderSearch[];
|
||||
private extraFiles: string[];
|
||||
private walker: FileWalker;
|
||||
|
||||
constructor(config: IRawSearch) {
|
||||
this.folderQueries = config.folderQueries;
|
||||
this.extraFiles = config.extraFiles;
|
||||
|
||||
this.walker = new FileWalker(config);
|
||||
}
|
||||
|
||||
public search(onResult: (result: IRawFileMatch) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
|
||||
this.walker.walk(this.folderQueries, this.extraFiles, onResult, (err: Error, isLimitHit: boolean) => {
|
||||
done(err, {
|
||||
limitHit: isLimitHit,
|
||||
stats: this.walker.getStats()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.walker.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This class exists to provide one interface on top of two ParsedExpressions, one for absolute expressions and one for relative expressions.
|
||||
* The absolute and relative expressions don't "have" to be kept separate, but this keeps us from having to path.join every single
|
||||
* file searched, it's only used for a text search with a searchPath
|
||||
*/
|
||||
class AbsoluteAndRelativeParsedExpression {
|
||||
private absoluteParsedExpr: glob.ParsedExpression;
|
||||
private relativeParsedExpr: glob.ParsedExpression;
|
||||
|
||||
constructor(expr: glob.IExpression, private root: string) {
|
||||
this.init(expr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split the IExpression into its absolute and relative components, and glob.parse them separately.
|
||||
*/
|
||||
private init(expr: glob.IExpression): void {
|
||||
let absoluteGlobExpr: glob.IExpression;
|
||||
let relativeGlobExpr: glob.IExpression;
|
||||
Object.keys(expr)
|
||||
.filter(key => expr[key])
|
||||
.forEach(key => {
|
||||
if (path.isAbsolute(key)) {
|
||||
absoluteGlobExpr = absoluteGlobExpr || glob.getEmptyExpression();
|
||||
absoluteGlobExpr[key] = expr[key];
|
||||
} else {
|
||||
relativeGlobExpr = relativeGlobExpr || glob.getEmptyExpression();
|
||||
relativeGlobExpr[key] = expr[key];
|
||||
}
|
||||
});
|
||||
|
||||
this.absoluteParsedExpr = absoluteGlobExpr && glob.parse(absoluteGlobExpr, { trimForExclusions: true });
|
||||
this.relativeParsedExpr = relativeGlobExpr && glob.parse(relativeGlobExpr, { trimForExclusions: true });
|
||||
}
|
||||
|
||||
public test(_path: string, basename?: string, siblingsFn?: () => string[] | TPromise<string[]>): string | TPromise<string> {
|
||||
return (this.relativeParsedExpr && this.relativeParsedExpr(_path, basename, siblingsFn)) ||
|
||||
(this.absoluteParsedExpr && this.absoluteParsedExpr(path.join(this.root, _path), basename, siblingsFn));
|
||||
}
|
||||
|
||||
public getBasenameTerms(): string[] {
|
||||
const basenameTerms = [];
|
||||
if (this.absoluteParsedExpr) {
|
||||
basenameTerms.push(...glob.getBasenameTerms(this.absoluteParsedExpr));
|
||||
}
|
||||
|
||||
if (this.relativeParsedExpr) {
|
||||
basenameTerms.push(...glob.getBasenameTerms(this.relativeParsedExpr));
|
||||
}
|
||||
|
||||
return basenameTerms;
|
||||
}
|
||||
|
||||
public getPathTerms(): string[] {
|
||||
const pathTerms = [];
|
||||
if (this.absoluteParsedExpr) {
|
||||
pathTerms.push(...glob.getPathTerms(this.absoluteParsedExpr));
|
||||
}
|
||||
|
||||
if (this.relativeParsedExpr) {
|
||||
pathTerms.push(...glob.getPathTerms(this.relativeParsedExpr));
|
||||
}
|
||||
|
||||
return pathTerms;
|
||||
}
|
||||
}
|
||||
496
src/vs/workbench/services/search/node/rawSearchService.ts
Normal file
496
src/vs/workbench/services/search/node/rawSearchService.ts
Normal file
@@ -0,0 +1,496 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import fs = require('fs');
|
||||
import { isAbsolute, sep } from 'path';
|
||||
|
||||
import gracefulFs = require('graceful-fs');
|
||||
gracefulFs.gracefulify(fs);
|
||||
|
||||
import arrays = require('vs/base/common/arrays');
|
||||
import { compareByScore } from 'vs/base/common/comparers';
|
||||
import objects = require('vs/base/common/objects');
|
||||
import scorer = require('vs/base/common/scorer');
|
||||
import strings = require('vs/base/common/strings');
|
||||
import { PPromise, TPromise } from 'vs/base/common/winjs.base';
|
||||
import { FileWalker, Engine as FileSearchEngine } from 'vs/workbench/services/search/node/fileSearch';
|
||||
import { MAX_FILE_SIZE } from 'vs/platform/files/common/files';
|
||||
import { RipgrepEngine } from 'vs/workbench/services/search/node/ripgrepTextSearch';
|
||||
import { Engine as TextSearchEngine } from 'vs/workbench/services/search/node/textSearch';
|
||||
import { TextSearchWorkerProvider } from 'vs/workbench/services/search/node/textSearchWorkerProvider';
|
||||
import { IRawSearchService, IRawSearch, IRawFileMatch, ISerializedFileMatch, ISerializedSearchProgressItem, ISerializedSearchComplete, ISearchEngine, IFileSearchProgressItem } from './search';
|
||||
import { ICachedSearchStats, IProgress } from 'vs/platform/search/common/search';
|
||||
|
||||
export class SearchService implements IRawSearchService {
|
||||
|
||||
private static BATCH_SIZE = 512;
|
||||
|
||||
private caches: { [cacheKey: string]: Cache; } = Object.create(null);
|
||||
|
||||
private textSearchWorkerProvider: TextSearchWorkerProvider;
|
||||
|
||||
public fileSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
|
||||
return this.doFileSearch(FileSearchEngine, config, SearchService.BATCH_SIZE);
|
||||
}
|
||||
|
||||
public textSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
|
||||
return config.useRipgrep ?
|
||||
this.ripgrepTextSearch(config) :
|
||||
this.legacyTextSearch(config);
|
||||
}
|
||||
|
||||
public ripgrepTextSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
|
||||
config.maxFilesize = MAX_FILE_SIZE;
|
||||
let engine = new RipgrepEngine(config);
|
||||
|
||||
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
|
||||
// Use BatchedCollector to get new results to the frontend every 2s at least, until 50 results have been returned
|
||||
const collector = new BatchedCollector<ISerializedFileMatch>(SearchService.BATCH_SIZE, p);
|
||||
engine.search((match) => {
|
||||
collector.addItem(match, match.numMatches);
|
||||
}, (message) => {
|
||||
p(message);
|
||||
}, (error, stats) => {
|
||||
collector.flush();
|
||||
|
||||
if (error) {
|
||||
e(error);
|
||||
} else {
|
||||
c(stats);
|
||||
}
|
||||
});
|
||||
}, () => {
|
||||
engine.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
public legacyTextSearch(config: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
|
||||
if (!this.textSearchWorkerProvider) {
|
||||
this.textSearchWorkerProvider = new TextSearchWorkerProvider();
|
||||
}
|
||||
|
||||
let engine = new TextSearchEngine(
|
||||
config,
|
||||
new FileWalker({
|
||||
folderQueries: config.folderQueries,
|
||||
extraFiles: config.extraFiles,
|
||||
includePattern: config.includePattern,
|
||||
excludePattern: config.excludePattern,
|
||||
filePattern: config.filePattern,
|
||||
maxFilesize: MAX_FILE_SIZE
|
||||
}),
|
||||
this.textSearchWorkerProvider);
|
||||
|
||||
return this.doTextSearch(engine, SearchService.BATCH_SIZE);
|
||||
}
|
||||
|
||||
public doFileSearch(EngineClass: { new(config: IRawSearch): ISearchEngine<IRawFileMatch>; }, config: IRawSearch, batchSize?: number): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
|
||||
|
||||
if (config.sortByScore) {
|
||||
let sortedSearch = this.trySortedSearchFromCache(config);
|
||||
if (!sortedSearch) {
|
||||
const walkerConfig = config.maxResults ? objects.assign({}, config, { maxResults: null }) : config;
|
||||
const engine = new EngineClass(walkerConfig);
|
||||
sortedSearch = this.doSortedSearch(engine, config);
|
||||
}
|
||||
|
||||
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
|
||||
process.nextTick(() => { // allow caller to register progress callback first
|
||||
sortedSearch.then(([result, rawMatches]) => {
|
||||
const serializedMatches = rawMatches.map(rawMatch => this.rawMatchToSearchItem(rawMatch));
|
||||
this.sendProgress(serializedMatches, p, batchSize);
|
||||
c(result);
|
||||
}, e, p);
|
||||
});
|
||||
}, () => {
|
||||
sortedSearch.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
let searchPromise: PPromise<void, IFileSearchProgressItem>;
|
||||
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
|
||||
const engine = new EngineClass(config);
|
||||
searchPromise = this.doSearch(engine, batchSize)
|
||||
.then(c, e, progress => {
|
||||
if (Array.isArray(progress)) {
|
||||
p(progress.map(m => this.rawMatchToSearchItem(m)));
|
||||
} else if ((<IRawFileMatch>progress).relativePath) {
|
||||
p(this.rawMatchToSearchItem(<IRawFileMatch>progress));
|
||||
} else {
|
||||
p(<IProgress>progress);
|
||||
}
|
||||
});
|
||||
}, () => {
|
||||
searchPromise.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
private rawMatchToSearchItem(match: IRawFileMatch): ISerializedFileMatch {
|
||||
return { path: match.base ? [match.base, match.relativePath].join(sep) : match.relativePath };
|
||||
}
|
||||
|
||||
private doSortedSearch(engine: ISearchEngine<IRawFileMatch>, config: IRawSearch): PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress> {
|
||||
let searchPromise: PPromise<void, IFileSearchProgressItem>;
|
||||
let allResultsPromise = new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>((c, e, p) => {
|
||||
let results: IRawFileMatch[] = [];
|
||||
searchPromise = this.doSearch(engine, -1)
|
||||
.then(result => {
|
||||
c([result, results]);
|
||||
}, e, progress => {
|
||||
if (Array.isArray(progress)) {
|
||||
results = progress;
|
||||
} else {
|
||||
p(progress);
|
||||
}
|
||||
});
|
||||
}, () => {
|
||||
searchPromise.cancel();
|
||||
});
|
||||
|
||||
let cache: Cache;
|
||||
if (config.cacheKey) {
|
||||
cache = this.getOrCreateCache(config.cacheKey);
|
||||
cache.resultsToSearchCache[config.filePattern] = allResultsPromise;
|
||||
allResultsPromise.then(null, err => {
|
||||
delete cache.resultsToSearchCache[config.filePattern];
|
||||
});
|
||||
allResultsPromise = this.preventCancellation(allResultsPromise);
|
||||
}
|
||||
|
||||
return new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress>((c, e, p) => {
|
||||
allResultsPromise.then(([result, results]) => {
|
||||
const scorerCache: ScorerCache = cache ? cache.scorerCache : Object.create(null);
|
||||
const unsortedResultTime = Date.now();
|
||||
const sortedResults = this.sortResults(config, results, scorerCache);
|
||||
const sortedResultTime = Date.now();
|
||||
|
||||
c([{
|
||||
stats: objects.assign({}, result.stats, {
|
||||
unsortedResultTime,
|
||||
sortedResultTime
|
||||
}),
|
||||
limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults
|
||||
}, sortedResults]);
|
||||
}, e, p);
|
||||
}, () => {
|
||||
allResultsPromise.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
private getOrCreateCache(cacheKey: string): Cache {
|
||||
const existing = this.caches[cacheKey];
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
return this.caches[cacheKey] = new Cache();
|
||||
}
|
||||
|
||||
private trySortedSearchFromCache(config: IRawSearch): PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress> {
|
||||
const cache = config.cacheKey && this.caches[config.cacheKey];
|
||||
if (!cache) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cacheLookupStartTime = Date.now();
|
||||
const cached = this.getResultsFromCache(cache, config.filePattern);
|
||||
if (cached) {
|
||||
return new PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IProgress>((c, e, p) => {
|
||||
cached.then(([result, results, cacheStats]) => {
|
||||
const cacheLookupResultTime = Date.now();
|
||||
const sortedResults = this.sortResults(config, results, cache.scorerCache);
|
||||
const sortedResultTime = Date.now();
|
||||
|
||||
const stats: ICachedSearchStats = {
|
||||
fromCache: true,
|
||||
cacheLookupStartTime: cacheLookupStartTime,
|
||||
cacheFilterStartTime: cacheStats.cacheFilterStartTime,
|
||||
cacheLookupResultTime: cacheLookupResultTime,
|
||||
cacheEntryCount: cacheStats.cacheFilterResultCount,
|
||||
resultCount: results.length
|
||||
};
|
||||
if (config.sortByScore) {
|
||||
stats.unsortedResultTime = cacheLookupResultTime;
|
||||
stats.sortedResultTime = sortedResultTime;
|
||||
}
|
||||
if (!cacheStats.cacheWasResolved) {
|
||||
stats.joined = result.stats;
|
||||
}
|
||||
c([
|
||||
{
|
||||
limitHit: result.limitHit || typeof config.maxResults === 'number' && results.length > config.maxResults,
|
||||
stats: stats
|
||||
},
|
||||
sortedResults
|
||||
]);
|
||||
}, e, p);
|
||||
}, () => {
|
||||
cached.cancel();
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private sortResults(config: IRawSearch, results: IRawFileMatch[], scorerCache: ScorerCache): IRawFileMatch[] {
|
||||
const filePattern = config.filePattern;
|
||||
const normalizedSearchValue = strings.stripWildcards(filePattern).toLowerCase();
|
||||
const compare = (elementA: IRawFileMatch, elementB: IRawFileMatch) => compareByScore(elementA, elementB, FileMatchAccessor, filePattern, normalizedSearchValue, scorerCache);
|
||||
return arrays.top(results, compare, config.maxResults);
|
||||
}
|
||||
|
||||
private sendProgress(results: ISerializedFileMatch[], progressCb: (batch: ISerializedFileMatch[]) => void, batchSize: number) {
|
||||
if (batchSize && batchSize > 0) {
|
||||
for (let i = 0; i < results.length; i += batchSize) {
|
||||
progressCb(results.slice(i, i + batchSize));
|
||||
}
|
||||
} else {
|
||||
progressCb(results);
|
||||
}
|
||||
}
|
||||
|
||||
private getResultsFromCache(cache: Cache, searchValue: string): PPromise<[ISerializedSearchComplete, IRawFileMatch[], CacheStats], IProgress> {
|
||||
if (isAbsolute(searchValue)) {
|
||||
return null; // bypass cache if user looks up an absolute path where matching goes directly on disk
|
||||
}
|
||||
|
||||
// Find cache entries by prefix of search value
|
||||
const hasPathSep = searchValue.indexOf(sep) >= 0;
|
||||
let cached: PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>;
|
||||
let wasResolved: boolean;
|
||||
for (let 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) {
|
||||
continue; // since a path character widens the search for potential more matches, require it in previous search too
|
||||
}
|
||||
|
||||
const c = cache.resultsToSearchCache[previousSearch];
|
||||
c.then(() => { wasResolved = false; });
|
||||
wasResolved = true;
|
||||
cached = this.preventCancellation(c);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!cached) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new PPromise<[ISerializedSearchComplete, IRawFileMatch[], CacheStats], IProgress>((c, e, p) => {
|
||||
cached.then(([complete, cachedEntries]) => {
|
||||
const cacheFilterStartTime = Date.now();
|
||||
|
||||
// Pattern match on results
|
||||
let results: IRawFileMatch[] = [];
|
||||
const normalizedSearchValueLowercase = strings.stripWildcards(searchValue).toLowerCase();
|
||||
for (let i = 0; i < cachedEntries.length; i++) {
|
||||
let entry = cachedEntries[i];
|
||||
|
||||
// Check if this entry is a match for the search value
|
||||
if (!scorer.matches(entry.relativePath, normalizedSearchValueLowercase)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
results.push(entry);
|
||||
}
|
||||
|
||||
c([complete, results, {
|
||||
cacheWasResolved: wasResolved,
|
||||
cacheFilterStartTime: cacheFilterStartTime,
|
||||
cacheFilterResultCount: cachedEntries.length
|
||||
}]);
|
||||
}, e, p);
|
||||
}, () => {
|
||||
cached.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
private doTextSearch(engine: TextSearchEngine, batchSize: number): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
|
||||
return new PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>((c, e, p) => {
|
||||
// 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, p);
|
||||
engine.search((matches) => {
|
||||
const totalMatches = matches.reduce((acc, m) => acc + m.numMatches, 0);
|
||||
collector.addItems(matches, totalMatches);
|
||||
}, (progress) => {
|
||||
p(progress);
|
||||
}, (error, stats) => {
|
||||
collector.flush();
|
||||
|
||||
if (error) {
|
||||
e(error);
|
||||
} else {
|
||||
c(stats);
|
||||
}
|
||||
});
|
||||
}, () => {
|
||||
engine.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
private doSearch(engine: ISearchEngine<IRawFileMatch>, batchSize?: number): PPromise<ISerializedSearchComplete, IFileSearchProgressItem> {
|
||||
return new PPromise<ISerializedSearchComplete, IFileSearchProgressItem>((c, e, p) => {
|
||||
let batch: IRawFileMatch[] = [];
|
||||
engine.search((match) => {
|
||||
if (match) {
|
||||
if (batchSize) {
|
||||
batch.push(match);
|
||||
if (batchSize > 0 && batch.length >= batchSize) {
|
||||
p(batch);
|
||||
batch = [];
|
||||
}
|
||||
} else {
|
||||
p(match);
|
||||
}
|
||||
}
|
||||
}, (progress) => {
|
||||
p(progress);
|
||||
}, (error, stats) => {
|
||||
if (batch.length) {
|
||||
p(batch);
|
||||
}
|
||||
if (error) {
|
||||
e(error);
|
||||
} else {
|
||||
c(stats);
|
||||
}
|
||||
});
|
||||
}, () => {
|
||||
engine.cancel();
|
||||
});
|
||||
}
|
||||
|
||||
public clearCache(cacheKey: string): TPromise<void> {
|
||||
delete this.caches[cacheKey];
|
||||
return TPromise.as(undefined);
|
||||
}
|
||||
|
||||
private preventCancellation<C, P>(promise: PPromise<C, P>): PPromise<C, P> {
|
||||
return new PPromise<C, P>((c, e, p) => {
|
||||
// Allow for piled up cancellations to come through first.
|
||||
process.nextTick(() => {
|
||||
promise.then(c, e, p);
|
||||
});
|
||||
}, () => {
|
||||
// Do not propagate.
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Cache {
|
||||
|
||||
public resultsToSearchCache: { [searchValue: string]: PPromise<[ISerializedSearchComplete, IRawFileMatch[]], IFileSearchProgressItem>; } = Object.create(null);
|
||||
|
||||
public scorerCache: ScorerCache = Object.create(null);
|
||||
}
|
||||
|
||||
interface ScorerCache {
|
||||
[key: string]: number;
|
||||
}
|
||||
|
||||
class FileMatchAccessor {
|
||||
|
||||
public static getLabel(match: IRawFileMatch): string {
|
||||
return match.basename;
|
||||
}
|
||||
|
||||
public static getResourcePath(match: IRawFileMatch): string {
|
||||
return match.relativePath;
|
||||
}
|
||||
}
|
||||
|
||||
interface CacheStats {
|
||||
cacheWasResolved: boolean;
|
||||
cacheFilterStartTime: number;
|
||||
cacheFilterResultCount: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
class BatchedCollector<T> {
|
||||
private static TIMEOUT = 4000;
|
||||
|
||||
// After RUN_TIMEOUT_UNTIL_COUNT items have been collected, stop flushing on timeout
|
||||
private static START_BATCH_AFTER_COUNT = 50;
|
||||
|
||||
private totalNumberCompleted = 0;
|
||||
private batch: T[] = [];
|
||||
private batchSize = 0;
|
||||
private timeoutHandle: number;
|
||||
|
||||
constructor(private maxBatchSize: number, private cb: (items: T | T[]) => void) {
|
||||
}
|
||||
|
||||
addItem(item: T, size: number): void {
|
||||
if (!item) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.maxBatchSize > 0) {
|
||||
this.addItemToBatch(item, size);
|
||||
} else {
|
||||
this.cb(item);
|
||||
}
|
||||
}
|
||||
|
||||
addItems(items: T[], size: number): void {
|
||||
if (!items) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.maxBatchSize > 0) {
|
||||
this.addItemsToBatch(items, size);
|
||||
} else {
|
||||
this.cb(items);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
553
src/vs/workbench/services/search/node/ripgrepTextSearch.ts
Normal file
553
src/vs/workbench/services/search/node/ripgrepTextSearch.ts
Normal file
@@ -0,0 +1,553 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { EventEmitter } from 'events';
|
||||
import * as path from 'path';
|
||||
import { StringDecoder, NodeStringDecoder } from 'string_decoder';
|
||||
|
||||
import * as cp from 'child_process';
|
||||
import { rgPath } from 'vscode-ripgrep';
|
||||
|
||||
import objects = require('vs/base/common/objects');
|
||||
import platform = require('vs/base/common/platform');
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import * as paths from 'vs/base/common/paths';
|
||||
import * as extfs from 'vs/base/node/extfs';
|
||||
import * as encoding from 'vs/base/node/encoding';
|
||||
import * as glob from 'vs/base/common/glob';
|
||||
import { ILineMatch, ISearchLog } from 'vs/platform/search/common/search';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
|
||||
import { ISerializedFileMatch, ISerializedSearchComplete, IRawSearch, IFolderSearch } from './search';
|
||||
|
||||
export class RipgrepEngine {
|
||||
private isDone = false;
|
||||
private rgProc: cp.ChildProcess;
|
||||
private postProcessExclusions: glob.ParsedExpression;
|
||||
|
||||
private ripgrepParser: RipgrepParser;
|
||||
|
||||
private resultsHandledP: TPromise<any> = TPromise.wrap(null);
|
||||
|
||||
constructor(private config: IRawSearch) {
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.isDone = true;
|
||||
this.ripgrepParser.cancel();
|
||||
this.rgProc.kill();
|
||||
}
|
||||
|
||||
// TODO@Rob - make promise-based once the old search is gone, and I don't need them to have matching interfaces anymore
|
||||
search(onResult: (match: ISerializedFileMatch) => void, onMessage: (message: ISearchLog) => void, done: (error: Error, complete: ISerializedSearchComplete) => void): void {
|
||||
if (!this.config.folderQueries.length && !this.config.extraFiles.length) {
|
||||
done(null, {
|
||||
limitHit: false,
|
||||
stats: null
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const rgArgs = getRgArgs(this.config);
|
||||
if (rgArgs.siblingClauses) {
|
||||
this.postProcessExclusions = glob.parseToAsync(rgArgs.siblingClauses, { trimForExclusions: true });
|
||||
}
|
||||
|
||||
const cwd = platform.isWindows ? 'c:/' : '/';
|
||||
process.nextTick(() => { // Allow caller to register progress callback
|
||||
const escapedArgs = rgArgs.globArgs
|
||||
.map(arg => arg.match(/^-/) ? arg : `'${arg}'`)
|
||||
.join(' ');
|
||||
|
||||
const rgCmd = `rg ${escapedArgs}\n - cwd: ${cwd}\n`;
|
||||
onMessage({ message: rgCmd });
|
||||
if (rgArgs.siblingClauses) {
|
||||
onMessage({ message: ` - Sibling clauses: ${JSON.stringify(rgArgs.siblingClauses)}\n` });
|
||||
}
|
||||
});
|
||||
this.rgProc = cp.spawn(rgPath, rgArgs.globArgs, { cwd });
|
||||
|
||||
this.ripgrepParser = new RipgrepParser(this.config.maxResults, cwd);
|
||||
this.ripgrepParser.on('result', (match: ISerializedFileMatch) => {
|
||||
if (this.postProcessExclusions) {
|
||||
const handleResultP = (<TPromise<string>>this.postProcessExclusions(match.path, undefined, () => getSiblings(match.path)))
|
||||
.then(globMatch => {
|
||||
if (!globMatch) {
|
||||
onResult(match);
|
||||
}
|
||||
});
|
||||
|
||||
this.resultsHandledP = TPromise.join([this.resultsHandledP, handleResultP]);
|
||||
} else {
|
||||
onResult(match);
|
||||
}
|
||||
});
|
||||
this.ripgrepParser.on('hitLimit', () => {
|
||||
this.cancel();
|
||||
done(null, {
|
||||
limitHit: true,
|
||||
stats: null
|
||||
});
|
||||
});
|
||||
|
||||
this.rgProc.stdout.on('data', data => {
|
||||
this.ripgrepParser.handleData(data);
|
||||
});
|
||||
|
||||
let gotData = false;
|
||||
this.rgProc.stdout.once('data', () => gotData = true);
|
||||
|
||||
let stderr = '';
|
||||
this.rgProc.stderr.on('data', data => {
|
||||
const message = data.toString();
|
||||
onMessage({ message });
|
||||
stderr += message;
|
||||
});
|
||||
|
||||
this.rgProc.on('close', code => {
|
||||
// Trigger last result, then wait on async result handling
|
||||
this.ripgrepParser.flush();
|
||||
this.resultsHandledP.then(() => {
|
||||
this.rgProc = null;
|
||||
if (!this.isDone) {
|
||||
this.isDone = true;
|
||||
let displayMsg: string;
|
||||
if (stderr && !gotData && (displayMsg = this.rgErrorMsgForDisplay(stderr))) {
|
||||
done(new Error(displayMsg), {
|
||||
limitHit: false,
|
||||
stats: null
|
||||
});
|
||||
} else {
|
||||
done(null, {
|
||||
limitHit: false,
|
||||
stats: null
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the first line of stderr and return an error for display or undefined, based on a whitelist.
|
||||
* Ripgrep produces stderr output which is not from a fatal error, and we only want the search to be
|
||||
* "failed" when a fatal error was produced.
|
||||
*/
|
||||
private rgErrorMsgForDisplay(msg: string): string | undefined {
|
||||
const firstLine = msg.split('\n')[0];
|
||||
|
||||
if (strings.startsWith(firstLine, 'Error parsing regex')) {
|
||||
return firstLine;
|
||||
}
|
||||
|
||||
if (strings.startsWith(firstLine, 'error parsing glob')) {
|
||||
return firstLine;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class RipgrepParser extends EventEmitter {
|
||||
private static RESULT_REGEX = /^\u001b\[m(\d+)\u001b\[m:(.*)(\r?)/;
|
||||
private static FILE_REGEX = /^\u001b\[m(.+)\u001b\[m$/;
|
||||
|
||||
public static MATCH_START_MARKER = '\u001b[m\u001b[31m';
|
||||
public static MATCH_END_MARKER = '\u001b[m';
|
||||
|
||||
private fileMatch: FileMatch;
|
||||
private remainder: string;
|
||||
private isDone: boolean;
|
||||
private stringDecoder: NodeStringDecoder;
|
||||
|
||||
private numResults = 0;
|
||||
|
||||
constructor(private maxResults: number, private rootFolder: string) {
|
||||
super();
|
||||
this.stringDecoder = new StringDecoder();
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
this.isDone = true;
|
||||
}
|
||||
|
||||
public flush(): void {
|
||||
this.handleDecodedData(this.stringDecoder.end());
|
||||
|
||||
if (this.fileMatch) {
|
||||
this.onResult();
|
||||
}
|
||||
}
|
||||
|
||||
public handleData(data: Buffer | string): void {
|
||||
const dataStr = typeof data === 'string' ? data : this.stringDecoder.write(data);
|
||||
this.handleDecodedData(dataStr);
|
||||
}
|
||||
|
||||
private handleDecodedData(decodedData: string): void {
|
||||
// If the previous data chunk didn't end in a newline, prepend it to this chunk
|
||||
const dataStr = this.remainder ?
|
||||
this.remainder + decodedData :
|
||||
decodedData;
|
||||
|
||||
const dataLines: string[] = dataStr.split(/\r\n|\n/);
|
||||
this.remainder = dataLines[dataLines.length - 1] ? dataLines.pop() : null;
|
||||
|
||||
for (let l = 0; l < dataLines.length; l++) {
|
||||
const outputLine = dataLines[l].trim();
|
||||
if (this.isDone) {
|
||||
break;
|
||||
}
|
||||
|
||||
let r: RegExpMatchArray;
|
||||
if (r = outputLine.match(RipgrepParser.RESULT_REGEX)) {
|
||||
const lineNum = parseInt(r[1]) - 1;
|
||||
let matchText = r[2];
|
||||
|
||||
// workaround https://github.com/BurntSushi/ripgrep/issues/416
|
||||
// If the match line ended with \r, append a match end marker so the match isn't lost
|
||||
if (r[3]) {
|
||||
matchText += RipgrepParser.MATCH_END_MARKER;
|
||||
}
|
||||
|
||||
// Line is a result - add to collected results for the current file path
|
||||
this.handleMatchLine(outputLine, lineNum, matchText);
|
||||
} else if (r = outputLine.match(RipgrepParser.FILE_REGEX)) {
|
||||
// Line is a file path - send all collected results for the previous file path
|
||||
if (this.fileMatch) {
|
||||
this.onResult();
|
||||
}
|
||||
|
||||
this.fileMatch = new FileMatch(path.isAbsolute(r[1]) ? r[1] : path.join(this.rootFolder, r[1]));
|
||||
} else {
|
||||
// Line is empty (or malformed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private handleMatchLine(outputLine: string, lineNum: number, text: string): void {
|
||||
const lineMatch = new LineMatch(text, lineNum);
|
||||
this.fileMatch.addMatch(lineMatch);
|
||||
|
||||
let lastMatchEndPos = 0;
|
||||
let matchTextStartPos = -1;
|
||||
|
||||
// Track positions with color codes subtracted - offsets in the final text preview result
|
||||
let matchTextStartRealIdx = -1;
|
||||
let textRealIdx = 0;
|
||||
let hitLimit = false;
|
||||
|
||||
const realTextParts: string[] = [];
|
||||
|
||||
for (let i = 0; i < text.length - (RipgrepParser.MATCH_END_MARKER.length - 1);) {
|
||||
if (text.substr(i, RipgrepParser.MATCH_START_MARKER.length) === RipgrepParser.MATCH_START_MARKER) {
|
||||
// Match start
|
||||
const chunk = text.slice(lastMatchEndPos, i);
|
||||
realTextParts.push(chunk);
|
||||
i += RipgrepParser.MATCH_START_MARKER.length;
|
||||
matchTextStartPos = i;
|
||||
matchTextStartRealIdx = textRealIdx;
|
||||
} else if (text.substr(i, RipgrepParser.MATCH_END_MARKER.length) === RipgrepParser.MATCH_END_MARKER) {
|
||||
// Match end
|
||||
const chunk = text.slice(matchTextStartPos, i);
|
||||
realTextParts.push(chunk);
|
||||
if (!hitLimit) {
|
||||
lineMatch.addMatch(matchTextStartRealIdx, textRealIdx - matchTextStartRealIdx);
|
||||
}
|
||||
|
||||
matchTextStartPos = -1;
|
||||
matchTextStartRealIdx = -1;
|
||||
i += RipgrepParser.MATCH_END_MARKER.length;
|
||||
lastMatchEndPos = i;
|
||||
this.numResults++;
|
||||
|
||||
// Check hit maxResults limit
|
||||
if (this.numResults >= this.maxResults) {
|
||||
// Finish the line, then report the result below
|
||||
hitLimit = true;
|
||||
}
|
||||
} else {
|
||||
i++;
|
||||
textRealIdx++;
|
||||
}
|
||||
}
|
||||
|
||||
const chunk = text.slice(lastMatchEndPos);
|
||||
realTextParts.push(chunk);
|
||||
|
||||
// Replace preview with version without color codes
|
||||
const preview = realTextParts.join('');
|
||||
lineMatch.preview = preview;
|
||||
|
||||
if (hitLimit) {
|
||||
this.cancel();
|
||||
this.onResult();
|
||||
this.emit('hitLimit');
|
||||
}
|
||||
}
|
||||
|
||||
private onResult(): void {
|
||||
this.emit('result', this.fileMatch.serialize());
|
||||
this.fileMatch = null;
|
||||
}
|
||||
}
|
||||
|
||||
export class FileMatch implements ISerializedFileMatch {
|
||||
path: string;
|
||||
lineMatches: LineMatch[];
|
||||
|
||||
constructor(path: string) {
|
||||
this.path = path;
|
||||
this.lineMatches = [];
|
||||
}
|
||||
|
||||
addMatch(lineMatch: LineMatch): void {
|
||||
this.lineMatches.push(lineMatch);
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.lineMatches.length === 0;
|
||||
}
|
||||
|
||||
serialize(): ISerializedFileMatch {
|
||||
let lineMatches: ILineMatch[] = [];
|
||||
let numMatches = 0;
|
||||
|
||||
for (let i = 0; i < this.lineMatches.length; i++) {
|
||||
numMatches += this.lineMatches[i].offsetAndLengths.length;
|
||||
lineMatches.push(this.lineMatches[i].serialize());
|
||||
}
|
||||
|
||||
return {
|
||||
path: this.path,
|
||||
lineMatches,
|
||||
numMatches
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class LineMatch implements ILineMatch {
|
||||
preview: string;
|
||||
lineNumber: number;
|
||||
offsetAndLengths: number[][];
|
||||
|
||||
constructor(preview: string, lineNumber: number) {
|
||||
this.preview = preview.replace(/(\r|\n)*$/, '');
|
||||
this.lineNumber = lineNumber;
|
||||
this.offsetAndLengths = [];
|
||||
}
|
||||
|
||||
getText(): string {
|
||||
return this.preview;
|
||||
}
|
||||
|
||||
getLineNumber(): number {
|
||||
return this.lineNumber;
|
||||
}
|
||||
|
||||
addMatch(offset: number, length: number): void {
|
||||
this.offsetAndLengths.push([offset, length]);
|
||||
}
|
||||
|
||||
serialize(): ILineMatch {
|
||||
const result = {
|
||||
preview: this.preview,
|
||||
lineNumber: this.lineNumber,
|
||||
offsetAndLengths: this.offsetAndLengths
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
interface IRgGlobResult {
|
||||
globArgs: string[];
|
||||
siblingClauses: glob.IExpression;
|
||||
}
|
||||
|
||||
function foldersToRgExcludeGlobs(folderQueries: IFolderSearch[], globalExclude: glob.IExpression, excludesToSkip: Set<string>): IRgGlobResult {
|
||||
const globArgs: string[] = [];
|
||||
let siblingClauses: glob.IExpression = {};
|
||||
folderQueries.forEach(folderQuery => {
|
||||
const totalExcludePattern = objects.assign({}, folderQuery.excludePattern || {}, globalExclude || {});
|
||||
const result = globExprsToRgGlobs(totalExcludePattern, folderQuery.folder, excludesToSkip);
|
||||
globArgs.push(...result.globArgs);
|
||||
if (result.siblingClauses) {
|
||||
siblingClauses = objects.assign(siblingClauses, result.siblingClauses);
|
||||
}
|
||||
});
|
||||
|
||||
return { globArgs, siblingClauses };
|
||||
}
|
||||
|
||||
function foldersToIncludeGlobs(folderQueries: IFolderSearch[], globalInclude: glob.IExpression): string[] {
|
||||
const globArgs = [];
|
||||
folderQueries.forEach(folderQuery => {
|
||||
const totalIncludePattern = objects.assign({}, globalInclude || {}, folderQuery.includePattern || {});
|
||||
const result = globExprsToRgGlobs(totalIncludePattern, folderQuery.folder);
|
||||
globArgs.push(...result.globArgs);
|
||||
});
|
||||
|
||||
return globArgs;
|
||||
}
|
||||
|
||||
function globExprsToRgGlobs(patterns: glob.IExpression, folder: string, excludesToSkip?: Set<string>): IRgGlobResult {
|
||||
const globArgs: string[] = [];
|
||||
let siblingClauses: glob.IExpression = null;
|
||||
Object.keys(patterns)
|
||||
.forEach(key => {
|
||||
if (excludesToSkip && excludesToSkip.has(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const value = patterns[key];
|
||||
key = getAbsoluteGlob(folder, key);
|
||||
|
||||
if (typeof value === 'boolean' && value) {
|
||||
globArgs.push(fixDriveC(key));
|
||||
} else if (value && value.when) {
|
||||
if (!siblingClauses) {
|
||||
siblingClauses = {};
|
||||
}
|
||||
|
||||
siblingClauses[key] = value;
|
||||
}
|
||||
});
|
||||
|
||||
return { globArgs, siblingClauses };
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a glob like "node_modules/**" in "/foo/bar" to "/foo/bar/node_modules/**".
|
||||
* Special cases C:/foo paths to write the glob like /foo instead - see https://github.com/BurntSushi/ripgrep/issues/530.
|
||||
*
|
||||
* Exported for testing
|
||||
*/
|
||||
export function getAbsoluteGlob(folder: string, key: string): string {
|
||||
const absolute = paths.isAbsolute(key) ?
|
||||
key :
|
||||
path.join(folder, key);
|
||||
|
||||
return trimTrailingSlash(absolute);
|
||||
}
|
||||
|
||||
function trimTrailingSlash(str: string): string {
|
||||
str = strings.rtrim(str, '\\');
|
||||
return strings.rtrim(str, '/');
|
||||
}
|
||||
|
||||
export function fixDriveC(path: string): string {
|
||||
const root = paths.getRoot(path);
|
||||
return root.toLowerCase() === 'c:/' ?
|
||||
path.replace(/^c:[/\\]/i, '/') :
|
||||
path;
|
||||
}
|
||||
|
||||
function getRgArgs(config: IRawSearch): IRgGlobResult {
|
||||
const args = ['--hidden', '--heading', '--line-number', '--color', 'ansi', '--colors', 'path:none', '--colors', 'line:none', '--colors', 'match:fg:red', '--colors', 'match:style:nobold'];
|
||||
args.push(config.contentPattern.isCaseSensitive ? '--case-sensitive' : '--ignore-case');
|
||||
|
||||
// includePattern can't have siblingClauses
|
||||
foldersToIncludeGlobs(config.folderQueries, config.includePattern).forEach(globArg => {
|
||||
args.push('-g', globArg);
|
||||
});
|
||||
|
||||
let siblingClauses: glob.IExpression;
|
||||
|
||||
// Find excludes that are exactly the same in all folderQueries - e.g. from user settings, and that start with `**`.
|
||||
// To make the command shorter, don't resolve these against every folderQuery path - see #33189.
|
||||
const universalExcludes = findUniversalExcludes(config.folderQueries);
|
||||
const rgGlobs = foldersToRgExcludeGlobs(config.folderQueries, config.excludePattern, universalExcludes);
|
||||
rgGlobs.globArgs
|
||||
.forEach(rgGlob => args.push('-g', `!${rgGlob}`));
|
||||
if (universalExcludes) {
|
||||
universalExcludes
|
||||
.forEach(exclude => args.push('-g', `!${trimTrailingSlash(exclude)}`));
|
||||
}
|
||||
siblingClauses = rgGlobs.siblingClauses;
|
||||
|
||||
if (config.maxFilesize) {
|
||||
args.push('--max-filesize', config.maxFilesize + '');
|
||||
}
|
||||
|
||||
if (config.disregardIgnoreFiles) {
|
||||
// Don't use .gitignore or .ignore
|
||||
args.push('--no-ignore');
|
||||
}
|
||||
|
||||
// Follow symlinks
|
||||
args.push('--follow');
|
||||
|
||||
// Set default encoding if only one folder is opened
|
||||
if (config.folderQueries.length === 1 && config.folderQueries[0].fileEncoding && config.folderQueries[0].fileEncoding !== 'utf8') {
|
||||
args.push('--encoding', encoding.toCanonicalName(config.folderQueries[0].fileEncoding));
|
||||
}
|
||||
|
||||
// Ripgrep handles -- as a -- arg separator. Only --.
|
||||
// - is ok, --- is ok, --some-flag is handled as query text. Need to special case.
|
||||
if (config.contentPattern.pattern === '--') {
|
||||
config.contentPattern.isRegExp = true;
|
||||
config.contentPattern.pattern = '\\-\\-';
|
||||
}
|
||||
|
||||
let searchPatternAfterDoubleDashes: string;
|
||||
if (config.contentPattern.isWordMatch) {
|
||||
const regexp = strings.createRegExp(config.contentPattern.pattern, config.contentPattern.isRegExp, { wholeWord: config.contentPattern.isWordMatch });
|
||||
const regexpStr = regexp.source.replace(/\\\//g, '/'); // RegExp.source arbitrarily returns escaped slashes. Search and destroy.
|
||||
args.push('--regexp', regexpStr);
|
||||
} else if (config.contentPattern.isRegExp) {
|
||||
args.push('--regexp', config.contentPattern.pattern);
|
||||
} else {
|
||||
searchPatternAfterDoubleDashes = config.contentPattern.pattern;
|
||||
args.push('--fixed-strings');
|
||||
}
|
||||
|
||||
// Folder to search
|
||||
args.push('--');
|
||||
|
||||
if (searchPatternAfterDoubleDashes) {
|
||||
// Put the query after --, in case the query starts with a dash
|
||||
args.push(searchPatternAfterDoubleDashes);
|
||||
}
|
||||
|
||||
args.push(...config.folderQueries.map(q => q.folder));
|
||||
args.push(...config.extraFiles);
|
||||
|
||||
return { globArgs: args, siblingClauses };
|
||||
}
|
||||
|
||||
function getSiblings(file: string): TPromise<string[]> {
|
||||
return new TPromise<string[]>((resolve, reject) => {
|
||||
extfs.readdir(path.dirname(file), (error: Error, files: string[]) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
}
|
||||
|
||||
resolve(files);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function findUniversalExcludes(folderQueries: IFolderSearch[]): Set<string> {
|
||||
if (folderQueries.length < 2) {
|
||||
// Nothing to simplify
|
||||
return null;
|
||||
}
|
||||
|
||||
const firstFolder = folderQueries[0];
|
||||
if (!firstFolder.excludePattern) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const universalExcludes = new Set<string>();
|
||||
Object.keys(firstFolder.excludePattern).forEach(key => {
|
||||
if (strings.startsWith(key, '**') && folderQueries.every(q => q.excludePattern && q.excludePattern[key] === true)) {
|
||||
universalExcludes.add(key);
|
||||
}
|
||||
});
|
||||
|
||||
return universalExcludes;
|
||||
}
|
||||
65
src/vs/workbench/services/search/node/search.ts
Normal file
65
src/vs/workbench/services/search/node/search.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { PPromise, TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IExpression } from 'vs/base/common/glob';
|
||||
import { IProgress, ILineMatch, IPatternInfo, ISearchStats, ISearchLog } from 'vs/platform/search/common/search';
|
||||
|
||||
export interface IFolderSearch {
|
||||
folder: string;
|
||||
excludePattern?: IExpression;
|
||||
includePattern?: IExpression;
|
||||
fileEncoding?: string;
|
||||
}
|
||||
|
||||
export interface IRawSearch {
|
||||
folderQueries: IFolderSearch[];
|
||||
extraFiles?: string[];
|
||||
filePattern?: string;
|
||||
excludePattern?: IExpression;
|
||||
includePattern?: IExpression;
|
||||
contentPattern?: IPatternInfo;
|
||||
maxResults?: number;
|
||||
sortByScore?: boolean;
|
||||
cacheKey?: string;
|
||||
maxFilesize?: number;
|
||||
useRipgrep?: boolean;
|
||||
disregardIgnoreFiles?: boolean;
|
||||
}
|
||||
|
||||
export interface IRawSearchService {
|
||||
fileSearch(search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
|
||||
textSearch(search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
|
||||
clearCache(cacheKey: string): TPromise<void>;
|
||||
}
|
||||
|
||||
export interface IRawFileMatch {
|
||||
base?: string;
|
||||
relativePath: string;
|
||||
basename: string;
|
||||
size?: number;
|
||||
}
|
||||
|
||||
export interface ISearchEngine<T> {
|
||||
search: (onResult: (matches: T) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => void) => void;
|
||||
cancel: () => void;
|
||||
}
|
||||
|
||||
export interface ISerializedSearchComplete {
|
||||
limitHit: boolean;
|
||||
stats: ISearchStats;
|
||||
}
|
||||
|
||||
export interface ISerializedFileMatch {
|
||||
path: string;
|
||||
lineMatches?: ILineMatch[];
|
||||
numMatches?: number;
|
||||
}
|
||||
|
||||
// Type of the possible values for progress calls from the engine
|
||||
export type ISerializedSearchProgressItem = ISerializedFileMatch | ISerializedFileMatch[] | IProgress | ISearchLog;
|
||||
export type IFileSearchProgressItem = IRawFileMatch | IRawFileMatch[] | IProgress;
|
||||
15
src/vs/workbench/services/search/node/searchApp.ts
Normal file
15
src/vs/workbench/services/search/node/searchApp.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { SearchChannel } from './searchIpc';
|
||||
import { SearchService } from './rawSearchService';
|
||||
|
||||
const server = new Server();
|
||||
const service = new SearchService();
|
||||
const channel = new SearchChannel(service);
|
||||
server.registerChannel('search', channel);
|
||||
48
src/vs/workbench/services/search/node/searchIpc.ts
Normal file
48
src/vs/workbench/services/search/node/searchIpc.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { PPromise, TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { IRawSearchService, IRawSearch, ISerializedSearchComplete, ISerializedSearchProgressItem } from './search';
|
||||
|
||||
export interface ISearchChannel extends IChannel {
|
||||
call(command: 'fileSearch', search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
|
||||
call(command: 'textSearch', search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
|
||||
call(command: 'clearCache', cacheKey: string): TPromise<void>;
|
||||
call(command: string, arg: any): TPromise<any>;
|
||||
}
|
||||
|
||||
export class SearchChannel implements ISearchChannel {
|
||||
|
||||
constructor(private service: IRawSearchService) { }
|
||||
|
||||
call(command: string, arg: any): TPromise<any> {
|
||||
switch (command) {
|
||||
case 'fileSearch': return this.service.fileSearch(arg);
|
||||
case 'textSearch': return this.service.textSearch(arg);
|
||||
case 'clearCache': return this.service.clearCache(arg);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchChannelClient implements IRawSearchService {
|
||||
|
||||
constructor(private channel: ISearchChannel) { }
|
||||
|
||||
fileSearch(search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
|
||||
return this.channel.call('fileSearch', search);
|
||||
}
|
||||
|
||||
textSearch(search: IRawSearch): PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem> {
|
||||
return this.channel.call('textSearch', search);
|
||||
}
|
||||
|
||||
public clearCache(cacheKey: string): TPromise<void> {
|
||||
return this.channel.call('clearCache', cacheKey);
|
||||
}
|
||||
}
|
||||
321
src/vs/workbench/services/search/node/searchService.ts
Normal file
321
src/vs/workbench/services/search/node/searchService.ts
Normal file
@@ -0,0 +1,321 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
'use strict';
|
||||
|
||||
import { PPromise, TPromise } from 'vs/base/common/winjs.base';
|
||||
import uri from 'vs/base/common/uri';
|
||||
import objects = require('vs/base/common/objects');
|
||||
import scorer = require('vs/base/common/scorer');
|
||||
import strings = require('vs/base/common/strings');
|
||||
import { getNextTickChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { IProgress, LineMatch, FileMatch, ISearchComplete, ISearchProgressItem, QueryType, IFileMatch, ISearchQuery, ISearchConfiguration, ISearchService, pathIncludedInQuery, ISearchResultProvider } from 'vs/platform/search/common/search';
|
||||
import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService';
|
||||
import { IModelService } from 'vs/editor/common/services/modelService';
|
||||
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
|
||||
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
|
||||
import { IRawSearch, IFolderSearch, ISerializedSearchComplete, ISerializedSearchProgressItem, ISerializedFileMatch, IRawSearchService } from './search';
|
||||
import { ISearchChannel, SearchChannelClient } from './searchIpc';
|
||||
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
|
||||
import { ResourceMap } from 'vs/base/common/map';
|
||||
import { IDisposable } from 'vs/base/common/lifecycle';
|
||||
|
||||
export class SearchService implements ISearchService {
|
||||
public _serviceBrand: any;
|
||||
|
||||
private diskSearch: DiskSearch;
|
||||
private readonly searchProvider: ISearchResultProvider[] = [];
|
||||
|
||||
constructor(
|
||||
@IModelService private modelService: IModelService,
|
||||
@IUntitledEditorService private untitledEditorService: IUntitledEditorService,
|
||||
@IEnvironmentService environmentService: IEnvironmentService,
|
||||
@IWorkspaceContextService private contextService: IWorkspaceContextService,
|
||||
@IConfigurationService private configurationService: IConfigurationService
|
||||
) {
|
||||
this.diskSearch = new DiskSearch(!environmentService.isBuilt || environmentService.verbose);
|
||||
this.registerSearchResultProvider(this.diskSearch);
|
||||
}
|
||||
|
||||
public registerSearchResultProvider(provider: ISearchResultProvider): IDisposable {
|
||||
this.searchProvider.push(provider);
|
||||
return {
|
||||
dispose: () => {
|
||||
const idx = this.searchProvider.indexOf(provider);
|
||||
if (idx >= 0) {
|
||||
this.searchProvider.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public extendQuery(query: ISearchQuery): void {
|
||||
const configuration = this.configurationService.getConfiguration<ISearchConfiguration>();
|
||||
|
||||
// Configuration: Encoding
|
||||
if (!query.fileEncoding) {
|
||||
const fileEncoding = configuration && configuration.files && configuration.files.encoding;
|
||||
query.fileEncoding = fileEncoding;
|
||||
}
|
||||
|
||||
// Configuration: File Excludes
|
||||
if (!query.disregardExcludeSettings) {
|
||||
const fileExcludes = configuration && configuration.files && configuration.files.exclude;
|
||||
if (fileExcludes) {
|
||||
if (!query.excludePattern) {
|
||||
query.excludePattern = fileExcludes;
|
||||
} else {
|
||||
objects.mixin(query.excludePattern, fileExcludes, false /* no overwrite */);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public search(query: ISearchQuery): PPromise<ISearchComplete, ISearchProgressItem> {
|
||||
|
||||
let combinedPromise: TPromise<void>;
|
||||
|
||||
return new PPromise<ISearchComplete, ISearchProgressItem>((onComplete, onError, onProgress) => {
|
||||
|
||||
// Get local results from dirty/untitled
|
||||
const localResults = this.getLocalResults(query);
|
||||
|
||||
// Allow caller to register progress callback
|
||||
process.nextTick(() => localResults.values().filter((res) => !!res).forEach(onProgress));
|
||||
|
||||
const providerPromises = this.searchProvider.map(provider => TPromise.wrap(provider.search(query)).then(e => e,
|
||||
err => {
|
||||
// TODO@joh
|
||||
// single provider fail. fail all?
|
||||
onError(err);
|
||||
},
|
||||
progress => {
|
||||
if (progress.resource) {
|
||||
// Match
|
||||
if (!localResults.has(progress.resource)) { // don't override local results
|
||||
onProgress(progress);
|
||||
}
|
||||
} else {
|
||||
// Progress
|
||||
onProgress(<IProgress>progress);
|
||||
}
|
||||
}
|
||||
));
|
||||
|
||||
combinedPromise = TPromise.join(providerPromises).then(values => {
|
||||
|
||||
const result: ISearchComplete = {
|
||||
limitHit: false,
|
||||
results: [],
|
||||
stats: undefined
|
||||
};
|
||||
|
||||
// TODO@joh
|
||||
// sorting, disjunct results
|
||||
for (const value of values) {
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
// TODO@joh individual stats/limit
|
||||
result.stats = value.stats || result.stats;
|
||||
result.limitHit = value.limitHit || result.limitHit;
|
||||
|
||||
for (const match of value.results) {
|
||||
if (!localResults.has(match.resource)) {
|
||||
result.results.push(match);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
||||
}).then(onComplete, onError);
|
||||
|
||||
}, () => combinedPromise && combinedPromise.cancel());
|
||||
}
|
||||
|
||||
private getLocalResults(query: ISearchQuery): ResourceMap<IFileMatch> {
|
||||
const localResults = new ResourceMap<IFileMatch>();
|
||||
|
||||
if (query.type === QueryType.Text) {
|
||||
let models = this.modelService.getModels();
|
||||
models.forEach((model) => {
|
||||
let resource = model.uri;
|
||||
if (!resource) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Support untitled files
|
||||
if (resource.scheme === 'untitled') {
|
||||
if (!this.untitledEditorService.exists(resource)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Don't support other resource schemes than files for now
|
||||
else if (resource.scheme !== 'file') {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.matches(resource, query)) {
|
||||
return; // respect user filters
|
||||
}
|
||||
|
||||
// Use editor API to find matches
|
||||
let matches = model.findMatches(query.contentPattern.pattern, false, query.contentPattern.isRegExp, query.contentPattern.isCaseSensitive, query.contentPattern.isWordMatch ? query.contentPattern.wordSeparators : null, false, query.maxResults);
|
||||
if (matches.length) {
|
||||
let fileMatch = new FileMatch(resource);
|
||||
localResults.set(resource, fileMatch);
|
||||
|
||||
matches.forEach((match) => {
|
||||
fileMatch.lineMatches.push(new LineMatch(model.getLineContent(match.range.startLineNumber), match.range.startLineNumber - 1, [[match.range.startColumn - 1, match.range.endColumn - match.range.startColumn]]));
|
||||
});
|
||||
} else {
|
||||
localResults.set(resource, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return localResults;
|
||||
}
|
||||
|
||||
private matches(resource: uri, query: ISearchQuery): boolean {
|
||||
// file pattern
|
||||
if (query.filePattern) {
|
||||
if (resource.scheme !== 'file') {
|
||||
return false; // if we match on file pattern, we have to ignore non file resources
|
||||
}
|
||||
|
||||
if (!scorer.matches(resource.fsPath, strings.stripWildcards(query.filePattern).toLowerCase())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// includes
|
||||
if (query.includePattern) {
|
||||
if (resource.scheme !== 'file') {
|
||||
return false; // if we match on file patterns, we have to ignore non file resources
|
||||
}
|
||||
}
|
||||
|
||||
return pathIncludedInQuery(query, resource.fsPath);
|
||||
}
|
||||
|
||||
public clearCache(cacheKey: string): TPromise<void> {
|
||||
return this.diskSearch.clearCache(cacheKey);
|
||||
}
|
||||
}
|
||||
|
||||
export class DiskSearch implements ISearchResultProvider {
|
||||
|
||||
private raw: IRawSearchService;
|
||||
|
||||
constructor(verboseLogging: boolean, timeout: number = 60 * 60 * 1000) {
|
||||
const client = new Client(
|
||||
uri.parse(require.toUrl('bootstrap')).fsPath,
|
||||
{
|
||||
serverName: 'Search',
|
||||
timeout: timeout,
|
||||
args: ['--type=searchService'],
|
||||
// See https://github.com/Microsoft/vscode/issues/27665
|
||||
// Pass in fresh execArgv to the forked process such that it doesn't inherit them from `process.execArgv`.
|
||||
// e.g. Launching the extension host process with `--inspect-brk=xxx` and then forking a process from the extension host
|
||||
// results in the forked process inheriting `--inspect-brk=xxx`.
|
||||
freshExecArgv: true,
|
||||
env: {
|
||||
AMD_ENTRYPOINT: 'vs/workbench/services/search/node/searchApp',
|
||||
PIPE_LOGGING: 'true',
|
||||
VERBOSE_LOGGING: verboseLogging
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const channel = getNextTickChannel(client.getChannel<ISearchChannel>('search'));
|
||||
this.raw = new SearchChannelClient(channel);
|
||||
}
|
||||
|
||||
public search(query: ISearchQuery): PPromise<ISearchComplete, ISearchProgressItem> {
|
||||
let request: PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>;
|
||||
|
||||
let rawSearch: IRawSearch = {
|
||||
folderQueries: query.folderQueries ? query.folderQueries.map(q => {
|
||||
return <IFolderSearch>{
|
||||
excludePattern: q.excludePattern,
|
||||
includePattern: q.includePattern,
|
||||
fileEncoding: q.fileEncoding,
|
||||
folder: q.folder.fsPath
|
||||
};
|
||||
}) : [],
|
||||
extraFiles: query.extraFileResources ? query.extraFileResources.map(r => r.fsPath) : [],
|
||||
filePattern: query.filePattern,
|
||||
excludePattern: query.excludePattern,
|
||||
includePattern: query.includePattern,
|
||||
maxResults: query.maxResults,
|
||||
sortByScore: query.sortByScore,
|
||||
cacheKey: query.cacheKey,
|
||||
useRipgrep: query.useRipgrep,
|
||||
disregardIgnoreFiles: query.disregardIgnoreFiles
|
||||
};
|
||||
|
||||
if (query.type === QueryType.Text) {
|
||||
rawSearch.contentPattern = query.contentPattern;
|
||||
}
|
||||
|
||||
if (query.type === QueryType.File) {
|
||||
request = this.raw.fileSearch(rawSearch);
|
||||
} else {
|
||||
request = this.raw.textSearch(rawSearch);
|
||||
}
|
||||
|
||||
return DiskSearch.collectResults(request);
|
||||
}
|
||||
|
||||
public static collectResults(request: PPromise<ISerializedSearchComplete, ISerializedSearchProgressItem>): PPromise<ISearchComplete, ISearchProgressItem> {
|
||||
let result: IFileMatch[] = [];
|
||||
return new PPromise<ISearchComplete, ISearchProgressItem>((c, e, p) => {
|
||||
request.done((complete) => {
|
||||
c({
|
||||
limitHit: complete.limitHit,
|
||||
results: result,
|
||||
stats: complete.stats
|
||||
});
|
||||
}, e, (data) => {
|
||||
|
||||
// Matches
|
||||
if (Array.isArray(data)) {
|
||||
const fileMatches = data.map(d => this.createFileMatch(d));
|
||||
result = result.concat(fileMatches);
|
||||
fileMatches.forEach(p);
|
||||
}
|
||||
|
||||
// Match
|
||||
else if ((<ISerializedFileMatch>data).path) {
|
||||
const fileMatch = this.createFileMatch(<ISerializedFileMatch>data);
|
||||
result.push(fileMatch);
|
||||
p(fileMatch);
|
||||
}
|
||||
|
||||
// Progress
|
||||
else {
|
||||
p(<IProgress>data);
|
||||
}
|
||||
});
|
||||
}, () => request.cancel());
|
||||
}
|
||||
|
||||
private static createFileMatch(data: ISerializedFileMatch): FileMatch {
|
||||
let fileMatch = new FileMatch(uri.file(data.path));
|
||||
if (data.lineMatches) {
|
||||
for (let j = 0; j < data.lineMatches.length; j++) {
|
||||
fileMatch.lineMatches.push(new LineMatch(data.lineMatches[j].preview, data.lineMatches[j].lineNumber, data.lineMatches[j].offsetAndLengths));
|
||||
}
|
||||
}
|
||||
return fileMatch;
|
||||
}
|
||||
|
||||
public clearCache(cacheKey: string): TPromise<void> {
|
||||
return this.raw.clearCache(cacheKey);
|
||||
}
|
||||
}
|
||||
166
src/vs/workbench/services/search/node/textSearch.ts
Normal file
166
src/vs/workbench/services/search/node/textSearch.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as path from 'path';
|
||||
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import { IProgress } from 'vs/platform/search/common/search';
|
||||
import { FileWalker } from 'vs/workbench/services/search/node/fileSearch';
|
||||
|
||||
import { ISerializedFileMatch, ISerializedSearchComplete, IRawSearch, ISearchEngine } from './search';
|
||||
import { ISearchWorker } from './worker/searchWorkerIpc';
|
||||
import { ITextSearchWorkerProvider } from './textSearchWorkerProvider';
|
||||
|
||||
export class Engine implements ISearchEngine<ISerializedFileMatch[]> {
|
||||
|
||||
private static PROGRESS_FLUSH_CHUNK_SIZE = 50; // optimization: number of files to process before emitting progress event
|
||||
|
||||
private config: IRawSearch;
|
||||
private walker: FileWalker;
|
||||
private walkerError: Error;
|
||||
|
||||
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: IRawSearch, walker: FileWalker, workerProvider: ITextSearchWorkerProvider) {
|
||||
this.config = config;
|
||||
this.walker = walker;
|
||||
this.workerProvider = workerProvider;
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.isCanceled = true;
|
||||
this.walker.cancel();
|
||||
|
||||
this.workers.forEach(w => {
|
||||
w.cancel()
|
||||
.then(null, onUnexpectedError);
|
||||
});
|
||||
}
|
||||
|
||||
initializeWorkers(): void {
|
||||
this.workers.forEach(w => {
|
||||
w.initialize()
|
||||
.then(null, onUnexpectedError);
|
||||
});
|
||||
}
|
||||
|
||||
search(onResult: (match: ISerializedFileMatch[]) => void, onProgress: (progress: IProgress) => void, done: (error: Error, complete: ISerializedSearchComplete) => 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 = { absolutePaths: batch, maxResults, pattern: this.config.contentPattern, fileEncoding };
|
||||
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.config.folderQueries, this.config.extraFiles, 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;
|
||||
}
|
||||
}, (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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as os from 'os';
|
||||
|
||||
import uri from 'vs/base/common/uri';
|
||||
import * as ipc from 'vs/base/parts/ipc/common/ipc';
|
||||
import { Client } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
|
||||
import { ISearchWorker, ISearchWorkerChannel, SearchWorkerChannelClient } from './worker/searchWorkerIpc';
|
||||
|
||||
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(
|
||||
uri.parse(require.toUrl('bootstrap')).fsPath,
|
||||
{
|
||||
serverName: 'Search Worker ' + this.workers.length,
|
||||
args: ['--type=searchWorker'],
|
||||
timeout: 30 * 1000,
|
||||
env: {
|
||||
AMD_ENTRYPOINT: 'vs/workbench/services/search/node/worker/searchWorkerApp',
|
||||
PIPE_LOGGING: 'true',
|
||||
VERBOSE_LOGGING: process.env.VERBOSE_LOGGING
|
||||
},
|
||||
useQueue: true
|
||||
});
|
||||
|
||||
const channel = ipc.getNextTickChannel(client.getChannel<ISearchWorkerChannel>('searchWorker'));
|
||||
const channelClient = new SearchWorkerChannelClient(channel);
|
||||
|
||||
this.workers.push(channelClient);
|
||||
}
|
||||
}
|
||||
369
src/vs/workbench/services/search/node/worker/searchWorker.ts
Normal file
369
src/vs/workbench/services/search/node/worker/searchWorker.ts
Normal file
@@ -0,0 +1,369 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import * as fs from 'fs';
|
||||
import gracefulFs = require('graceful-fs');
|
||||
gracefulFs.gracefulify(fs);
|
||||
|
||||
import { onUnexpectedError } from 'vs/base/common/errors';
|
||||
import * as strings from 'vs/base/common/strings';
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { ISerializedFileMatch } from '../search';
|
||||
import * as baseMime from 'vs/base/common/mime';
|
||||
import { ILineMatch } from 'vs/platform/search/common/search';
|
||||
import { UTF16le, UTF16be, UTF8, UTF8_with_bom, encodingExists, decode, bomLength } from 'vs/base/node/encoding';
|
||||
import { detectMimeAndEncodingFromBuffer } from 'vs/base/node/mime';
|
||||
|
||||
import { ISearchWorker, ISearchWorkerSearchArgs, ISearchWorkerSearchResult } from './searchWorkerIpc';
|
||||
|
||||
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(): TPromise<void> {
|
||||
this.currentSearchEngine = new SearchWorkerEngine();
|
||||
return TPromise.wrap<void>(undefined);
|
||||
}
|
||||
|
||||
cancel(): TPromise<void> {
|
||||
// Cancel the current search. It will stop searching and close its open files.
|
||||
if (this.currentSearchEngine) {
|
||||
this.currentSearchEngine.cancel();
|
||||
}
|
||||
|
||||
return TPromise.wrap<void>(null);
|
||||
}
|
||||
|
||||
search(args: ISearchWorkerSearchArgs): TPromise<ISearchWorkerSearchResult> {
|
||||
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 = TPromise.wrap(null);
|
||||
private isCanceled = false;
|
||||
|
||||
/**
|
||||
* Searches some number of the given paths concurrently, and starts searches in other paths when those complete.
|
||||
*/
|
||||
searchBatch(args: ISearchWorkerSearchArgs): TPromise<ISearchWorkerSearchResult> {
|
||||
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): TPromise<ISearchWorkerSearchResult> {
|
||||
if (this.isCanceled) {
|
||||
return TPromise.wrap<ISearchWorkerSearchResult>(null);
|
||||
}
|
||||
|
||||
return new TPromise<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): TPromise<void> => {
|
||||
return this.searchInFile(absolutePath, contentPattern, fileEncoding, args.maxResults && (args.maxResults - result.numMatches)).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);
|
||||
};
|
||||
|
||||
TPromise.join(args.absolutePaths.map(startSearchInFile)).then(() => {
|
||||
batchDone(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.isCanceled = true;
|
||||
}
|
||||
|
||||
private searchInFile(absolutePath: string, contentPattern: RegExp, fileEncoding: string, maxResults?: number): TPromise<IFileSearchResult> {
|
||||
let fileMatch: FileMatch = null;
|
||||
let limitReached = false;
|
||||
let numMatches = 0;
|
||||
|
||||
const perLineCallback = (line: string, lineNumber: number) => {
|
||||
let lineMatch: LineMatch = null;
|
||||
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);
|
||||
}
|
||||
|
||||
if (lineMatch === null) {
|
||||
lineMatch = new LineMatch(line, lineNumber);
|
||||
fileMatch.addMatch(lineMatch);
|
||||
}
|
||||
|
||||
lineMatch.addMatch(match.index, match[0].length);
|
||||
|
||||
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): TPromise<void> {
|
||||
return new TPromise<void>((resolve, reject) => {
|
||||
fs.open(filename, 'r', null, (error: Error, fd: number) => {
|
||||
if (error) {
|
||||
return resolve(null);
|
||||
}
|
||||
|
||||
const buffer = new Buffer(options.bufferLength);
|
||||
let line = '';
|
||||
let lineNumber = 0;
|
||||
let lastBufferHadTrailingCR = false;
|
||||
|
||||
const readFile = (isFirstRead: boolean, clb: (error: Error) => 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: NodeBuffer) => {
|
||||
const decodeBuffer = (buffer: NodeBuffer, 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 mimeAndEncoding = detectMimeAndEncodingFromBuffer({ buffer, bytesRead }, false);
|
||||
if (mimeAndEncoding.mimes[mimeAndEncoding.mimes.length - 1] !== baseMime.MIME_TEXT) {
|
||||
return clb(null); // skip files that seem binary
|
||||
}
|
||||
|
||||
// Check for BOM offset
|
||||
switch (mimeAndEncoding.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(null);
|
||||
}
|
||||
|
||||
if (line.length) {
|
||||
perLineCallback(line, lineNumber); // handle last line
|
||||
}
|
||||
|
||||
fs.close(fd, (error: Error) => {
|
||||
resolve(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class FileMatch implements ISerializedFileMatch {
|
||||
path: string;
|
||||
lineMatches: LineMatch[];
|
||||
|
||||
constructor(path: string) {
|
||||
this.path = path;
|
||||
this.lineMatches = [];
|
||||
}
|
||||
|
||||
addMatch(lineMatch: LineMatch): void {
|
||||
this.lineMatches.push(lineMatch);
|
||||
}
|
||||
|
||||
isEmpty(): boolean {
|
||||
return this.lineMatches.length === 0;
|
||||
}
|
||||
|
||||
serialize(): ISerializedFileMatch {
|
||||
let lineMatches: ILineMatch[] = [];
|
||||
let numMatches = 0;
|
||||
|
||||
for (let i = 0; i < this.lineMatches.length; i++) {
|
||||
numMatches += this.lineMatches[i].offsetAndLengths.length;
|
||||
lineMatches.push(this.lineMatches[i].serialize());
|
||||
}
|
||||
|
||||
return {
|
||||
path: this.path,
|
||||
lineMatches,
|
||||
numMatches
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export class LineMatch implements ILineMatch {
|
||||
preview: string;
|
||||
lineNumber: number;
|
||||
offsetAndLengths: number[][];
|
||||
|
||||
constructor(preview: string, lineNumber: number) {
|
||||
this.preview = preview.replace(/(\r|\n)*$/, '');
|
||||
this.lineNumber = lineNumber;
|
||||
this.offsetAndLengths = [];
|
||||
}
|
||||
|
||||
getText(): string {
|
||||
return this.preview;
|
||||
}
|
||||
|
||||
getLineNumber(): number {
|
||||
return this.lineNumber;
|
||||
}
|
||||
|
||||
addMatch(offset: number, length: number): void {
|
||||
this.offsetAndLengths.push([offset, length]);
|
||||
}
|
||||
|
||||
serialize(): ILineMatch {
|
||||
const result = {
|
||||
preview: this.preview,
|
||||
lineNumber: this.lineNumber,
|
||||
offsetAndLengths: this.offsetAndLengths
|
||||
};
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { Server } from 'vs/base/parts/ipc/node/ipc.cp';
|
||||
import { SearchWorkerChannel } from './searchWorkerIpc';
|
||||
import { SearchWorker } from './searchWorker';
|
||||
|
||||
const server = new Server();
|
||||
const worker = new SearchWorker();
|
||||
const channel = new SearchWorkerChannel(worker);
|
||||
server.registerChannel('searchWorker', channel);
|
||||
@@ -0,0 +1,68 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
'use strict';
|
||||
|
||||
import { TPromise } from 'vs/base/common/winjs.base';
|
||||
import { IChannel } from 'vs/base/parts/ipc/common/ipc';
|
||||
import { ISerializedFileMatch } from '../search';
|
||||
import { IPatternInfo } from 'vs/platform/search/common/search';
|
||||
import { SearchWorker } from './searchWorker';
|
||||
|
||||
export interface ISearchWorkerSearchArgs {
|
||||
pattern: IPatternInfo;
|
||||
fileEncoding: string;
|
||||
absolutePaths: string[];
|
||||
maxResults?: number;
|
||||
}
|
||||
|
||||
export interface ISearchWorkerSearchResult {
|
||||
matches: ISerializedFileMatch[];
|
||||
numMatches: number;
|
||||
limitReached: boolean;
|
||||
}
|
||||
|
||||
export interface ISearchWorker {
|
||||
initialize(): TPromise<void>;
|
||||
search(args: ISearchWorkerSearchArgs): TPromise<ISearchWorkerSearchResult>;
|
||||
cancel(): TPromise<void>;
|
||||
}
|
||||
|
||||
export interface ISearchWorkerChannel extends IChannel {
|
||||
call(command: 'initialize'): TPromise<void>;
|
||||
call(command: 'search', args: ISearchWorkerSearchArgs): TPromise<ISearchWorkerSearchResult>;
|
||||
call(command: 'cancel'): TPromise<void>;
|
||||
call(command: string, arg?: any): TPromise<any>;
|
||||
}
|
||||
|
||||
export class SearchWorkerChannel implements ISearchWorkerChannel {
|
||||
constructor(private worker: SearchWorker) {
|
||||
}
|
||||
|
||||
call(command: string, arg?: any): TPromise<any> {
|
||||
switch (command) {
|
||||
case 'initialize': return this.worker.initialize();
|
||||
case 'search': return this.worker.search(arg);
|
||||
case 'cancel': return this.worker.cancel();
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export class SearchWorkerChannelClient implements ISearchWorker {
|
||||
constructor(private channel: ISearchWorkerChannel) { }
|
||||
|
||||
initialize(): TPromise<void> {
|
||||
return this.channel.call('initialize');
|
||||
}
|
||||
|
||||
search(args: ISearchWorkerSearchArgs): TPromise<ISearchWorkerSearchResult> {
|
||||
return this.channel.call('search', args);
|
||||
}
|
||||
|
||||
cancel(): TPromise<void> {
|
||||
return this.channel.call('cancel');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user