feat: hide dotfiles in file explorer (#1516)

* feat: hide dotfiles in file explorer

* refactor: rename to "hidden files" and read flag from store

Rename the explorer dotfile toggle to show-semantics "hidden files"
so copy and code share one cross-platform vocabulary, and stop
prop-drilling the flag now that it lives in the panel store.

- explorerShowHiddenFiles (default true) replaces explorerHideDotFiles;
  i18n keys + translations switched from "dotfiles" to "hidden files"
- imperative helpers read usePanelStore.getState() instead of taking
  the flag as a parameter; flag stays threaded only through the
  reactive tree-row render path
- drop dead usePanelState exposure; collapse migration double-guard

* fix: address Greptile review on hidden-file path matching

- isHiddenExplorerPath no longer treats ".." segments as hidden
  (only "." was exempt before); add tests for ".." and "../sibling"
- assert the showHiddenFiles i18n key alongside hideHiddenFiles

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
This commit is contained in:
Yurui Zhou
2026-06-15 12:28:17 +08:00
committed by GitHub
co-authored by Mohamed Boudra
parent f4450cda51
commit eb94b70848
13 changed files with 230 additions and 51 deletions
@@ -22,6 +22,8 @@ import {
ChevronRight,
Copy,
Download,
Eye,
EyeOff,
MoreVertical,
RotateCw,
} from "lucide-react-native";
@@ -43,6 +45,7 @@ import { buildWorkspaceExplorerStateKey } from "@/hooks/use-file-explorer-action
import { usePanelStore, type SortOption } from "@/stores/panel-store";
import { formatTimeAgo } from "@/utils/time";
import { buildAbsoluteExplorerPath } from "@/utils/explorer-paths";
import { filterVisibleExplorerEntries, isHiddenExplorerPath } from "@/file-explorer/visibility";
import { useWebScrollViewScrollbar } from "@/components/use-web-scrollbar";
import { isWeb } from "@/constants/platform";
@@ -267,7 +270,11 @@ export function FileExplorerPane({
workspaceRoot: normalizedWorkspaceRoot,
});
const sortOption = usePanelStore((state) => state.explorerSortOption);
const showHiddenFiles = usePanelStore((state) => state.explorerShowHiddenFiles);
const setSortOption = usePanelStore((state) => state.setExplorerSortOption);
const toggleExplorerShowHiddenFiles = usePanelStore(
(state) => state.toggleExplorerShowHiddenFiles,
);
const expandedPathsArray = usePanelStore((state) =>
workspaceStateKey ? state.expandedPathsByWorkspace[workspaceStateKey] : undefined,
);
@@ -379,6 +386,14 @@ export function FileExplorerPane({
setSortOption(SORT_OPTIONS[nextIndex].value);
}, [sortOption, setSortOption]);
const handleToggleHiddenFiles = useCallback(() => {
const willShow = !usePanelStore.getState().explorerShowHiddenFiles;
toggleExplorerShowHiddenFiles();
if (willShow) {
requestPersistedExpandedPaths({ workspaceStateKey, requestDirectoryListing });
}
}, [requestDirectoryListing, toggleExplorerShowHiddenFiles, workspaceStateKey]);
const refreshExplorer = useCallback(
() =>
refreshExplorerDirectories({
@@ -409,8 +424,8 @@ export function FileExplorerPane({
const currentSortLabel = resolveCurrentSortLabel(sortOption, sortLabels);
const treeRows = useMemo(
() => resolveTreeRows({ directories, expandedPaths, sortOption }),
[directories, expandedPaths, sortOption],
() => resolveTreeRows({ directories, expandedPaths, sortOption, showHiddenFiles }),
[directories, expandedPaths, showHiddenFiles, sortOption],
);
const showInitialLoading = resolveShowInitialLoading({
@@ -483,6 +498,7 @@ export function FileExplorerPane({
scrollbar={scrollbar}
renderTreeRow={renderTreeRow}
handleSortCycle={handleSortCycle}
handleToggleHiddenFiles={handleToggleHiddenFiles}
handleRefresh={handleRefresh}
handleBackFromError={handleBackFromError}
handleRetry={handleRetry}
@@ -505,6 +521,7 @@ interface FileExplorerPaneContentProps {
scrollbar: ReturnType<typeof useWebScrollViewScrollbar>;
renderTreeRow: (info: ListRenderItemInfo<TreeRow>) => ReactElement;
handleSortCycle: () => void;
handleToggleHiddenFiles: () => void;
handleRefresh: () => void;
handleBackFromError: () => void;
handleRetry: () => void;
@@ -527,6 +544,7 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) {
scrollbar,
renderTreeRow,
handleSortCycle,
handleToggleHiddenFiles,
handleRefresh,
handleBackFromError,
handleRetry,
@@ -534,6 +552,26 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) {
iconButtonStyle: iconButtonStyleProp,
} = props;
const showHiddenFiles = usePanelStore((state) => state.explorerShowHiddenFiles);
const hiddenFilesToggleAccessibilityLabel = showHiddenFiles
? t("workspace.fileExplorer.actions.hideHiddenFiles")
: t("workspace.fileExplorer.actions.showHiddenFiles");
const emptyLabel = showHiddenFiles
? t("workspace.fileExplorer.empty.noFiles")
: t("workspace.fileExplorer.empty.noVisibleFiles");
const hiddenFilesToggleStyle = useCallback(
(state: PressableStateCallbackType) => [
iconButtonStyleProp(state),
!showHiddenFiles && styles.iconButtonActive,
],
[showHiddenFiles, iconButtonStyleProp],
);
const hiddenFilesToggleAccessibilityState = useMemo(
() => ({ selected: !showHiddenFiles }),
[showHiddenFiles],
);
if (error) {
return (
<View style={styles.centerState}>
@@ -561,14 +599,6 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) {
);
}
if (treeRows.length === 0) {
return (
<View style={styles.centerState}>
<Text style={styles.emptyText}>{t("workspace.fileExplorer.empty.noFiles")}</Text>
</View>
);
}
return (
<View style={TREE_PANE_CONTAINER_STYLE}>
<View style={styles.paneHeader} testID="files-pane-header">
@@ -576,45 +606,67 @@ function FileExplorerPaneContent(props: FileExplorerPaneContentProps) {
<Text style={styles.sortTriggerText}>{currentSortLabel}</Text>
<ChevronDown size={12} color={theme.colors.foregroundMuted} />
</Pressable>
<Pressable
onPress={handleRefresh}
disabled={isRefreshFetching}
hitSlop={8}
style={iconButtonStyleProp}
accessibilityRole="button"
accessibilityLabel={
isRefreshFetching
? t("workspace.fileExplorer.actions.refreshing")
: t("workspace.fileExplorer.actions.refresh")
}
>
<View style={styles.refreshIcon}>
{isRefreshFetching ? (
<LoadingSpinner size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
<View style={styles.headerActions}>
<Pressable
onPress={handleToggleHiddenFiles}
hitSlop={8}
style={hiddenFilesToggleStyle}
accessibilityRole="button"
accessibilityLabel={hiddenFilesToggleAccessibilityLabel}
accessibilityState={hiddenFilesToggleAccessibilityState}
>
{showHiddenFiles ? (
<Eye size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
) : (
<RotateCw size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
<EyeOff size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
)}
</View>
</Pressable>
</Pressable>
<Pressable
onPress={handleRefresh}
disabled={isRefreshFetching}
hitSlop={8}
style={iconButtonStyleProp}
accessibilityRole="button"
accessibilityLabel={
isRefreshFetching
? t("workspace.fileExplorer.actions.refreshing")
: t("workspace.fileExplorer.actions.refresh")
}
>
<View style={styles.refreshIcon}>
{isRefreshFetching ? (
<LoadingSpinner size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
) : (
<RotateCw size={theme.iconSize.sm} color={theme.colors.foregroundMuted} />
)}
</View>
</Pressable>
</View>
</View>
<FlatList
ref={treeListRef}
style={styles.treeList}
data={treeRows}
renderItem={renderTreeRow}
keyExtractor={treeRowKeyExtractor}
testID="file-explorer-tree-scroll"
contentContainerStyle={styles.entriesContent}
onLayout={scrollbar.onLayout}
onScroll={scrollbar.onScroll}
onContentSizeChange={scrollbar.onContentSizeChange}
scrollEventThrottle={16}
showsVerticalScrollIndicator={!showDesktopWebScrollbar}
initialNumToRender={24}
maxToRenderPerBatch={40}
windowSize={12}
/>
{scrollbar.overlay}
{treeRows.length === 0 ? (
<View style={styles.centerState}>
<Text style={styles.emptyText}>{emptyLabel}</Text>
</View>
) : (
<FlatList
ref={treeListRef}
style={styles.treeList}
data={treeRows}
renderItem={renderTreeRow}
keyExtractor={treeRowKeyExtractor}
testID="file-explorer-tree-scroll"
contentContainerStyle={styles.entriesContent}
onLayout={scrollbar.onLayout}
onScroll={scrollbar.onScroll}
onContentSizeChange={scrollbar.onContentSizeChange}
scrollEventThrottle={16}
showsVerticalScrollIndicator={!showDesktopWebScrollbar}
initialNumToRender={24}
maxToRenderPerBatch={40}
windowSize={12}
/>
)}
{treeRows.length > 0 ? scrollbar.overlay : null}
</View>
);
}
@@ -643,12 +695,14 @@ function buildTreeRows({
directories,
expandedPaths,
sortOption,
showHiddenFiles,
path,
depth,
}: {
directories: Map<string, { path: string; entries: ExplorerEntry[] }>;
expandedPaths: Set<string>;
sortOption: SortOption;
showHiddenFiles: boolean;
path: string;
depth: number;
}): TreeRow[] {
@@ -658,7 +712,10 @@ function buildTreeRows({
}
const rows: TreeRow[] = [];
const entries = sortEntries(directory.entries, sortOption);
const entries = sortEntries(
filterVisibleExplorerEntries(directory.entries, showHiddenFiles),
sortOption,
);
for (const entry of entries) {
rows.push({ entry, depth });
@@ -668,6 +725,7 @@ function buildTreeRows({
directories,
expandedPaths,
sortOption,
showHiddenFiles,
path: entry.path,
depth: depth + 1,
}),
@@ -731,15 +789,24 @@ function resolveTreeRows({
directories,
expandedPaths,
sortOption,
showHiddenFiles,
}: {
directories: Map<string, { path: string; entries: ExplorerEntry[] }>;
expandedPaths: Set<string>;
sortOption: SortOption;
showHiddenFiles: boolean;
}): TreeRow[] {
if (!directories.get(".")) {
return [];
}
return buildTreeRows({ directories, expandedPaths, sortOption, path: ".", depth: 0 });
return buildTreeRows({
directories,
expandedPaths,
sortOption,
showHiddenFiles,
path: ".",
depth: 0,
});
}
type StartDownloadFn = ReturnType<typeof useDownloadStore.getState>["startDownload"];
@@ -890,12 +957,13 @@ function requestPersistedExpandedPaths({
opts?: { recordHistory?: boolean; setCurrentPath?: boolean },
) => Promise<boolean>;
}): void {
const showHiddenFiles = usePanelStore.getState().explorerShowHiddenFiles;
const persistedPaths = usePanelStore.getState().expandedPathsByWorkspace[workspaceStateKey ?? ""];
if (!persistedPaths) {
return;
}
for (const path of persistedPaths) {
if (path !== ".") {
if (path !== "." && (showHiddenFiles || !isHiddenExplorerPath(path))) {
void requestDirectoryListing(path, {
recordHistory: false,
setCurrentPath: false,
@@ -919,7 +987,10 @@ async function refreshExplorerDirectories({
if (!hasWorkspaceScope) {
return null;
}
const directoryPaths = Array.from(expandedPaths);
const showHiddenFiles = usePanelStore.getState().explorerShowHiddenFiles;
const directoryPaths = Array.from(expandedPaths).filter(
(path) => showHiddenFiles || !isHiddenExplorerPath(path),
);
if (!directoryPaths.includes(".")) {
directoryPaths.unshift(".");
}
@@ -1011,6 +1082,11 @@ const styles = StyleSheet.create((theme) => ({
fontSize: theme.fontSize.xs,
color: theme.colors.foregroundMuted,
},
headerActions: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[1],
},
treeList: {
flex: 1,
minHeight: 0,
@@ -1154,6 +1230,9 @@ const styles = StyleSheet.create((theme) => ({
iconButtonHovered: {
backgroundColor: theme.colors.surface2,
},
iconButtonActive: {
backgroundColor: theme.colors.surface2,
},
refreshIcon: {
width: 16,
height: 16,