You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
codediff sets ~28 buffer-local keymaps on user file buffers (original_bufnr + modified_bufnr in WORKING mode) and ~3 window-local options (scrollbind, cursorbind, diff) without ever saving the pre-existing user/plugin state and without restoring it on teardown. When a CodeDiff session ends — or when a buffer escapes the session via gf / <C-w>d without ending it — the user is left with codediff's overrides nailed to their buffer until they restart Neovim.
This is a tracking issue for the lifecycle-cleanup refactor that fixes the class. It collects every reported symptom under one umbrella so they can close via a single coherent fix.
lua/codediff/ui/view/keymaps.lua:647,650,653,677 (bypass the chokepoint)
name-collision sweep — ih leaks because line 479 hard-codes mode = "n"
❌
❌
10 conflict keys + do/dp overrides
lua/codediff/ui/view/conflict/keymaps.lua:31..140 (bypass the chokepoint)
never deleted at all
❌
❌
scrollbind / cursorbind window options
lua/codediff/ui/view/side_by_side.lua
only restored on tab-close path; leaks when buffer escapes via gf or <C-w>d
❌
❌
Grep confirms zero occurrences of maparg, mapset, keymap_save, saved_keymap, original_map across the entire tree. No prior attempt at save-and-restore exists.
The gf (open-in-previous-tab) flow with the default close_on_open_in_prev_tab = false is the most insidious case: the diff session stays alive AND the file buffer now appears in another tab still wearing codediff's [c/]c (etc.) bindings. No teardown event ever fires for that escape until the whole session ends.
Proposed fix
Single per-session registry of overridden state, keyed by (bufnr | winid, scope, key), populated at set-time and replayed/del'd at teardown.
Then route every keymap set on a real user buffer through set_tab_keymap, fixing three bypass classes:
view/keymaps.lua lines 647/650/653/677 — direct vim.keymap.set calls (hunk-staging + ih text-object). Also fixes the mode = "n" hard-code by iterating saved_maps (which carries the actual mode) in clear_tab_keymaps.
All of view/conflict/keymaps.lua — direct sets + active pcall(vim.keymap.del, "n", view_keymaps.diff_get, …) (which currently obliterates the user's prior do/dp without saving).
Per-buffer restore inside open_in_prev_tab (view/keymaps.lua around line 263): when the user gfs out and close_on_open_in_prev_tab=false, pop and restore the escaped buffer's saved keymaps so the file lives outside the diff with the user's original bindings intact.
Extend the same registry pattern to window-local options (scrollbind, cursorbind, diff) for #254. The escape-on-gf hook is the same.
Items this addresses
Directly closes (refactor itself is the fix)
[c and ]c don't work after closing #289 — [c and ]c don't work after closing. Comment thread (@ibhagwan, @ruicsh) describes the exact missing save/restore plus the gf-without-close edge case.
Wrong highlight in 3-way merge conflict #387 — Wrong highlight in 3-way merge until focus changes. Symptom of out-of-order conflict-keymap setup; routing all conflict-mode keymap setup through the single chokepoint imposes a uniform attach order.
Latent bugs the refactor prevents (no issue filed yet)
view/keymaps.lua:677 — hunk_textobject ih registered in {o, x} modes is never cleaned up because clear_tab_keymaps:479 hard-codes mode = "n". Even codediff's own mapping leaks.
conflict/keymaps.lua:31,34 actively dels the user's prior do/dp mappings without saving them — every conflict-mode session permanently breaks any user do/dp mapping.
Any future bug report about <leader>h* (gitsigns), [h/]h (mini.diff), ]x/[x (vim-unimpaired), etc. — all currently latent.
Suggested PR sequencing
Add the save/restore helper (snapshot_keymap, restore_keymap) on set_tab_keymap chokepoint. Behavior-neutral by itself.
Migrate the four bypass sites in view/keymaps.lua to route through set_tab_keymap.
Migrate all of conflict/keymaps.lua similarly.
Fix the mode = "n" hard-code by iterating saved_maps.
Add per-buffer restore in open_in_prev_tab (closes the gf-without-close gap).
Regression test: a gitsigns-style stub pre-sets [c/]c buffer-local, runs :CodeDiff, closes, asserts vim.fn.maparg(...) post-close matches pre-open. Same shape for <C-w>d / gf escape paths.
Total target: ~150 LOC, 1 day. Single PR is acceptable; the work is one coherent design.
Acceptance criteria
Zero direct vim.keymap.set / nvim_buf_set_keymap calls on original_bufnr / modified_bufnr outside of set_tab_keymap
clear_tab_keymaps iterates saved_maps (covers all modes, not just n); ih text-object is correctly torn down
open_in_prev_tab with close_on_open_in_prev_tab=false leaves the escaped buffer with the user's original mappings (or no buffer-local mapping if they had none)
Window-local scrollbind / cursorbind are restored on the same lifecycle hooks
New tests/ui/keymap_lifecycle_spec.lua asserts pre/post symmetry across :tabclose, q, gf, <C-w>d, and :CodeDiff re-invocation paths
These four argue that this refactor and #392 should co-land in the same milestone. The shared design (per-buffer/per-window save-restore registry, escape hooks for gf/<C-w>d) means doing them together is cheaper than the sum of the parts.
References
Full audit: keymap-289 agent transcript (2026-05-16)
Summary
codediff sets ~28 buffer-local keymaps on user file buffers (
original_bufnr+modified_bufnrin WORKING mode) and ~3 window-local options (scrollbind,cursorbind,diff) without ever saving the pre-existing user/plugin state and without restoring it on teardown. When a CodeDiff session ends — or when a buffer escapes the session viagf/<C-w>dwithout ending it — the user is left with codediff's overrides nailed to their buffer until they restart Neovim.This is a tracking issue for the lifecycle-cleanup refactor that fixes the class. It collects every reported symptom under one umbrella so they can close via a single coherent fix.
The mechanism (current code)
lua/codediff/ui/lifecycle/accessors.lua:442set_tab_keymapclear_tab_keymapsline 466lua/codediff/ui/view/keymaps.lua:647,650,653,677(bypass the chokepoint)ihleaks because line 479 hard-codesmode = "n"do/dpoverrideslua/codediff/ui/view/conflict/keymaps.lua:31..140(bypass the chokepoint)scrollbind/cursorbindwindow optionslua/codediff/ui/view/side_by_side.luagfor<C-w>dGrep confirms zero occurrences of
maparg,mapset,keymap_save,saved_keymap,original_mapacross the entire tree. No prior attempt at save-and-restore exists.The
gf(open-in-previous-tab) flow with the defaultclose_on_open_in_prev_tab = falseis the most insidious case: the diff session stays alive AND the file buffer now appears in another tab still wearing codediff's[c/]c(etc.) bindings. No teardown event ever fires for that escape until the whole session ends.Proposed fix
Single per-session registry of overridden state, keyed by
(bufnr | winid, scope, key), populated at set-time and replayed/del'd at teardown.Then route every keymap set on a real user buffer through
set_tab_keymap, fixing three bypass classes:view/keymaps.lualines 647/650/653/677 — directvim.keymap.setcalls (hunk-staging +ihtext-object). Also fixes themode = "n"hard-code by iteratingsaved_maps(which carries the actual mode) inclear_tab_keymaps.view/conflict/keymaps.lua— direct sets + activepcall(vim.keymap.del, "n", view_keymaps.diff_get, …)(which currently obliterates the user's priordo/dpwithout saving).open_in_prev_tab(view/keymaps.luaaround line 263): when the usergfs out andclose_on_open_in_prev_tab=false, pop and restore the escaped buffer's saved keymaps so the file lives outside the diff with the user's original bindings intact.Extend the same registry pattern to window-local options (
scrollbind,cursorbind,diff) for #254. The escape-on-gfhook is the same.Items this addresses
Directly closes (refactor itself is the fix)
[cand]cdon't work after closing. Comment thread (@ibhagwan,@ruicsh) describes the exact missing save/restore plus thegf-without-close edge case.next_hunkkeymaps permanently override existing keymaps. Strict subset of [c and ]c don't work after closing #289; same root cause.Likely closes (same root cause, currently masked)
scrollbindpersists when buffer opened in new tab via<C-w>d. Same family of lifecycle gap (window-local-option leak); the per-buffer/per-window escape hook this issue forces us to design covers it.Partial — refactor unblocks but doesn't fully fix
git_root, fugitive auto-refresh) are unrelated and belong with [Architecture] Unify path handling: separate repo-relative vs absolute forms in SessionConfig #392.gfopens file in wrong tab in explorer. Primary fix is wiringopen_in_prev_tabto explorer focus, but the per-buffer restore hook this refactor adds is the right place to layer that wiring.set_tab_keymap.Latent bugs the refactor prevents (no issue filed yet)
view/keymaps.lua:677—hunk_textobject ihregistered in{o, x}modes is never cleaned up becauseclear_tab_keymaps:479hard-codesmode = "n". Even codediff's own mapping leaks.conflict/keymaps.lua:31,34activelydels the user's priordo/dpmappings without saving them — every conflict-mode session permanently breaks any userdo/dpmapping.<leader>h*(gitsigns),[h/]h(mini.diff),]x/[x(vim-unimpaired), etc. — all currently latent.Suggested PR sequencing
snapshot_keymap,restore_keymap) onset_tab_keymapchokepoint. Behavior-neutral by itself.view/keymaps.luato route throughset_tab_keymap.conflict/keymaps.luasimilarly.mode = "n"hard-code by iteratingsaved_maps.open_in_prev_tab(closes thegf-without-close gap).scrollbind,cursorbind); closes [Bug] scroll mirroring buffer option persists when opening buffer in new tab #254.[c/]cbuffer-local, runs:CodeDiff, closes, assertsvim.fn.maparg(...)post-close matches pre-open. Same shape for<C-w>d/gfescape paths.Total target: ~150 LOC, 1 day. Single PR is acceptable; the work is one coherent design.
Acceptance criteria
vim.keymap.set/nvim_buf_set_keymapcalls onoriginal_bufnr/modified_bufnroutside ofset_tab_keymapclear_tab_keymapsiteratessaved_maps(covers all modes, not justn);ihtext-object is correctly torn downopen_in_prev_tabwithclose_on_open_in_prev_tab=falseleaves the escaped buffer with the user's original mappings (or no buffer-local mapping if they had none)scrollbind/cursorbindare restored on the same lifecycle hookstests/ui/keymap_lifecycle_spec.luaasserts pre/post symmetry across:tabclose,q,gf,<C-w>d, and:CodeDiffre-invocation pathsCross-cluster items also touched by #392 (path-handling refactor)
git_rootdetection for oil.nvim/fugitivegfescapeThese four argue that this refactor and #392 should co-land in the same milestone. The shared design (per-buffer/per-window save-restore registry, escape hooks for
gf/<C-w>d) means doing them together is cheaper than the sum of the parts.References
keymap-289agent transcript (2026-05-16)backlog-vs-keymapagent transcript (2026-05-16) — 2 closed, 2 likely, 3 partial, 80 unrelated