Add TextSearchProvider (+ improve FileSearchProvider)

feature/search
Kelvin Schoofs 6 years ago
parent f6666e26b1
commit 736e4152d2

@ -0,0 +1,34 @@
type Task = () => any;
export class ConcurrencyLimiter {
protected tasks: Task[] = [];
protected workers: Promise<void>[] = [];
constructor(protected maxWorkers = 10) { }
public addTask(task: Task) {
this.tasks.push(task);
if (this.workers.length < this.maxWorkers) {
this.createWorker();
}
}
public clear() {
this.tasks = [];
}
public toPromise() {
if (!this.workers.length) return Promise.resolve();
return Promise.all(this.workers).then(() => this.toPromise());
}
protected async createWorker() {
const worker = this.taskRunner();
this.workers.push(worker);
worker.catch(() => { }).then(() => this.workers.splice(this.workers.indexOf(worker)));
}
protected async taskRunner() {
let task: Task | undefined = this.tasks.pop();
while (task) {
try {
await task();
} catch (e) { }
task = this.tasks.pop();
}
}
}

@ -53,7 +53,9 @@ export function activate(context: vscode.ExtensionContext) {
subscribe(vscode.commands.registerCommand(command, callback, thisArg)); subscribe(vscode.commands.registerCommand(command, callback, thisArg));
subscribe(vscode.workspace.registerFileSystemProvider('ssh', manager, { isCaseSensitive: true })); subscribe(vscode.workspace.registerFileSystemProvider('ssh', manager, { isCaseSensitive: true }));
subscribe(vscode.workspace.registerFileSearchProvider('ssh', new SearchProvider(manager))); const searchProvider = new SearchProvider(manager);
subscribe(vscode.workspace.registerFileSearchProvider('ssh', searchProvider));
subscribe(vscode.workspace.registerTextSearchProvider('ssh', searchProvider));
async function pickAndClick(func: (name: string) => void, name?: string, activeOrNot?: boolean) { async function pickAndClick(func: (name: string) => void, name?: string, activeOrNot?: boolean) {
name = name || await pickConfig(manager, activeOrNot); name = name || await pickConfig(manager, activeOrNot);

@ -2,9 +2,66 @@
import * as minimatch from 'minimatch'; import * as minimatch from 'minimatch';
import { posix as path } from 'path'; import { posix as path } from 'path';
import * as vscode from 'vscode'; import * as vscode from 'vscode';
import { ConcurrencyLimiter } from './concurrencyLimiter';
import { Manager } from './manager'; import { Manager } from './manager';
export class SearchProvider implements vscode.FileSearchProvider { function textSearchQueryToRegExp(query: vscode.TextSearchQuery): RegExp {
const flags = `${query.isCaseSensitive ? '' : 'i'}mug`;
let pattern = query.pattern;
if (query.isRegExp) {
if (query.isWordMatch) pattern = `\\b${query.pattern}\\b`;
return new RegExp(pattern, flags);
}
pattern = pattern.replace(/\\/g, '\\\\');
if (query.isWordMatch) pattern = `\\b${query.pattern}\\b`;
return new RegExp(pattern, flags);
}
type RegExpMatchHandler = (lastIndex: number, mach: RegExpExecArray) => true | null | undefined | void;
function forEachMatch(content: string, regex: RegExp, handler: RegExpMatchHandler) {
let mat = regex.exec(content);
while (mat) {
const { lastIndex } = regex;
if (handler(lastIndex, mat)) break;
mat = regex.exec(content);
}
}
type SimpleRange = [number, number];
function getSearchResultRanges(content: string, query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, regex: RegExp): SimpleRange[] {
const results: SimpleRange[] = [];
if (query.isMultiline) {
forEachMatch(content, /[^\r\n]+/g, (index, mat) => {
const res = getSearchResultRanges(mat[0], query, options, regex);
res.forEach(range => (range[0] += index, range[1] += index));
results.push(...res);
});
return results;
}
forEachMatch(content, regex, (index, mat) => {
results.push([index - mat[0].length, index]);
});
return results;
}
function getSearchResults(content: string, query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, regexp: RegExp): vscode.Range[] {
const indexes = getSearchResultRanges(content, query, options, regexp);
const indexForLine: number[] = [0];
for (let i = 0; i < content.length; i += 1) {
if (content[i] === '\n') {
indexForLine.push(i + 1);
}
}
return indexes.map((range) => {
const startLine = indexForLine.findIndex(v => v >= range[0]) - 1;
const endLine = indexForLine.findIndex(v => v >= range[1]) - 1;
const startChar = range[0] - indexForLine[startLine];
const endChar = range[1] - indexForLine[endLine];
return new vscode.Range(startLine, startChar, endLine, endChar);
});
}
export class SearchProvider implements vscode.FileSearchProvider, vscode.TextSearchProvider {
protected cache: [vscode.CancellationToken, Promise<vscode.Uri[]>][] = []; protected cache: [vscode.CancellationToken, Promise<vscode.Uri[]>][] = [];
constructor(protected manager: Manager) { } constructor(protected manager: Manager) { }
public async provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<vscode.Uri[]> { public async provideFileSearchResults(query: vscode.FileSearchQuery, options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<vscode.Uri[]> {
@ -12,12 +69,43 @@ export class SearchProvider implements vscode.FileSearchProvider {
const paths = await this.getTree(options, session || token, !session); const paths = await this.getTree(options, session || token, !session);
if (token.isCancellationRequested) return []; if (token.isCancellationRequested) return [];
const pattern = query.pattern.toLowerCase(); const pattern = query.pattern.toLowerCase();
return paths.map<vscode.Uri | null>((relative) => { return paths.map<vscode.Uri | false>((relative) => {
if (!relative.path.toLowerCase().includes(pattern)) return null; return relative.path.toLowerCase().includes(pattern) && relative;
return folder.with({ path: path.join(folder.path, relative.path) });
}).filter(s => !!s) as vscode.Uri[]; }).filter(s => !!s) as vscode.Uri[];
} }
protected async getTree(options: vscode.FileSearchOptions, session: vscode.CancellationToken, singleton = false): Promise<vscode.Uri[]> { public async provideTextSearchResults(query: vscode.TextSearchQuery, options: vscode.TextSearchOptions, progress: vscode.Progress<vscode.TextSearchResult>, token: vscode.CancellationToken): Promise<vscode.TextSearchComplete> {
const paths = await this.getTree(options, token, true);
const regexp = textSearchQueryToRegExp(query);
const limiter = new ConcurrencyLimiter(20);
token.onCancellationRequested(limiter.clear, limiter);
let found = 0;
const fs = (await this.manager.getFs(options.folder))!;
async function handleFile(uri: vscode.Uri) {
if (token.isCancellationRequested) return;
if (options.maxFileSize) {
const stats = await fs.stat(uri);
if (stats.size > options.maxFileSize) return;
}
const buffer = Buffer.from(await fs.readFile(uri));
const content = Buffer.from(buffer.toString(), options.encoding);
const ranges = getSearchResults(content.toString(), query, options, regexp);
found += getSearchResults.length;
progress.report({
ranges, uri,
preview: {
matches: ranges,
text: content.toString(),
},
} as vscode.TextSearchMatch);
if (found >= options.maxResults) limiter.clear();
}
for (const filepath of paths) {
limiter.addTask(() => handleFile(filepath));
}
await limiter.toPromise();
return { limitHit: found > options.maxResults };
}
protected async getTree(options: vscode.SearchOptions, session: vscode.CancellationToken, singleton = false): Promise<vscode.Uri[]> {
let cached = this.cache.find(([t]) => session === t); let cached = this.cache.find(([t]) => session === t);
if (cached) return await cached[1]; if (cached) return await cached[1];
const singletonSource = singleton && new vscode.CancellationTokenSource(); const singletonSource = singleton && new vscode.CancellationTokenSource();
@ -36,6 +124,7 @@ export class SearchProvider implements vscode.FileSearchProvider {
return res; return res;
} }
protected async internal_buildTree(options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<vscode.Uri[]> { protected async internal_buildTree(options: vscode.FileSearchOptions, token: vscode.CancellationToken): Promise<vscode.Uri[]> {
// TODO: For the options, actually use the following: includes, useIgnoreFiles and useGlobalIgnoreFiles
const { folder } = options; const { folder } = options;
const fs = await this.manager.getFs(folder); const fs = await this.manager.getFs(folder);
if (!fs || token.isCancellationRequested) return []; if (!fs || token.isCancellationRequested) return [];
@ -52,6 +141,8 @@ export class SearchProvider implements vscode.FileSearchProvider {
if (exclude(joined)) return; if (exclude(joined)) return;
// tslint:disable-next-line:no-bitwise // tslint:disable-next-line:no-bitwise
if (type & vscode.FileType.Directory) { if (type & vscode.FileType.Directory) {
// tslint:disable-next-line:no-bitwise
if ((type & vscode.FileType.SymbolicLink) && !options.followSymlinks) return;
return readDirectory(uri.with({ path: joined })); return readDirectory(uri.with({ path: joined }));
} else { } else {
res.push(uri.with({ path: joined })); res.push(uri.with({ path: joined }));
@ -59,6 +150,11 @@ export class SearchProvider implements vscode.FileSearchProvider {
})); }));
} }
await readDirectory(folder); await readDirectory(folder);
if (options.includes.length) {
const includes = options.includes.map(e => minimatch.makeRe(e, { nocase: true }));
const include = (p: string) => includes.some(reg => reg.test(p));
return res.filter(uri => include(uri.path) || uri.path[0] === '/' && include(uri.path.slice(1)));
}
return res; return res;
} }
} }

Loading…
Cancel
Save