feat: allow control over displaying hidden/dotfiles and .gitignore matches (#179)
* feat: add chat setting to toggle hidden files (dotfiles) * feat: add toggle to show/hide gitignored files in file browser * refactor: don't reuse visibility setting for dotfiles toggle * fix: honor hidden/gitignored toggles across runtimes --------- Co-authored-by: Bohdan Triapitsyn <artmore@protonmail.com>
This commit is contained in:
committed by
GitHub
co-authored by
Bohdan Triapitsyn
parent
6544b12e78
commit
2de0d9d3dd
@@ -68,11 +68,13 @@ const normalizeRelativeSearchPath = (rootPath, targetPath) => {
|
||||
return relative.split(path.sep).join('/') || targetPath;
|
||||
};
|
||||
|
||||
const shouldSkipSearchDirectory = (name) => {
|
||||
const shouldSkipSearchDirectory = (name, includeHidden) => {
|
||||
if (!name) {
|
||||
return false;
|
||||
}
|
||||
// allow dot dirs/files; still skip excluded dirs below
|
||||
if (!includeHidden && name.startsWith('.')) {
|
||||
return true;
|
||||
}
|
||||
return FILE_SEARCH_EXCLUDED_DIRS.has(name.toLowerCase());
|
||||
};
|
||||
|
||||
@@ -157,33 +159,74 @@ const fuzzyMatchScoreNormalized = (normalizedQuery, candidate) => {
|
||||
};
|
||||
|
||||
const searchFilesystemFiles = async (rootPath, options) => {
|
||||
const { limit, query } = options;
|
||||
const { limit, query, includeHidden, respectGitignore } = options;
|
||||
const includeHiddenEntries = Boolean(includeHidden);
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
const matchAll = normalizedQuery.length === 0;
|
||||
const queue = [rootPath];
|
||||
const visited = new Set([rootPath]);
|
||||
const shouldRespectGitignore = respectGitignore !== false;
|
||||
// Collect more candidates for fuzzy matching, then sort and trim
|
||||
const collectLimit = matchAll ? limit : Math.max(limit * 3, 200);
|
||||
const candidates = [];
|
||||
|
||||
while (queue.length > 0 && candidates.length < collectLimit) {
|
||||
const batch = queue.splice(0, FILE_SEARCH_MAX_CONCURRENCY);
|
||||
const dirLists = await Promise.all(batch.map((dir) => listDirectoryEntries(dir)));
|
||||
|
||||
for (let index = 0; index < batch.length; index += 1) {
|
||||
const currentDir = batch[index];
|
||||
const dirents = dirLists[index];
|
||||
const dirResults = await Promise.all(
|
||||
batch.map(async (dir) => {
|
||||
if (!shouldRespectGitignore) {
|
||||
return { dir, dirents: await listDirectoryEntries(dir), ignoredPaths: new Set() };
|
||||
}
|
||||
|
||||
try {
|
||||
const dirents = await listDirectoryEntries(dir);
|
||||
const pathsToCheck = dirents.map((dirent) => dirent.name).filter(Boolean);
|
||||
if (pathsToCheck.length === 0) {
|
||||
return { dir, dirents, ignoredPaths: new Set() };
|
||||
}
|
||||
|
||||
const result = await new Promise((resolve) => {
|
||||
const child = spawn('git', ['check-ignore', '--', ...pathsToCheck], {
|
||||
cwd: dir,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
||||
child.on('close', () => resolve(stdout));
|
||||
child.on('error', () => resolve(''));
|
||||
});
|
||||
|
||||
const ignoredNames = new Set(
|
||||
String(result)
|
||||
.split('\n')
|
||||
.map((name) => name.trim())
|
||||
.filter(Boolean)
|
||||
);
|
||||
|
||||
return { dir, dirents, ignoredPaths: ignoredNames };
|
||||
} catch {
|
||||
return { dir, dirents: await listDirectoryEntries(dir), ignoredPaths: new Set() };
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
for (const { dir: currentDir, dirents, ignoredPaths } of dirResults) {
|
||||
for (const dirent of dirents) {
|
||||
const entryName = dirent.name;
|
||||
if (!entryName) {
|
||||
if (!entryName || (!includeHiddenEntries && entryName.startsWith('.'))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (shouldRespectGitignore && ignoredPaths.has(entryName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const entryPath = path.join(currentDir, entryName);
|
||||
|
||||
if (dirent.isDirectory()) {
|
||||
if (shouldSkipSearchDirectory(entryName)) {
|
||||
if (shouldSkipSearchDirectory(entryName, includeHiddenEntries)) {
|
||||
continue;
|
||||
}
|
||||
if (!visited.has(entryPath)) {
|
||||
@@ -4330,8 +4373,10 @@ async function main(options = {}) {
|
||||
: typeof req.query.directory === 'string' && req.query.directory.trim().length > 0
|
||||
? req.query.directory.trim()
|
||||
: os.homedir();
|
||||
const rawQuery = typeof req.query.q === 'string' ? req.query.q : '';
|
||||
const limitParam = typeof req.query.limit === 'string' ? Number.parseInt(req.query.limit, 10) : undefined;
|
||||
const rawQuery = typeof req.query.q === 'string' ? req.query.q : '';
|
||||
const includeHidden = req.query.includeHidden === 'true';
|
||||
const respectGitignore = req.query.respectGitignore !== 'false';
|
||||
const limitParam = typeof req.query.limit === 'string' ? Number.parseInt(req.query.limit, 10) : undefined;
|
||||
const parsedLimit = Number.isFinite(limitParam) ? Number(limitParam) : DEFAULT_FILE_SEARCH_LIMIT;
|
||||
const limit = Math.max(1, Math.min(parsedLimit, MAX_FILE_SEARCH_LIMIT));
|
||||
|
||||
@@ -4342,7 +4387,12 @@ async function main(options = {}) {
|
||||
return res.status(400).json({ error: 'Specified root is not a directory' });
|
||||
}
|
||||
|
||||
const files = await searchFilesystemFiles(resolvedRoot, { limit, query: rawQuery || '' });
|
||||
const files = await searchFilesystemFiles(resolvedRoot, {
|
||||
limit,
|
||||
query: rawQuery || '',
|
||||
includeHidden,
|
||||
respectGitignore,
|
||||
});
|
||||
res.json({
|
||||
root: resolvedRoot,
|
||||
count: files.length,
|
||||
|
||||
Reference in New Issue
Block a user