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:
syntext
2026-01-20 15:20:17 +02:00
committed by GitHub
co-authored by Bohdan Triapitsyn
parent 6544b12e78
commit 2de0d9d3dd
19 changed files with 639 additions and 157 deletions
+62 -12
View File
@@ -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,