Skip to content

feat(diff): Add search and extension filter to diff sidebar#37068

Draft
McMichalK wants to merge 46 commits into
go-gitea:mainfrom
McMichalK:filter-by-file-extension
Draft

feat(diff): Add search and extension filter to diff sidebar#37068
McMichalK wants to merge 46 commits into
go-gitea:mainfrom
McMichalK:filter-by-file-extension

Conversation

@McMichalK

@McMichalK McMichalK commented Apr 1, 2026

Copy link
Copy Markdown

Add file extension filter for pull request diffs

Introduces a new dropdown filter in the PR diff toolbar that allows reviewers to show/hide files based on their extensions. This helps focus on specific file types during code review (e.g., viewing only .ts files when reviewing TypeScript changes, or hiding .test.js files when reviewing implementation).

Closes: Feature of file filter in PR page #27256

Usage

  1. Open a pull request
  2. Click the filter icon (🔽) in the diff toolbar, above file diff tree
  3. Select/deselect file extensions to show/hide (files without extensions appear as (no extension))
  4. Use the "Select all" / "Deselect all" buttons for quick toggling

How it works

  • The filter scans all files in the PR diff and extracts their extensions
  • Files are grouped by extension with a count displayed
  • Filtering adds the .tw-hidden CSS class to non-matching files, instantly hiding them
  • The button shows a subtle outline indicator when a filter is active
  • Filter state is ephemeral (resets on page reload) — appropriate for per-PR review sessions

Keyboard Navigation

For users who prefer keyboard navigation:

  • Arrow Down/Up: Navigate through checkboxes and buttons
  • Escape: Close the dropdown menu
  • Space: Toggle checkboxes (native HTML behavior)
  • Enter: Activate buttons (native HTML behavior)

Technical Details

  • Vue 3 component (DiffFileExtensionFilter.vue) following the same pattern as the existing commit filter
  • Event listeners use capture-phase for dropdown compatibility with other elements
  • All UI uses Tailwind CSS and Fomantic-UI classes consistent with Gitea's design system
  • Fully accessible with ARIA attributes and semantic HTML
  • Code passes all linting and formatting checks

Testing

  1. Navigate to any pull request with multiple file types
  2. Click the filter button and verify the extension list appears
  3. Test checkbox toggling and the "Select all"/"Deselect all" buttons
  4. Click "Apply" and verify files are hidden/shown correctly
  5. Verify the button shows a visible outline when filtering is active
  6. Close the dropdown and reopen—checkboxes should reflect current view state
  7. Test keyboard navigation: use arrow keys to navigate, Escape to close

Screenshots

Filter by extension dropdown:
obraz

obraz

Note: This implementation uses AI assistance (GitHub Copilot). All code has been reviewed and tested against Gitea's contribution guidelines and passes all linting checks.

There is new button in pull request diff template, which opens dropbox containing list of all unique extensions that the diff contains.
List is created with checkmarks that can be checked/unchecked to filter out files with specific extension. There is also "select all" and "deselect all" buttons.
To apply the change and filter out the files user have to click on "Apply" button. If any extensions were filtered out, then button which opens the dropbox is outlined to signal some files were filtered out.
@GiteaBot GiteaBot added the lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. label Apr 1, 2026
@wxiaoguang

This comment was marked as resolved.

@wxiaoguang wxiaoguang marked this pull request as draft April 1, 2026 09:14
@bircni

bircni commented Apr 1, 2026

Copy link
Copy Markdown
Member

Can we also move the filter to the left, above the files and also add the search bar?

@bircni

bircni commented Apr 1, 2026

Copy link
Copy Markdown
Member
Bildschirmfoto 2026-04-01 um 12 03 57

I think the right side is already very full

@McMichalK

Copy link
Copy Markdown
Author

Can we also move the filter to the left, above the files and also add the search bar?

My thinking was: there is already one feature filtering the diff - filtering by commits, so I've added similar feature nearby to let user have those side by side.

Bildschirmfoto 2026-04-01 um 12 03 57 I think the right side is already very full

Also, as a daily gitea user, which is doing couple PR reviews per day, I do not agree that right side is very crowded right now, because everything is well placed.

I'm open to discussion tho ;)

@McMichalK

Copy link
Copy Markdown
Author

In recent changes I've fixed the code for those issues:

  1. Rewrite it with modern <script setup>.
  2. There is a "loading more" button if the file list exceeds the limit, you need to sync the state after more files are loaded.

And I also added the search box, as suggested. I did not changed the position of the button, waiting for more voices for this change.

@McMichalK McMichalK marked this pull request as ready for review April 1, 2026 16:25
@bircni

bircni commented Apr 1, 2026

Copy link
Copy Markdown
Member

And I also added the search box, as suggested. I did not changed the position of the button, waiting for more voices for this change.

@silverwind whats your opinion on that?
I would really like to have file sorting above the files not at the right side

@bircni bircni left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do a proper pass over the AI-generated output before requesting review.

Comment thread web_src/js/components/DiffFileExtensionFilter.vue Outdated
Comment thread web_src/js/components/DiffFileExtensionFilter.vue
Comment thread web_src/js/components/DiffFileExtensionFilter.vue Outdated
Comment thread templates/repo/diff/box.tmpl Outdated
Comment thread web_src/js/components/DiffFileExtensionFilter.vue Outdated
Comment thread web_src/js/components/DiffFileExtensionFilter.vue Outdated
@GiteaBot GiteaBot added lgtm/blocked A maintainer has reservations with the PR and thus it cannot be merged and removed lgtm/need 2 This PR needs two approvals by maintainers to be considered for merging. labels Apr 1, 2026
Comment thread web_src/js/components/DiffFileExtensionFilter.vue Outdated
@wxiaoguang wxiaoguang dismissed their stale review June 25, 2026 00:37

dismiss

@GiteaBot GiteaBot added lgtm/need 1 This PR needs approval from one additional maintainer to be merged. and removed lgtm/blocked A maintainer has reservations with the PR and thus it cannot be merged labels Jun 25, 2026
@McMichalK

Copy link
Copy Markdown
Author

Could someone else review, please?

@lunny

lunny commented Jun 27, 2026

Copy link
Copy Markdown
Member

Could someone else review, please?

ci failure are related.

@McMichalK

Copy link
Copy Markdown
Author

Could someone else review, please?

ci failure are related.

@lunny I'm not sure what's wrong, because I managed to run those tests locally and it's passing no problem, so I'm not sure what to do about this?

@puni9869

Copy link
Copy Markdown
Member

please check https://github.com/go-gitea/gitea/actions/runs/28135552439/job/83321462123?pr=37068#step:7:1102

Diff-boxes are now in separate div, so div with "no file matched" text is in separate place in parent div
@McMichalK

Copy link
Copy Markdown
Author

please check https://github.com/go-gitea/gitea/actions/runs/28135552439/job/83321462123?pr=37068#step:7:1102

@lunny @puni9869 Thank you for pointing me the link, I managed to fix the failing tests now and sorry for trouble.

@McMichalK

Copy link
Copy Markdown
Author

Anyone else would like to take a look?

@McMichalK

Copy link
Copy Markdown
Author

@wxiaoguang @bircni Hi, I would be most grateful if you could give this PR a look and one more approval, I've invested quite some time in this so it would be very cool to see it merged.

@wxiaoguang

Copy link
Copy Markdown
Contributor

@lunny @silverwind

@bircni

bircni commented Jul 12, 2026

Copy link
Copy Markdown
Member

Finally had the time to try Fable so here you go
Will check on it tomorrow myself

Review — file extension filter

Nice feature overall! I ran a deeper review over the branch; 4 correctness issues in the new filtering logic and 4 smaller cleanups. Details below, roughly ordered by severity.


🐛 Correctness

web_src/js/modules/diff-file.ts:181 — Renamed files are matched asymmetrically between the diff boxes and the tree/counts.

const matched = matches(newName) || (oldName !== newName && matches(oldName));

The file boxes match against data-old-filename too, but filterDiffTree and countMatchingFiles only test entry.FullName (the backend's WebDiffFileItem carries no old name). Searching for a renamed file's old path keeps its diff box visible while the sidebar says "No files matched your search", and visibleCount can exceed countMatchingFiles, so the "Show more — N matching below" count is understated (only masked by the Math.max(0, …) clamp). Suggest matching only the new name everywhere, or including old names in the tree matching too.

web_src/js/modules/diff-file.ts:187 — "No files matched" can show directly above "Show more — N matching below".

On a paginated diff, a filter that only matches not-yet-loaded files gives visibleCount === 0, so #diff-no-matches is shown — while updateShowMoreButton simultaneously says N files match below (countMatchingFiles walks the full tree, the placeholder only counts loaded boxes). The page contradicts itself; the placeholder should account for countMatchingFiles(store) > 0.

web_src/js/components/DiffFileTree.vue:64 — Hiding the file tree strands an active filter with no visible control to clear it.

toggleVisibility only flips store.fileTreeIsVisible; the query/extension filter stays applied to the diff boxes while the v-if unmounts the search input, clear button, and extension menu. Worst case the user sees a full-page "No files matched" with no hint why — and since the hidden state persists in localStorage (the filter doesn't), it survives until they rediscover the toggle. Suggest resetting (or bypassing) the filters when the tree is hidden.

web_src/js/modules/diff-file.ts:91 — Dotfiles each become their own "extension" in the filter menu.

extname() in web_src/js/utils.ts returns the full basename for dotfiles (.gitignore, dir/.eslintrc.eslintrc), so a PR touching a few dotfiles fills the menu with one-off filename entries, while Makefile correctly lands in "(no extension)". Node's path.extname returns '' for dotfiles — suggest normalizing them into the "(no extension)" bucket.


🧹 Cleanups

web_src/js/components/DiffFileTree.vue:23 — The filter watch runs the full DOM pass on every keystroke.

Each character triggers applyFiltersToFileBoxes: a querySelectorAll over every loaded .diff-file-box, a display toggle per box, and two full fullNameMap iterations — noticeable thrash on PRs with thousands of files. onInputDebounce (web_src/js/utils/dom.ts:244) already exists; debouncing the file-box side effect (the tree computed can stay live) would fix it.

web_src/js/components/DiffFileTree.vue:38 — The MutationObserver duplicates the existing onShowMoreFiles hook.

The only childList mutation of #diff-file-boxes is loadMoreFiles in repo-diff.ts, which already calls onShowMoreFiles() right after the replaceWith. Calling applyFiltersToFileBoxes from there (guarded by pageData.DiffFileTree, since it also runs without a tree) removes the observer and its lifecycle state; there's no import cycle preventing it.

web_src/js/modules/diff-file.ts:143fillTemplate re-implements the %[N]d substitution already inlined in pull-view-file.ts:19-21.

Two independent implementations of the same Go-style placeholder handling means a fix to one silently misses the other. Suggest one shared helper next to trN in web_src/js/modules/i18n.ts, used by both call sites.

web_src/js/features/repo-diff.ts:173 — Still-relevant comments were removed.

In particular // "children:HTMLCollection" will be empty after replaceWith documents a real footgun (inlining the Array.from would silently drop all but the first loaded box — the same comment survives ~30 lines below for the analogous code), plus // Default to true if unset and // expand file if the selected file is folded in DiffFileTree.vue. Please restore them.


Review assisted by Claude Code (Fable 5).

@lunny lunny added this to the 28.0.0 milestone Jul 13, 2026
@GiteaBot GiteaBot added lgtm/done This PR has enough approvals to get merged. There are no important open reservations anymore. and removed lgtm/need 1 This PR needs approval from one additional maintainer to be merged. labels Jul 13, 2026
Comment thread templates/repo/diff/box.tmpl Outdated
data-file-extensions="{{ctx.Locale.Tr "repo.diff.file_extensions"}}"
data-no-file-extension="{{ctx.Locale.Tr "repo.diff.no_file_extension"}}"
data-select-all-file-extensions="{{ctx.Locale.Tr "repo.diff.select_all_file_extensions"}}"
data-select-none-file-extensions="{{ctx.Locale.Tr "repo.diff.select_none_file_extensions"}}"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The locale related attributes should have "data-text-" or "data-locale-" prefix to distinguish them from other attributes.

Comment on lines +15 to +19
const el = document.querySelector<HTMLElement>('#diff-file-tree')!;

const filterFilesPlaceholder = el.getAttribute('data-text-filter-files')!;
const filterFilesNoResults = el.getAttribute('data-text-filter-files-no-results')!;
const filterFilesClearLabel = el.getAttribute('data-text-filter-files-clear')!;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should avoid making the Vue component query its parent.

See other Vue component, the caller should prepare the props (locale attributes) for it.

@bircni bircni marked this pull request as draft July 13, 2026 04:51
Comment thread web_src/js/modules/diff-file.ts Outdated
Comment on lines +154 to +164
el.textContent = fillTemplate(el.getAttribute('data-text-loaded') ?? '', loadedFiles, totalFiles);
}

function updateShowMoreButton(matchingBelow: number) {
const btn = document.querySelector('#diff-show-more-files');
if (!btn) return;
if (matchingBelow > 0) {
btn.textContent = fillTemplate(btn.getAttribute('data-text-matching') ?? '', matchingBelow);
} else {
btn.textContent = btn.getAttribute('data-text-default') ?? btn.textContent;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why you expect that these attributes wouldn't exist ........

@wxiaoguang

Copy link
Copy Markdown
Contributor

Is it expected that when I click "Select none", then nothing is listed?

image

@wxiaoguang

Copy link
Copy Markdown
Contributor

Also, some reviews from #37068 (comment) are still valid

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lgtm/done This PR has enough approvals to get merged. There are no important open reservations anymore. type/feature Completely new functionality. Can only be merged if feature freeze is not active.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature of file filter in PR page

9 participants