Repository navigation
Add file library exclusion globs #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| function normalizeSeparators(value: string): string { | ||
| return value.replace(/\\/g, '/').replace(/\/+$/, '') | ||
| } | ||
|
|
||
| function normalizePattern(value: string): string { | ||
| return value.trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '') | ||
| } | ||
|
|
||
| function escapeRegexCharacter(value: string): string { | ||
| return /[\\^$.*+?()[\]{}|]/.test(value) ? `\\${value}` : value | ||
| } | ||
|
|
||
| function globSource(pattern: string): string { | ||
| let source = '' | ||
| let index = 0 | ||
| while (index < pattern.length) { | ||
| if (pattern.slice(index, index + 3) === '**/') { | ||
| source += '(?:.*/)?' | ||
| index += 3 | ||
| continue | ||
| } | ||
| if (pattern.slice(index, index + 3) === '/**' && index + 3 === pattern.length) { | ||
| source += '(?:/.*)?' | ||
| index += 3 | ||
| continue | ||
| } | ||
| if (pattern.slice(index, index + 2) === '**') { | ||
| source += '.*' | ||
| index += 2 | ||
| continue | ||
| } | ||
| if (pattern[index] === '*') source += '[^/]*' | ||
| else if (pattern[index] === '?') source += '[^/]' | ||
| else source += escapeRegexCharacter(pattern[index]) | ||
| index += 1 | ||
| } | ||
| return source | ||
| } | ||
|
|
||
| interface CompiledPattern { | ||
| basenameOnly: boolean | ||
| expression: RegExp | ||
| } | ||
|
|
||
| export function compileFileTreeExcludeGlobs(value: string): CompiledPattern[] { | ||
| if (typeof value !== 'string' || !value.trim()) return [] | ||
| return value | ||
| .split(/\r?\n/) | ||
| .slice(0, 100) | ||
| .map((line) => normalizePattern(line).slice(0, 256)) | ||
| .filter(Boolean) | ||
|
Comment on lines
+48
to
+51
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the preference contains blank separator lines, the 100-line limit is applied before normalization and empty-line removal, so those blanks consume pattern slots and valid globs later in the textarea are silently ignored. For example, 50 patterns separated by blank lines already exhaust the limit before any subsequent entries are compiled, causing entries the user explicitly excluded to remain visible. Filter empty normalized lines before taking the first 100 patterns. Useful? React with 👍 / 👎. |
||
| .map((pattern) => ({ | ||
| basenameOnly: !pattern.includes('/'), | ||
| expression: new RegExp(`^${globSource(pattern)}$`) | ||
| })) | ||
|
Comment on lines
+52
to
+55
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For filenames containing a non-BMP character such as an emoji, Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| function relativeTreePath(nodePath: string, folderPath: string): string | null { | ||
| const node = normalizeSeparators(nodePath) | ||
| const root = normalizeSeparators(folderPath) | ||
| if (node === root) return '' | ||
| return node.startsWith(`${root}/`) ? node.slice(root.length + 1) : null | ||
| } | ||
|
|
||
| function isPreservedBranch(nodePath: string, preservePaths: string[]): boolean { | ||
| const node = normalizeSeparators(nodePath) | ||
| return preservePaths.some((candidate) => candidate === node || candidate.startsWith(`${node}/`)) | ||
| } | ||
|
|
||
| export function filterFileTreeByGlobs( | ||
| nodes: TypeOpenTreeNode[], | ||
| folderPath: string | null, | ||
| patternText: string, | ||
| preservePaths: Array<string | null | undefined> = [] | ||
| ): TypeOpenTreeNode[] { | ||
| if (!folderPath) return nodes | ||
| const patterns = compileFileTreeExcludeGlobs(patternText) | ||
| if (!patterns.length) return nodes | ||
|
|
||
| const preserved = preservePaths.filter((value): value is string => typeof value === 'string' && value.length > 0).map(normalizeSeparators) | ||
| const visit = (node: TypeOpenTreeNode, ancestorExcluded = false): TypeOpenTreeNode | null => { | ||
| const relativePath = relativeTreePath(node.path, folderPath) | ||
| const matches = relativePath !== null && patterns.some(({ basenameOnly, expression }) => expression.test(basenameOnly ? node.name : relativePath)) | ||
| const excluded = ancestorExcluded || matches | ||
| const preservedBranch = isPreservedBranch(node.path, preserved) | ||
| if (excluded && !preservedBranch) return null | ||
|
|
||
| if (!node.children) return node | ||
| const children = node.children | ||
| .map((child) => visit(child, excluded)) | ||
| .filter((child): child is TypeOpenTreeNode => child !== null) | ||
| return { ...node, children } | ||
| } | ||
|
|
||
| return nodes.map((node) => visit(node)).filter((node): node is TypeOpenTreeNode => node !== null) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When a user selects a directory or image and then adds a glob that excludes it,
visibleFileTreeremoves the row butSidebarretains the node in its localselectedNodestate. The toolbar therefore remains enabled with no visible selection, and Create, Rename, Reveal, or Move to Trash still operates on that hidden stale path; in particular, clicking the trash action can remove an item the user can no longer see. Clear the selection whenever its path is absent from the newly filtered tree.Useful? React with 👍 / 👎.