Keep the cursor on its code line - #314
Conversation
There was a problem hiding this comment.
🔵 Needs a closer look
It changes core cursor/navigation lifecycle and the on-disk session format across multiple views, and the description's "unchanged on disk" guarantee does not match the write path, warranting human verification.
Pull request overview
This PR implements step 1 of issue #313: when you step between revisions of a file in a diff/file-history view, the cursor now stays on the same line of code rather than the same line number. It does so by diffing the buffer you are leaving against the one arriving and mapping the saved cursor line through the resulting hunks, and by adding a second restore path so re-visited entries (which don't re-fire file_open_new) also get their cursor carried.
Changes:
- Add
StandardView._map_lnum/_translate_winviewand aStandardView.CarryState({winview, bufnr?, bufname?});snapshot_main_viewnow records the source buffer sorestore_main_viewcan translate the cursor line viavim.diff. - Add a
file_open_postlistener that restores the cursor on re-visited entries (entry.opened == true), leaving first opens tofile_open_new. - Migrate
cursor_mapentries to the nestedwinviewshape across seeding (--selected-row) and session read;sanitize_cursor_maplifts pre-carry bare dicts and drops buffer handles. - Document the behavior and add extensive unit + functional tests.
File summaries
| File | Description |
|---|---|
lua/diffview/scene/views/standard/standard_view.lua |
Core carry logic: line mapping, winview translation, capture/apply, new restore listener. |
lua/diffview/scene/views/diff/diff_view.lua |
Seeds cursor_map in the new winview-wrapped shape. |
lua/diffview/session.lua |
sanitize_cursor_map converts sidecar entries to CarryState, dropping stale buffer handles. |
doc/diffview.txt |
New diffview-cursor-carry help section. |
lua/diffview/tests/functional/cursor_carry_spec.lua |
New unit tests for _map_lnum, _translate_winview, snapshot/restore. |
lua/diffview/tests/functional/file_history_cursor_carry_spec.lua |
New end-to-end carry tests across commit/entry steps. |
lua/diffview/tests/functional/focus_diff_spec.lua |
Updates seeding assertions to the winview shape. |
lua/diffview/tests/functional/session_spec.lua |
Tests sidecar rehydration + buffer-handle dropping. |
I reviewed the mapping math (insertion/deletion/change deltas, EOF and lower-bound clamping), the event ordering that distinguishes first open (opened still false during file_open_post, handled by file_open_new) from re-visits, the safety fallbacks (invalid/reused buffer, missing source buffer, session-restored states without bufnr), and the round-trip through sanitize_cursor_map. The logic is sound, guarded with safe fallbacks to the prior behavior, and the tests are internally consistent with the described semantics. I found no objective defects to comment on.
One note for the human reviewer (not tied to a changed line, so not filed as a comment): the description states "Session sidecars are unchanged on disk" and "the write drops buffer handles," but session.lua:269 writes the live view.cursor_map, so bufnr/bufname are now serialized to disk (nested under winview) and only dropped on read by sanitize_cursor_map. This is functionally safe but worth verifying against the intended on-disk guarantee.
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 0
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
696ec86 to
e4f4e32
Compare
Part of #313
If a commit added or removed lines above your cursor, stepping to that commit
moves the cursor into code irrelevant to what you were reading. Diffview saves
the cursor's line number and jumps to that number in the new revision, without
looking at what the commit did to the lines above it.
It now diffs the buffer you are leaving against the one arriving and maps the
cursor line through the result. A line below a hunk shifts by that hunk's size
delta. A line the hunk rewrote maps to the hunk's first line, or to the last
surviving line before it when the hunk only deletes. The saved state carries
the buffer it came from, so there is something to diff against.
Diffview also only jumps to that line number on an entry's first open. Step
back onto a commit you have already seen and the cursor goes to line 1 instead.
file_open_newdoes not fire on a repeat visit, so a second listener onfile_open_postrestores those.vim.diff"indices" hunks, so it does notdepend on which VCS the view is showing.
behaviour, so this cannot end up worse than what you have now. That includes
the case where the recorded buffer handle has been reused for another file.
they mean nothing in the next nvim run, and the read accepts a sidecar
written before this change as a bare
winsaveviewdict.SIDECAR_VERSIONstays at 1.