diff --git a/KEYBINDS.md b/KEYBINDS.md index 7162709..08cf66a 100644 --- a/KEYBINDS.md +++ b/KEYBINDS.md @@ -1,28 +1,30 @@ # Neovim Keybinds & Commands Cheatsheet -## LSP (built-in, Neovim 0.11+) +## LSP | Key | Action | |-----|--------| -| `grn` | Rename symbol | -| `grr` | Go to references | -| `gra` | Code action | -| `gri` | Go to implementation | -| `gO` | Document symbols | -| `gd` | Go to definition | +| `gd` | Go to definition (reuses existing split) | | `gD` | Go to declaration | +| `gi` | Go to implementation | +| `gr` | Go to references | +| `gt` | Go to type definition | | `K` | Hover documentation | -| `` (insert) | Signature help | +| `` | Signature help (normal + insert) | +| `ca` | Code action | +| `rn` | Rename symbol | +| `f` | Format file | -## Diagnostics (built-in) +## Diagnostics -| Key / Command | Action | -|---------------|--------| +| Key | Action | +|-----|--------| | `[d` | Previous diagnostic | | `]d` | Next diagnostic | -| `d` | Open diagnostic float | -| `:lua vim.diagnostic.setqflist()` | All diagnostics to quickfix | -| `:lua vim.diagnostic.setloclist()` | Buffer diagnostics to loclist | +| `gl` | Open diagnostic float | +| `q` | Buffer diagnostics to loclist | + +Go files auto-format and organize imports on save. ## Autocompletion (blink.cmp) @@ -38,29 +40,58 @@ Sources: LSP, buffer words, file paths. -## Buffer Navigation +## Pane Navigation | Key | Action | |-----|--------| -| `` | Next buffer | -| `` | Previous buffer | -| `x` | Close current buffer | +| `` | Move left (or prev terminal tab in popup) | +| `` | Move right (or next terminal tab in popup) | +| `` | Move down | +| `` | Move up | -Each split window shows its own filename bar (winbar) at the top. +Navigation wraps around (left from leftmost goes to rightmost). +Alt+h/l switch terminal tabs when the popup terminal is open. + +## Buffer Tabs (per-split) + +| Key | Action | +|-----|--------| +| `` | Next buffer in current split | +| `` | Previous buffer in current split | +| `x` | Close current buffer | +| `:q` | Close current buffer (keeps split) | +| `:wq` | Save and close current buffer | + +Each file belongs to one split only. Selecting an already-open file from oil focuses its split. ## File Explorer (oil.nvim) | Key / Command | Action | |---------------|--------| | `e` | Toggle file tree (left sidebar) | -| `-` or `` | Go up one directory (in file tree) | -| `` | Open file in last used code window (auto-focuses there) | +| `-` or `` | Go up one directory | +| `` | Open file in last used code split | +| `d` | Delete file/directory under cursor | | `g.` | Toggle hidden files | | `` | Preview file | | `` | Close oil | -| `:w` (in oil buffer) | Save filesystem changes (renames, deletes, etc.) | +| `:w` (in oil buffer) | Save filesystem changes (renames, moves, etc.) | -Edit filenames directly in the buffer, then `:w` to apply. +File tree opens automatically on startup. Edit filenames directly in the buffer, then `:w` to apply. + +## Popup Terminal + +| Key | Action | +|-----|--------| +| `` | Toggle popup terminal | +| `` | Previous terminal tab | +| `` | Next terminal tab | +| `` | New terminal tab | +| `` | Close terminal tab | +| `` | Paste from clipboard | +| `` | Exit to normal mode | + +Terminal session persists when hidden. Tab indicator in title: `Terminal [1] 2 3`. ## Task Runner (overseer.nvim) @@ -74,7 +105,7 @@ Edit filenames directly in the buffer, then `:w` to apply. | `:OverseerTaskAction` | Pick a task, then an action | | `:OverseerClearCache` | Clear cached tasks | -Tasks integrate with quickfix: errors from tasks populate the quickfix list. +Tasks integrate with quickfix: errors populate the quickfix list. ## Mason (LSP server management) @@ -84,7 +115,6 @@ Tasks integrate with quickfix: errors from tasks populate the quickfix list. | `:MasonInstall ` | Install a server | | `:MasonUninstall ` | Remove a server | | `:MasonUpdate` | Update all servers | -| `:MasonLog` | View install logs | Auto-installed servers: `gopls`, `bashls`, `ansiblels`, `yamlls`. @@ -106,35 +136,21 @@ Syntax highlighting is automatic once a parser is installed. | `:cclose` | Close quickfix window | | `:cnext` / `]q` | Next quickfix item | | `:cprev` / `[q` | Previous quickfix item | -| `:cfirst` | First item | -| `:clast` | Last item | -## Window Resizing (built-in) +## Window Management | Key | Action | |-----|--------| -| `>` | Wider (right) | -| `<` | Narrower (left) | +| `v` | Vertical split | +| `s` | Horizontal split | +| `>` | Wider | +| `<` | Narrower | | `+` | Taller | | `-` | Shorter | | `=` | Equal size all | | `10>` | Wider by 10 columns | -## General (built-in) - -| Key | Action | -|-----|--------| -| `s` | Split horizontal | -| `v` | Split vertical | -| `` | Toggle floating popup terminal | -| `` | Move between panes (also works from terminal) | -| `h/j/k/l` | Move between panes (default) | -| `:e ` | Open file | -| `u` | Undo | -| `` | Redo | -| `.` | Repeat last change | -| `*` | Search word under cursor | -| `:noh` | Clear search highlight | +Splitting from oil auto-redirects to the code area. ## Sessions / Layout @@ -143,7 +159,7 @@ Syntax highlighting is automatic once a parser is installed. | `ss` | Save current layout/session | | `sl` | Load saved layout/session | -Saves window positions, sizes, buffers, and current directory. +Saves window positions, sizes, buffers, terminals, and current directory. ## Statusline diff --git a/lua/core/autocmds.lua b/lua/core/autocmds.lua index ffad7ac..923e9e9 100644 --- a/lua/core/autocmds.lua +++ b/lua/core/autocmds.lua @@ -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", "", function() cycle_win_buf(1) end, { desc = "Next buffer tab" }) -vim.keymap.set("n", "", function() cycle_win_buf(-1) end, { desc = "Previous buffer tab" }) +vim.keymap.set("n", "", function() cycle_buf(1) end, { desc = "Next buffer tab" }) +vim.keymap.set("n", "", function() cycle_buf(-1) end, { desc = "Previous buffer tab" }) vim.keymap.set("n", "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", "x", function() end, { desc = "Close buffer tab" }) --- Splits duplicate current file (default Vim behavior) -vim.keymap.set("n", "v", ":vsplit", { desc = "Vertical split" }) -vim.keymap.set("n", "s", ":split", { 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", "", function() wrap_move("h") end, { desc = "Move to left pane (wrap)" }) -vim.keymap.set("n", "", function() wrap_move("l") end, { desc = "Move to right pane (wrap)" }) -vim.keymap.set("n", "", function() wrap_move("j") end, { desc = "Move to pane below (wrap)" }) -vim.keymap.set("n", "", function() wrap_move("k") end, { desc = "Move to pane above (wrap)" }) -vim.keymap.set("t", "", function() vim.cmd("stopinsert") wrap_move("h") end, { desc = "Move to left pane (wrap)" }) -vim.keymap.set("t", "", function() vim.cmd("stopinsert") wrap_move("l") end, { desc = "Move to right pane (wrap)" }) -vim.keymap.set("t", "", function() vim.cmd("stopinsert") wrap_move("j") end, { desc = "Move to pane below (wrap)" }) -vim.keymap.set("t", "", 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 ) 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", "", toggle_popup_term, { desc = "Toggle popup terminal" }) vim.keymap.set("t", "", toggle_popup_term, { desc = "Toggle popup terminal" }) -vim.keymap.set("t", "", function() switch_term(popup_term.current % #popup_term.bufs + 1) end, { desc = "Next terminal tab" }) -vim.keymap.set("t", "", function() switch_term((popup_term.current - 2) % #popup_term.bufs + 1) end, { desc = "Prev terminal tab" }) vim.keymap.set("t", "", new_term_tab, { desc = "New terminal tab" }) vim.keymap.set("t", "", 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", "", function() wrap_move("h") end, { desc = "Move to left pane (wrap)" }) +vim.keymap.set("n", "", function() wrap_move("l") end, { desc = "Move to right pane (wrap)" }) +vim.keymap.set("n", "", function() wrap_move("j") end, { desc = "Move to pane below (wrap)" }) +vim.keymap.set("n", "", 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", "", 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", "", 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", "", "") +vim.keymap.set("t", "", "") + +-- 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", "", function() local reg = vim.fn.getreg("+") @@ -182,7 +228,17 @@ vim.keymap.set("t", "", function() end end, { desc = "Paste from clipboard" }) vim.keymap.set("t", "", [[V]], { desc = "Select current line (normal mode)" }) -vim.keymap.set("t", "", [[]], { 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" diff --git a/lua/core/options.lua b/lua/core/options.lua index 6ded463..b47c5de 100644 --- a/lua/core/options.lua +++ b/lua/core/options.lua @@ -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) diff --git a/lua/plugins/oil.lua b/lua/plugins/oil.lua index 9ef6f38..13d6634 100644 --- a/lua/plugins/oil.lua +++ b/lua/plugins/oil.lua @@ -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 diff --git a/session.vim b/session.vim deleted file mode 100644 index 6534834..0000000 --- a/session.vim +++ /dev/null @@ -1,95 +0,0 @@ -let SessionLoad = 1 -let s:so_save = &g:so | let s:siso_save = &g:siso | setg so=0 siso=0 | setl so=-1 siso=-1 -let v:this_session=expand(":p") -doautoall SessionLoadPre -silent only -cd ~ -if expand('%') == '' && !&modified && line('$') <= 1 && getline(1) == '' - let s:wipebuf = bufnr('%') -endif -let s:shortmess_save = &shortmess -set shortmess+=aoO -badd +1 ~/Projects/Referit-Monitoring/devops/azure-pipeline.yml -badd +1 ~/Projects/Referit-Monitoring/devops/jobs/deploy-bicep.yml -badd +1 ~/Projects/Referit-Monitoring/devops/jobs/detect-changes.yml -argglobal -%argdel -edit ~/Projects/Referit-Monitoring/devops/jobs/detect-changes.yml -let s:save_splitbelow = &splitbelow -let s:save_splitright = &splitright -set splitbelow splitright -wincmd _ | wincmd | -vsplit -1wincmd h -wincmd w -let &splitbelow = s:save_splitbelow -let &splitright = s:save_splitright -wincmd t -let s:save_winminheight = &winminheight -let s:save_winminwidth = &winminwidth -set winminheight=0 -set winheight=1 -set winminwidth=0 -set winwidth=1 -wincmd = -argglobal -balt ~/Projects/Referit-Monitoring/devops/jobs/deploy-bicep.yml -setlocal foldmethod=manual -setlocal foldexpr=0 -setlocal foldmarker={{{,}}} -setlocal foldignore=# -setlocal foldlevel=0 -setlocal foldminlines=1 -setlocal foldnestmax=20 -setlocal foldenable -silent! normal! zE -let &fdl = &fdl -let s:l = 56 - ((55 * winheight(0) + 36) / 73) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 56 -normal! 08| -wincmd w -argglobal -if bufexists(fnamemodify("~/Projects/Referit-Monitoring/devops/azure-pipeline.yml", ":p")) | buffer ~/Projects/Referit-Monitoring/devops/azure-pipeline.yml | else | edit ~/Projects/Referit-Monitoring/devops/azure-pipeline.yml | endif -if &buftype ==# 'terminal' - silent file ~/Projects/Referit-Monitoring/devops/azure-pipeline.yml -endif -balt ~/Projects/Referit-Monitoring/devops/jobs/detect-changes.yml -setlocal foldmethod=manual -setlocal foldexpr=0 -setlocal foldmarker={{{,}}} -setlocal foldignore=# -setlocal foldlevel=0 -setlocal foldminlines=1 -setlocal foldnestmax=20 -setlocal foldenable -silent! normal! zE -let &fdl = &fdl -let s:l = 1 - ((0 * winheight(0) + 36) / 73) -if s:l < 1 | let s:l = 1 | endif -keepjumps exe s:l -normal! zt -keepjumps 1 -normal! 0 -wincmd w -wincmd = -if exists('s:wipebuf') && len(win_findbuf(s:wipebuf)) == 0 && getbufvar(s:wipebuf, '&buftype') isnot# 'terminal' - silent exe 'bwipe ' . s:wipebuf -endif -unlet! s:wipebuf -set winheight=1 winwidth=20 -let &shortmess = s:shortmess_save -let &winminheight = s:save_winminheight -let &winminwidth = s:save_winminwidth -let s:sx = expand(":p:r")."x.vim" -if filereadable(s:sx) - exe "source " . fnameescape(s:sx) -endif -let &g:so = s:so_save | let &g:siso = s:siso_save -set hlsearch -nohlsearch -doautoall SessionLoadPost -unlet SessionLoad -" vim: set ft=vim :