Repository navigation
Quality-of-life improvements for reviewing diffs - #2
Merged
Merged
Conversation
The header box is drawn at the tree window's text width instead of the configured tree.width, which now only sets the initial width. A WinResized autocmd redraws the header in place whenever that width differs from the width it was drawn at, so dragging the border or :vertical resize keep the box aligned. The tree below the header is left untouched. The autocmd removes itself on the first resize after the view is closed. The text width is the window width minus the columns drawn left of the text. The panel now turns off all of them, including 'statuscolumn', which takes space whenever it is set, so a global status column no longer wraps the box. A column turned on later, for example by a status column plugin, narrows the box on the next resize.
<Tab> from the file tree now moves focus to the diff pane used last instead of always the base (left) pane. The pane focused when the view opens counts as used, so the first <Tab> goes back there. The tracking autocmd removes itself on the first window change after the view is closed.
Each file of the open diff keeps its own cursor position and pane: the line, column and the diff pane (base or head) used last. Leaving a file records it; returning puts the cursor back there, and moves focus to that pane when a diff pane has focus. With the tree focused, focus stays in the tree and <Tab> then goes to that pane. Both panes move to the position, as their rows are aligned. A file not shown before opens at its first hunk (or its top without scroll_to_first_hunk) in the pane in use; until a pane is used, that is the head pane, which also has focus when the view opens. Hunk navigation across files still lands on a hunk. Positions belong to the open view and are dropped when a new diff opens.
Resizing Neovim gave the whole change in width to the rightmost pane, so only the head pane grew or shrank. The two panes now keep splitting the space next to the tree in their last ratio; dragging the separator between them sets a new ratio, while a change of the tree width keeps it. A resize made while another tab is current is applied when the diff tab is entered. The ratio is kept as a fraction, not re-read from the rounded column widths, so repeated resizes do not drift away from it. 'scrollbind' only follows the current window, so scrolling the other pane with the mouse wheel left its partner behind. The partner now takes the same top line. Keyboard scrolling is unchanged.
Selecting a file in the tree now also moves focus to its diff pane: the pane and position the file was left at, else the pane used last, else the head pane. Toggling a directory keeps focus in the tree. On by default; with `focus_diff_on_select = false`, focus stays in the tree as before. `focus_diff()` moves focus to that pane and backs the `<Tab>` mapping in the tree.
A double click on a file in the tree opens it like `select` (<CR>): the file's remembered position comes back, and with focus_diff_on_select its diff pane gets focus. A double click on a directory toggles it.
The plugin uses vim.system and vim.uv, which Neovim 0.10 introduced, so the README and help file now list 0.10 as the minimum instead of 0.9.
The tree filled a file index to node ID table that nothing read.
The panel inherited the global wrap option, so in a narrow panel a long row wrapped onto a second screen line. The panel now turns wrap off and cuts a row that does not fit at the panel edge, ending it with "…" as the header box does. Rows are redrawn when the panel width changes, keeping the cursor and the current-file highlight.
A double click on the split between the two diff panes gives them the same width, leaving the tree alone; later Neovim resizes keep that even split. It works while any window of the diff view has focus. Other double clicks keep their default, such as selecting a word in a pane. equalize_panes() does the same from Lua.
A double click on the side panel's right border gives the panel its configured width (tree.width) again; the diff panes keep their ratio. It works while any window of the diff view has focus. reset_tree_width() does the same from Lua.
New settings: - context_size (default 3): unchanged lines kept visible above and below each change; the rest of the unchanged lines are folded in both diff panes. A file without changes is folded as a whole. 0 turns folding off and leaves the panes' own fold options untouched. - min_fold_size (default 2): shorter runs of unchanged lines stay visible. - fold_by_default (default true): whether the folds start closed. - fold_fill (default "━"): the character of the rule across a closed fold; "=" is used when Neovim rejects it. - fold_accent (default "Directory"): the highlight group whose colour closed folds take. A closed fold reads "━━━━ ▸ N unchanged lines ━━━━", the count centred in the pane, on a band of the fold_accent colour blended into the background (DifftFold, used for Folded in the diff panes only). Invalid values (context_size below 0, min_fold_size below 1) are reported and ignored. The settings apply to files shown after they change.
Folds belong to each window, so opening or closing a fold of unchanged lines in one pane left the other pane as it was. Once Neovim is idle, the pane where a fold changed now passes its state to the other one, whatever made the change: fold commands, :foldopen, a search opening a fold, the API. The other pane's view is lined up again afterwards, as 'scrollbind' does not follow a fold change made in another window. Folds deleted or added in a pane (zE, zd, zf, :fold, the API) would leave the panes showing different lines side by side. Such a pane gets the plugin's folds back and loses any added fold. Each fold keeps its state where it survived: from the pane where it is intact, or its last synced state when it is gone from both.
Each file of the open diff keeps the open or closed state of its folds of unchanged lines along with its cursor position. The states are recorded when the file is shown and after every fold change, so they are current at any time; switching files and closing the view first settle a change not yet passed to the other pane. Returning to a file rebuilds the folds with those states. When the folds differ by then (context_size or min_fold_size changed), fold_by_default applies, and a fold that would hide the remembered cursor line is opened in both panes. States belong to the open view and are dropped when a new diff opens.
A column at the right edge of the tree now shows each file's review state: "•" for a file not shown yet in this diff view, nothing once it was shown, and "✓" for a file marked as reviewed. Rows are cut and padded to end just before that column, so the markers line up and stay visible in a narrow panel. R (toggle_reviewed), the :DifftToggleReviewed command and toggle_reviewed() toggle the mark of the shown file; in the tree, of the file under the cursor, or of every file in the directory under it. With auto_review, showing a file also marks it. The glyphs are tree.icons.unvisited and tree.icons.reviewed, highlighted with DifftTreeUnvisited and DifftTreeReviewed. The marks belong to the open view, are current at any time, and start over in a new diff.
- ]u / [u (next_unreviewed, prev_unreviewed) show the next or previous
file not marked as reviewed, in tree order and wrapping around, opening
collapsed directories around it; with none left, a message says so.
- The header box counts reviewed files ("3/12 reviewed") instead of the
file count once a file is marked.
- A directory shows the reviewed marker once every file in it is reviewed.
:vsplit gives the new pane at least 'winwidth' columns, so when the space next to the tree was under about 40 columns the head pane took 20 and the base pane got the rest, down to a single column. The panes are now set to equal widths once created, which is also the ratio later resizes keep.
git called difft as its external diff tool, one changed file after another, so a large diff took seconds on a single CPU. The changed files are now listed with `git diff --name-status -z`, and difft runs on each file's exact old and new bytes, up to max_parallel_difft_calls files at a time (default 0: one per CPU). difft gets the same arguments git gives an external diff tool and runs from the repository root, so its results, including language and .gitattributes handling, match `git diff` exactly. jj diffs are unchanged. On this repository (32 CPUs), HEAD~60..HEAD (34 files) loads in 2.5 s instead of 8.3 s; the slowest single file sets the floor.
Computing a large diff blocks Neovim for a few seconds with no sign of progress. open() now creates the diff tab first and draws a centred window styled like the side panel's header box: "Loading…" in its top border and the range row below, with the same highlights. Then it computes the diff and builds the view in that tab. When there are no changes or the diff fails, the tab is closed again and the previous tab is current, as before. The window is placed for the current screen size, which can change around startup. Input still queued is handled before it is placed, and :Difft run while Neovim starts up (nvim -c 'Difft …') waits until startup has finished and the size has stayed unchanged for a moment (at most a second): a GUI may resize the grid right after startup, as Neovide does when it applies its scale factor, and a resize arriving while the diff is computed cannot be followed.
The plugin blends its own groups (diff backgrounds, panel colours, filler, fold and header text) from the theme's Normal, Comment, Added, Removed and Changed colours. It did so in setup() and again only on ColorScheme or a 'background' change. Some setups apply the theme later without either event: NvChad's starter config runs plugin setup through lazy.nvim first and then loads its theme from precompiled highlight files with plain nvim_set_hl calls. The derived groups then stayed blended from Neovim's default theme (for example a panel darker and bluer than the editor background). The groups are now also derived again once at VimEnter, after the whole config has run, and whenever a diff view opens, keeping the overrides given to setup(). Linked groups are unchanged.
The Rust workflow becomes a general CI workflow (ci.yaml, "CI"), still run on pushes to main, pull requests and the merge queue. A new job runs the Lua tests with `just test-lua` on stable Neovim, with plenary.nvim and nui.nvim installed as start packages. It builds the native library and installs difft and jj first, so the end-to-end git and jj tests run instead of being skipped.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Quality-of-life improvements for reviewing a diff: the layout behaves on resize, the view remembers where you were in each file, unchanged lines fold away, files can be marked as reviewed, and large git diffs load faster with a loading message shown while they run.
Improvements
Layout and resizing
:vertical resize) redraws the header box at the new width. A global'statuscolumn'no longer makes it wrap.equalize_panes().tree.widthagain. It is also available asreset_tree_width().'wrap'option. A row too long for the panel is cut at the edge and ends with….Navigation and focus
<Tab>returns to the diff pane used last instead of always going to the base pane.'scrollbind'only follows the current window, so wheel-scrolling the other pane used to leave its partner behind.focus_diff_on_select(default on). Selecting a file in the tree moves focus to its diff pane, at the position it was left. Set it tofalseto keep focus in the tree. It is also available asfocus_diff().<CR>, or toggles a directory.Folding unchanged lines
━━━━ ▸ N unchanged lines ━━━━, with the count centred on a tinted band. New settings:context_size(default3): the number of unchanged lines kept visible around each change.0turns folding off.min_fold_size(default2): shorter runs of unchanged lines stay visible.fold_by_default(defaulttrue): whether folds start closed.fold_fill(default"━"): the character of the fold rule.fold_accent(default"Directory"): the highlight group that colours closed folds.:foldopen, a search, the API), is mirrored in the other pane, and the views stay aligned. If folds are deleted or added by hand, the panes get the plugin's folds back.Review tracking
•for a file not shown yet and✓for a file marked as reviewed. Markers stay visible in a narrow panel. The glyphs are set bytree.icons.unvisitedandtree.icons.reviewed, and the colours byDifftTreeUnvisitedandDifftTreeReviewed.R,:DifftToggleReviewedortoggle_reviewed()toggles the mark on the shown file. In the tree, it applies to the file under the cursor or to every file in the directory under the cursor. Withauto_review, showing a file marks it.]uand[u(next_unreviewed/prev_unreviewed) move to the next or previous unreviewed file. The header counts progress (3/12 reviewed), and a fully reviewed directory gets the reviewed marker.Loading
max_parallel_difft_callsat a time (default0, meaning one per CPU). It gets the same arguments and working directory as when git runs it as an external diff tool, so the results matchgit diff. On a 34-file range, loading took 2.5 s instead of 8.3 s. jj diffs are unchanged.nvim -c 'Difft …'waits for startup to settle, so the box is centred even when a GUI such as Neovide resizes the grid right after startup.VimEnterand whenever a diff view opens. This covers configs that apply their theme after plugin setup without aColorSchemeevent, such as NvChad's starter config. Overrides given tosetup()are kept.Housekeeping
vim.systemandvim.uv.CIworkflow, which also runs the Lua tests (just test-lua) on stable Neovim with the native library built and difft and jj installed, so the end-to-end git and jj tests run instead of being skipped.Testing
Every item has plenary tests:
rename,fold,focus,review,header_resize,parallel,loadingandhighlight. Behaviour that depends on the main loop (autocmds firing on idle, resize, mouse events) is tested in a child Neovim over RPC. The full suite passes locally and runs in CI.🤖 Generated with Claude Code