core/packages/file-search/src/node/file-search.service.ts

214 lines
6.4 KiB
TypeScript

import readline from 'readline';
import fuzzy from 'fuzzy';
import { Autowired, Injectable } from '@opensumi/di';
import { CancellationToken, CancellationTokenSource, path } from '@opensumi/ide-core-common';
import { INodeLogger } from '@opensumi/ide-core-node';
import { IProcessFactory } from '@opensumi/ide-process';
import { rgPath } from '@opensumi/vscode-ripgrep';
import { IFileSearchService } from '../common';
const { replaceAsarInPath, Path, dirname } = path;
@Injectable()
export class FileSearchService implements IFileSearchService {
@Autowired(IProcessFactory)
processFactory: IProcessFactory;
@Autowired(INodeLogger)
logger: INodeLogger;
private isAbsolutePathPattern(pattern: string): boolean {
return path.isAbsolute(pattern) || pattern.startsWith('/') || pattern.startsWith('\\');
}
// 这里应该返回文件的 `fsPath` 而非 `file://` 协议文件路径
// 否则在 Windows 下,盘符路径会被隐藏
async find(
searchPattern: string,
options: IFileSearchService.Options,
clientToken?: CancellationToken,
): Promise<string[]> {
const cancellationSource = new CancellationTokenSource();
if (clientToken) {
clientToken.onCancellationRequested(() => cancellationSource.cancel());
}
const token = cancellationSource.token;
const opts = {
fuzzyMatch: true,
limit: Number.MAX_SAFE_INTEGER,
useGitIgnore: true,
onlyFolders: false,
...options,
};
const roots: IFileSearchService.RootOptions = options.rootOptions || {};
let stringPattern = searchPattern.toLocaleLowerCase();
// 如果传入绝对路径,则将父级目录作为根目录
if (this.isAbsolutePathPattern(searchPattern)) {
const parent = path.dirname(searchPattern);
roots[parent] = {};
stringPattern = path.basename(searchPattern).toLocaleLowerCase();
}
if (options.rootUris) {
for (const rootUri of options.rootUris) {
if (!roots[rootUri]) {
roots[rootUri] = {};
}
}
}
// eslint-disable-next-line guard-for-in
for (const rootUri in roots) {
const rootOptions = roots[rootUri];
if (opts.includePatterns) {
const includePatterns = rootOptions.includePatterns || [];
rootOptions.includePatterns = [...includePatterns, ...opts.includePatterns];
}
if (opts.excludePatterns) {
const excludePatterns = rootOptions.excludePatterns || [];
rootOptions.excludePatterns = [...excludePatterns, ...opts.excludePatterns];
}
if (rootOptions.useGitIgnore === undefined) {
rootOptions.useGitIgnore = opts.useGitIgnore;
}
if (rootOptions.noIgnoreParent === undefined) {
rootOptions.noIgnoreParent = opts.noIgnoreParent;
}
}
const exactMatches = new Set<string>();
const fuzzyMatches = new Set<string>();
await Promise.all(
Object.keys(roots).map(async (cwd) => {
try {
const rootOptions = roots[cwd];
await this.doFind(
cwd,
rootOptions,
(candidate) => {
const fileUri = path.join(cwd, candidate);
if (opts.onlyFolders) {
try {
const fs = require('fs');
const stat = fs.statSync(fileUri);
if (!stat.isDirectory()) {
return;
}
} catch (e) {
return;
}
}
if (exactMatches.has(fileUri) || fuzzyMatches.has(fileUri)) {
return;
}
if (
!searchPattern ||
searchPattern === '*' ||
candidate.toLocaleLowerCase().indexOf(stringPattern) !== -1
) {
exactMatches.add(fileUri);
} else if (opts.fuzzyMatch && fuzzy.test(searchPattern, candidate)) {
fuzzyMatches.add(fileUri);
}
if (exactMatches.size + fuzzyMatches.size === opts.limit) {
cancellationSource.cancel();
}
},
token,
);
} catch (e) {
this.logger.error(`Failed to search on path ${cwd}.\n${e}`);
}
}),
);
const sortedExactMatches = Array.from(exactMatches).sort((a, b) => {
const depthA = Path.pathDepth(a);
const depthB = Path.pathDepth(b);
if (depthA === depthB) {
const dirA = dirname(a);
const dirB = dirname(b);
return dirB.localeCompare(dirA, 'en', { numeric: true });
} else {
return depthB - depthA;
}
});
return [...sortedExactMatches, ...fuzzyMatches];
}
private doFind(
cwd: string,
options: IFileSearchService.BaseOptions,
accept: (fileUri: string) => void,
token: CancellationToken,
): Promise<void> {
return new Promise((resolve, reject) => {
try {
const args = this.getSearchArgs(options);
if (options.onlyFolders) {
args.push('--type-list', 'd:dir');
args.push('--type', 'd');
}
const process = this.processFactory.create({ command: replaceAsarInPath(rgPath), args, options: { cwd } });
process.onError(reject);
process.outputStream.on('close', resolve);
const lineReader = readline.createInterface({
input: process.outputStream,
output: process.inputStream,
});
lineReader.on('line', (line) => {
if (token.isCancellationRequested) {
process.dispose();
} else {
accept(line);
}
});
} catch (e) {
reject(e);
}
});
}
private getSearchArgs(options: IFileSearchService.BaseOptions): string[] {
const args = ['--files', '--hidden', '--case-sensitive', '--no-require-git'];
if (options.onlyFolders) {
args.push('--type-list', 'd:dir');
args.push('--type', 'd');
}
if (options.includePatterns) {
for (const includePattern of options.includePatterns) {
if (includePattern) {
args.push('--glob', includePattern);
}
}
}
if (options.excludePatterns) {
for (const excludePattern of options.excludePatterns) {
if (excludePattern) {
args.push('--glob', `!${excludePattern}`);
}
}
}
if (!options.useGitIgnore) {
args.push('-uu');
}
if (options.noIgnoreParent) {
args.push('--no-ignore-parent');
}
return args;
}
}