fix: multiple navigation fixes for splits/popup terminal

chore: update Keybinds.md

more navigation fixes (primarly disabling mouse when terminal is open)

rm session
This commit is contained in:
Wesley van Tilburg
2026-03-14 13:56:36 +01:00
committed by Wesley van Tilburg
parent bb91392a21
commit 6dfbb4ec22
5 changed files with 213 additions and 185 deletions

View File

@@ -6,8 +6,8 @@ vim.api.nvim_create_autocmd("TextYankPost", {
end,
})
-- Buffer tab navigation (scoped to current window)
local function cycle_win_buf(dir)
-- Buffer tab navigation (per-window, exclusive)
local function cycle_buf(dir)
local bufs = _G.get_win_bufs()
if #bufs <= 1 then return end
local current = vim.api.nvim_get_current_buf()
@@ -18,13 +18,13 @@ local function cycle_win_buf(dir)
return
end
end
if #bufs > 0 then vim.api.nvim_set_current_buf(bufs[1]) end
end
vim.keymap.set("n", "<Tab>", function() cycle_win_buf(1) end, { desc = "Next buffer tab" })
vim.keymap.set("n", "<S-Tab>", function() cycle_win_buf(-1) end, { desc = "Previous buffer tab" })
vim.keymap.set("n", "<Tab>", function() cycle_buf(1) end, { desc = "Next buffer tab" })
vim.keymap.set("n", "<S-Tab>", function() cycle_buf(-1) end, { desc = "Previous buffer tab" })
vim.keymap.set("n", "<leader>x", function()
local bufs = _G.get_win_bufs()
local current = vim.api.nvim_get_current_buf()
-- Switch to another buffer in this window, or create a blank one
local switched = false
for _, buf in ipairs(bufs) do
if buf ~= current then
@@ -41,29 +41,40 @@ vim.keymap.set("n", "<leader>x", function()
end, { desc = "Close buffer tab" })
-- Splits duplicate current file (default Vim behavior)
vim.keymap.set("n", "<C-w>v", ":vsplit<CR>", { desc = "Vertical split" })
vim.keymap.set("n", "<C-w>s", ":split<CR>", { desc = "Horizontal split" })
-- Prevent splitting from oil — redirect to last code window
vim.api.nvim_create_autocmd("WinNew", {
group = vim.api.nvim_create_augroup("no_split_oil", { clear = true }),
callback = function()
-- Skip floating windows (like popup terminal)
local new_win = vim.api.nvim_get_current_win()
if vim.api.nvim_win_get_config(new_win).relative ~= "" then return end
-- Move between panes with Alt+h/l, wraps around (works in normal and terminal mode)
local function wrap_move(dir)
local cur = vim.api.nvim_get_current_win()
vim.cmd("wincmd " .. dir)
if vim.api.nvim_get_current_win() == cur then
-- Didn't move, wrap around
local opposite = ({ h = "l", l = "h", j = "k", k = "j" })[dir]
-- Go to the far opposite end
vim.cmd("99wincmd " .. opposite)
end
end
vim.keymap.set("n", "<A-h>", function() wrap_move("h") end, { desc = "Move to left pane (wrap)" })
vim.keymap.set("n", "<A-l>", function() wrap_move("l") end, { desc = "Move to right pane (wrap)" })
vim.keymap.set("n", "<A-j>", function() wrap_move("j") end, { desc = "Move to pane below (wrap)" })
vim.keymap.set("n", "<A-k>", function() wrap_move("k") end, { desc = "Move to pane above (wrap)" })
vim.keymap.set("t", "<A-h>", function() vim.cmd("stopinsert") wrap_move("h") end, { desc = "Move to left pane (wrap)" })
vim.keymap.set("t", "<A-l>", function() vim.cmd("stopinsert") wrap_move("l") end, { desc = "Move to right pane (wrap)" })
vim.keymap.set("t", "<A-j>", function() vim.cmd("stopinsert") wrap_move("j") end, { desc = "Move to pane below (wrap)" })
vim.keymap.set("t", "<A-k>", function() vim.cmd("stopinsert") wrap_move("k") end, { desc = "Move to pane above (wrap)" })
local prev_win = vim.fn.win_getid(vim.fn.winnr("#"))
if not vim.api.nvim_win_is_valid(prev_win) then return end
local prev_buf = vim.api.nvim_win_get_buf(prev_win)
if vim.bo[prev_buf].filetype ~= "oil" then return end
-- A split was created from oil — close it and redo from code window
local new_buf = vim.api.nvim_get_current_buf()
vim.api.nvim_win_close(new_win, true)
local target = _G.last_code_win
if not target or not vim.api.nvim_win_is_valid(target) then
-- Find any non-oil window
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
local buf = vim.api.nvim_win_get_buf(win)
if vim.bo[buf].filetype ~= "oil" and vim.api.nvim_win_get_config(win).relative == "" then
target = win
break
end
end
end
if target and vim.api.nvim_win_is_valid(target) then
vim.api.nvim_set_current_win(target)
vim.cmd("vsplit")
end
end,
})
-- Floating popup terminal with tabs (toggle with <C-t>)
local popup_term = { bufs = {}, current = 1, win = nil }
@@ -164,16 +175,51 @@ local function close_term_tab()
end
end
-- Init first tab
ensure_term_buf(1)
vim.keymap.set("n", "<C-t>", toggle_popup_term, { desc = "Toggle popup terminal" })
vim.keymap.set("t", "<C-t>", toggle_popup_term, { desc = "Toggle popup terminal" })
vim.keymap.set("t", "<A-]>", function() switch_term(popup_term.current % #popup_term.bufs + 1) end, { desc = "Next terminal tab" })
vim.keymap.set("t", "<A-[>", function() switch_term((popup_term.current - 2) % #popup_term.bufs + 1) end, { desc = "Prev terminal tab" })
vim.keymap.set("t", "<C-n>", new_term_tab, { desc = "New terminal tab" })
vim.keymap.set("t", "<C-w>", close_term_tab, { desc = "Close terminal tab" })
-- Move between panes with Alt+h/l, wraps around (works in normal and terminal mode)
local function wrap_move(dir)
local cur = vim.api.nvim_get_current_win()
vim.cmd("wincmd " .. dir)
if vim.api.nvim_get_current_win() == cur then
local opposite = ({ h = "l", l = "h", j = "k", k = "j" })[dir]
vim.cmd("99wincmd " .. opposite)
end
end
vim.keymap.set("n", "<A-h>", function() wrap_move("h") end, { desc = "Move to left pane (wrap)" })
vim.keymap.set("n", "<A-l>", function() wrap_move("l") end, { desc = "Move to right pane (wrap)" })
vim.keymap.set("n", "<A-j>", function() wrap_move("j") end, { desc = "Move to pane below (wrap)" })
vim.keymap.set("n", "<A-k>", function() wrap_move("k") end, { desc = "Move to pane above (wrap)" })
-- Alt+h/l in terminal: only switch terminal tabs in popup, no pane navigation
vim.keymap.set("t", "<A-h>", function()
if popup_term.win and vim.api.nvim_win_is_valid(popup_term.win) then
switch_term((popup_term.current - 2) % #popup_term.bufs + 1)
end
end, { desc = "Prev term tab" })
vim.keymap.set("t", "<A-l>", function()
if popup_term.win and vim.api.nvim_win_is_valid(popup_term.win) then
switch_term(popup_term.current % #popup_term.bufs + 1)
end
end, { desc = "Next term tab" })
-- Block pane navigation in terminal mode
vim.keymap.set("t", "<A-j>", "<Nop>")
vim.keymap.set("t", "<A-k>", "<Nop>")
-- Disable mouse when terminal is focused, restore on leave
vim.api.nvim_create_autocmd("TermEnter", {
group = vim.api.nvim_create_augroup("term_no_mouse", { clear = true }),
callback = function() vim.o.mouse = "" end,
})
vim.api.nvim_create_autocmd("TermLeave", {
group = vim.api.nvim_create_augroup("term_restore_mouse", { clear = true }),
callback = function() vim.o.mouse = "a" end,
})
-- Terminal copy/paste
vim.keymap.set("t", "<C-v>", function()
local reg = vim.fn.getreg("+")
@@ -182,7 +228,17 @@ vim.keymap.set("t", "<C-v>", function()
end
end, { desc = "Paste from clipboard" })
vim.keymap.set("t", "<C-y>", [[<C-\><C-n>V]], { desc = "Select current line (normal mode)" })
vim.keymap.set("t", "<Esc>", [[<C-\><C-n>]], { desc = "Exit to normal mode" })
-- Auto-enter insert mode when switching to a terminal window
-- Uses WinEnter only (not BufEnter) so :wq from nested editors (e.g. git commit) works
vim.api.nvim_create_autocmd("WinEnter", {
group = vim.api.nvim_create_augroup("term_auto_insert", { clear = true }),
callback = function()
if vim.bo.buftype == "terminal" and vim.fn.mode() ~= "t" then
vim.cmd.startinsert()
end
end,
})
-- Save/restore layout with :mksession
vim.opt.sessionoptions = "buffers,curdir,folds,winpos,winsize,terminal"

View File

@@ -28,24 +28,42 @@ opt.splitright = true
opt.equalalways = false
opt.cursorline = true
-- Per-window buffer tabs (winbar)
-- Track which buffers were opened in each window
-- Per-window buffer tabs (exclusive: each file belongs to one split only)
_G.win_bufs = {}
-- Remove a buffer from all windows' tracking
function _G.untrack_buf_everywhere(buf)
for win, bufs in pairs(_G.win_bufs) do
for i, b in ipairs(bufs) do
if b == buf then
table.remove(bufs, i)
break
end
end
end
end
function _G.track_buf()
local win = vim.api.nvim_get_current_win()
local buf = vim.api.nvim_get_current_buf()
if not vim.bo[buf].buflisted or vim.bo[buf].filetype == "oil" then return end
if not _G.win_bufs[win] then _G.win_bufs[win] = {} end
-- Remove if already tracked, then add to end
for i, b in ipairs(_G.win_bufs[win]) do
if b == buf then table.remove(_G.win_bufs[win], i) break end
if not vim.bo[buf].buflisted
or vim.bo[buf].filetype == "oil"
or vim.bo[buf].buftype == "terminal"
or vim.api.nvim_buf_get_name(buf) == "" then
return
end
if not _G.win_bufs[win] then _G.win_bufs[win] = {} end
-- Already tracked in this window, skip
for _, b in ipairs(_G.win_bufs[win]) do
if b == buf then return end
end
-- Remove from any other window (exclusive ownership)
_G.untrack_buf_everywhere(buf)
table.insert(_G.win_bufs[win], buf)
end
function _G.get_win_bufs()
local win = vim.api.nvim_get_current_win()
function _G.get_win_bufs(win)
win = win or vim.api.nvim_get_current_win()
local bufs = {}
for _, buf in ipairs(_G.win_bufs[win] or {}) do
if vim.api.nvim_buf_is_valid(buf) and vim.bo[buf].buflisted then
@@ -57,13 +75,13 @@ function _G.get_win_bufs()
end
function _G.winbar_tabs()
local bufs = _G.get_win_bufs()
local win = vim.api.nvim_get_current_win()
local bufs = _G.get_win_bufs(win)
if #bufs == 0 then return "" end
local current = vim.api.nvim_get_current_buf()
local parts = {}
for _, buf in ipairs(bufs) do
local name = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), ":t")
if name == "" then name = "[No Name]" end
local mod = vim.bo[buf].modified and " +" or ""
if buf == current then
table.insert(parts, "%#TabLineSel# " .. name .. mod .. " %#WinBar#")
@@ -79,6 +97,15 @@ vim.api.nvim_create_autocmd("BufEnter", {
callback = function() _G.track_buf() end,
})
-- Clean up tracking when a window closes
vim.api.nvim_create_autocmd("WinClosed", {
group = vim.api.nvim_create_augroup("clean_win_bufs", { clear = true }),
callback = function(ev)
local win = tonumber(ev.match)
if win then _G.win_bufs[win] = nil end
end,
})
opt.winbar = "%{%v:lua.winbar_tabs()%}"
-- Clipboard (system)

View File

@@ -66,6 +66,22 @@ vim.api.nvim_create_autocmd("FileType", {
local dir = oil.get_current_dir()
if not dir then return end
local filepath = dir .. entry.name
local abs_path = vim.fn.fnamemodify(filepath, ":p")
-- Check if file is already tracked in any window's tab list
for win, bufs in pairs(_G.win_bufs or {}) do
if vim.api.nvim_win_is_valid(win) then
for _, buf in ipairs(bufs) do
if vim.api.nvim_buf_is_valid(buf)
and vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), ":p") == abs_path then
vim.api.nvim_set_current_win(win)
vim.api.nvim_set_current_buf(buf)
return
end
end
end
end
local target_win = nil
-- Try last focused code window
@@ -92,7 +108,6 @@ vim.api.nvim_create_autocmd("FileType", {
if target_win then
vim.api.nvim_set_current_win(target_win)
-- Remember blank buffer to clean up after opening
local old_buf = vim.api.nvim_get_current_buf()
local is_blank = vim.api.nvim_buf_get_name(old_buf) == ""
and not vim.bo[old_buf].modified
@@ -100,7 +115,16 @@ vim.api.nvim_create_autocmd("FileType", {
and vim.api.nvim_buf_get_lines(old_buf, 0, 1, false)[1] == ""
vim.cmd.edit(vim.fn.fnameescape(filepath))
if is_blank and old_buf ~= vim.api.nvim_get_current_buf() then
vim.api.nvim_buf_delete(old_buf, { force = true })
local in_use = false
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
if vim.api.nvim_win_get_buf(win) == old_buf then
in_use = true
break
end
end
if not in_use then
vim.api.nvim_buf_delete(old_buf, { force = true })
end
end
else
-- No code window exists: create a split to the right