initial config push

This commit is contained in:
Wesley van Tilburg
2026-03-09 16:07:08 +00:00
commit bb91392a21
13 changed files with 898 additions and 0 deletions

201
lua/core/autocmds.lua Normal file
View File

@@ -0,0 +1,201 @@
-- Highlight on yank
vim.api.nvim_create_autocmd("TextYankPost", {
group = vim.api.nvim_create_augroup("highlight_yank", { clear = true }),
callback = function()
vim.hl.on_yank()
end,
})
-- Buffer tab navigation (scoped to current window)
local function cycle_win_buf(dir)
local bufs = _G.get_win_bufs()
if #bufs <= 1 then return end
local current = vim.api.nvim_get_current_buf()
for i, buf in ipairs(bufs) do
if buf == current then
local next = bufs[((i - 1 + dir) % #bufs) + 1]
vim.api.nvim_set_current_buf(next)
return
end
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", "<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
vim.api.nvim_set_current_buf(buf)
switched = true
break
end
end
if not switched then
vim.cmd("enew")
vim.bo.bufhidden = "wipe"
end
vim.cmd("bdelete " .. current)
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" })
-- 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)" })
-- Floating popup terminal with tabs (toggle with <C-t>)
local popup_term = { bufs = {}, current = 1, win = nil }
local function ensure_term_buf(idx)
if not popup_term.bufs[idx] or not vim.api.nvim_buf_is_valid(popup_term.bufs[idx]) then
popup_term.bufs[idx] = vim.api.nvim_create_buf(false, true)
end
return popup_term.bufs[idx]
end
local function term_title()
local parts = {}
for i = 1, #popup_term.bufs do
if not vim.api.nvim_buf_is_valid(popup_term.bufs[i]) then break end
if i == popup_term.current then
parts[#parts + 1] = "[" .. i .. "]"
else
parts[#parts + 1] = " " .. i .. " "
end
end
return " Terminal " .. table.concat(parts) .. " "
end
local function open_popup_win(buf)
local width = math.floor(vim.o.columns * 0.8)
local height = math.floor(vim.o.lines * 0.75)
popup_term.win = vim.api.nvim_open_win(buf, true, {
relative = "editor",
width = width,
height = height,
col = math.floor((vim.o.columns - width) / 2),
row = math.floor((vim.o.lines - height) / 2) - 1,
style = "minimal",
border = "rounded",
title = term_title(),
title_pos = "center",
})
end
local function update_title()
if popup_term.win and vim.api.nvim_win_is_valid(popup_term.win) then
vim.api.nvim_win_set_config(popup_term.win, { title = term_title() })
end
end
local function switch_term(idx)
if idx < 1 or idx > #popup_term.bufs then return end
popup_term.current = idx
local buf = ensure_term_buf(idx)
if popup_term.win and vim.api.nvim_win_is_valid(popup_term.win) then
vim.api.nvim_win_set_buf(popup_term.win, buf)
update_title()
if vim.bo[buf].buftype == "terminal" then
vim.cmd.startinsert()
end
end
end
local function toggle_popup_term()
if popup_term.win and vim.api.nvim_win_is_valid(popup_term.win) then
vim.api.nvim_win_hide(popup_term.win)
popup_term.win = nil
return
end
local buf = ensure_term_buf(popup_term.current)
open_popup_win(buf)
if vim.bo[buf].buftype ~= "terminal" then
vim.cmd.terminal()
end
vim.cmd.startinsert()
end
local function new_term_tab()
local idx = #popup_term.bufs + 1
ensure_term_buf(idx)
popup_term.current = idx
if popup_term.win and vim.api.nvim_win_is_valid(popup_term.win) then
vim.api.nvim_win_set_buf(popup_term.win, popup_term.bufs[idx])
update_title()
vim.cmd.terminal()
vim.cmd.startinsert()
end
end
local function close_term_tab()
if #popup_term.bufs <= 1 then return end
local buf = popup_term.bufs[popup_term.current]
table.remove(popup_term.bufs, popup_term.current)
if popup_term.current > #popup_term.bufs then
popup_term.current = #popup_term.bufs
end
switch_term(popup_term.current)
if vim.api.nvim_buf_is_valid(buf) then
vim.api.nvim_buf_delete(buf, { force = true })
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" })
-- Terminal copy/paste
vim.keymap.set("t", "<C-v>", function()
local reg = vim.fn.getreg("+")
if reg ~= "" then
vim.api.nvim_paste(reg, true, -1)
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" })
-- Save/restore layout with :mksession
vim.opt.sessionoptions = "buffers,curdir,folds,winpos,winsize,terminal"
vim.keymap.set("n", "<leader>ss", ":mksession! ~/.config/nvim/session.vim<CR>", { desc = "Save session/layout" })
vim.keymap.set("n", "<leader>sl", ":source ~/.config/nvim/session.vim<CR>", { desc = "Load session/layout" })
-- Trim trailing whitespace on save
vim.api.nvim_create_autocmd("BufWritePre", {
group = vim.api.nvim_create_augroup("trim_whitespace", { clear = true }),
pattern = "*",
callback = function()
local pos = vim.api.nvim_win_get_cursor(0)
vim.cmd([[%s/\s\+$//e]])
vim.api.nvim_win_set_cursor(0, pos)
end,
})

98
lua/core/options.lua Normal file
View File

@@ -0,0 +1,98 @@
local opt = vim.opt
-- Line numbers
opt.number = true
opt.relativenumber = false
-- Search
opt.ignorecase = true
opt.smartcase = true
opt.hlsearch = true
opt.incsearch = true
-- Tabs / indentation
opt.expandtab = true
opt.shiftwidth = 4
opt.tabstop = 4
opt.smartindent = true
-- Undo
opt.undofile = true
-- UI
opt.termguicolors = true
opt.signcolumn = "yes"
opt.scrolloff = 8
opt.splitbelow = true
opt.splitright = true
opt.equalalways = false
opt.cursorline = true
-- Per-window buffer tabs (winbar)
-- Track which buffers were opened in each window
_G.win_bufs = {}
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
end
table.insert(_G.win_bufs[win], buf)
end
function _G.get_win_bufs()
local win = 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
table.insert(bufs, buf)
end
end
_G.win_bufs[win] = bufs
return bufs
end
function _G.winbar_tabs()
local bufs = _G.get_win_bufs()
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#")
else
table.insert(parts, " " .. name .. mod .. " ")
end
end
return table.concat(parts, "|")
end
vim.api.nvim_create_autocmd("BufEnter", {
group = vim.api.nvim_create_augroup("track_win_bufs", { clear = true }),
callback = function() _G.track_buf() end,
})
opt.winbar = "%{%v:lua.winbar_tabs()%}"
-- Clipboard (system)
opt.clipboard = "unnamedplus"
-- Statusline (with git branch)
function _G.git_branch()
local branch = vim.fn.system("git -C " .. vim.fn.shellescape(vim.fn.expand("%:p:h")) .. " branch --show-current 2>/dev/null")
branch = branch:gsub("%s+", "")
if branch == "" then return "" end
return " " .. branch .. " |"
end
opt.statusline = " %f %m%r%=%{v:lua.git_branch()} %l:%c %p%% "
-- Misc
opt.updatetime = 250
opt.mouse = "a"

9
lua/core/plugins.lua Normal file
View File

@@ -0,0 +1,9 @@
vim.pack.add({
"https://github.com/rebelot/kanagawa.nvim",
"https://github.com/nvim-treesitter/nvim-treesitter",
"https://github.com/williamboman/mason.nvim",
"https://github.com/williamboman/mason-lspconfig.nvim",
{ src = "https://github.com/saghen/blink.cmp", version = vim.version.range("1.*") },
"https://github.com/stevearc/oil.nvim",
"https://github.com/stevearc/overseer.nvim",
})

6
lua/plugins/cmp.lua Normal file
View File

@@ -0,0 +1,6 @@
require("blink.cmp").setup({
keymap = { preset = "default" },
sources = {
default = { "lsp", "path", "buffer" },
},
})

133
lua/plugins/lsp.lua Normal file
View File

@@ -0,0 +1,133 @@
require("mason").setup()
-- Add mason bin to PATH so vim.lsp.enable can find installed servers
vim.env.PATH = vim.fn.stdpath("data") .. "/mason/bin:" .. vim.env.PATH
require("mason-lspconfig").setup({
ensure_installed = {
"gopls",
"bashls",
"ansiblels",
"yamlls",
},
})
vim.lsp.config("gopls", {
cmd = { "gopls" },
filetypes = { "go", "gomod", "gowork", "gotmpl" },
root_markers = { "go.mod", "go.work", ".git" },
settings = {
gopls = {
analyses = {
unusedparams = true,
shadow = true,
nilness = true,
unusedwrite = true,
useany = true,
},
staticcheck = true,
gofumpt = true,
},
},
})
vim.lsp.config("bashls", {
cmd = { "bash-language-server", "start" },
filetypes = { "sh", "bash" },
})
vim.lsp.config("ansiblels", {
cmd = { "ansible-language-server", "--stdio" },
filetypes = { "yaml.ansible" },
root_markers = { "ansible.cfg", ".ansible-lint", "playbooks" },
})
vim.lsp.config("yamlls", {
cmd = { "yaml-language-server", "--stdio" },
filetypes = { "yaml", "yaml.docker-compose" },
})
vim.lsp.enable({ "gopls", "bashls", "ansiblels", "yamlls" })
-- Diagnostics UI
vim.diagnostic.config({
virtual_text = true,
signs = true,
underline = true,
update_in_insert = false,
float = { border = "rounded" },
})
-- LSP keymaps (set when an LSP attaches)
vim.api.nvim_create_autocmd("LspAttach", {
group = vim.api.nvim_create_augroup("lsp_keymaps", { clear = true }),
callback = function(ev)
local opts = { buffer = ev.buf }
-- Jump to definition, reusing an existing split if the file is already open
local function goto_definition_reuse()
local params = vim.lsp.util.make_position_params(0, "utf-8")
vim.lsp.buf_request(0, "textDocument/definition", params, function(err, result)
if err or not result or vim.tbl_isempty(result) then return end
local target = result[1] or result
local uri = target.uri or target.targetUri
local range = target.range or target.targetSelectionRange
local bufnr = vim.uri_to_bufnr(uri)
-- Check if file is already visible in a window
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
if vim.api.nvim_win_get_buf(win) == bufnr then
vim.api.nvim_set_current_win(win)
local pos = range.start
vim.api.nvim_win_set_cursor(win, { pos.line + 1, pos.character })
return
end
end
-- Not open anywhere, use default behavior
vim.lsp.util.show_document(target, "utf-8", { focus = true })
end)
end
-- Navigation
vim.keymap.set("n", "gd", goto_definition_reuse, opts)
vim.keymap.set("n", "gD", vim.lsp.buf.declaration, opts)
vim.keymap.set("n", "gi", vim.lsp.buf.implementation, opts)
vim.keymap.set("n", "gr", vim.lsp.buf.references, opts)
vim.keymap.set("n", "gt", vim.lsp.buf.type_definition, opts)
-- Info
vim.keymap.set("n", "K", vim.lsp.buf.hover, opts)
vim.keymap.set("n", "<C-k>", vim.lsp.buf.signature_help, opts)
vim.keymap.set("i", "<C-k>", vim.lsp.buf.signature_help, opts)
-- Actions
vim.keymap.set("n", "<leader>ca", vim.lsp.buf.code_action, opts)
vim.keymap.set("n", "<leader>rn", vim.lsp.buf.rename, opts)
vim.keymap.set("n", "<leader>f", function() vim.lsp.buf.format({ async = true }) end, opts)
-- Diagnostics
vim.keymap.set("n", "[d", vim.diagnostic.goto_prev, opts)
vim.keymap.set("n", "]d", vim.diagnostic.goto_next, opts)
vim.keymap.set("n", "<leader>q", vim.diagnostic.setloclist, opts)
vim.keymap.set("n", "gl", vim.diagnostic.open_float, opts)
end,
})
-- Auto-format and organize imports on save for Go
vim.api.nvim_create_autocmd("BufWritePre", {
group = vim.api.nvim_create_augroup("go_organize_imports", { clear = true }),
pattern = "*.go",
callback = function()
-- Organize imports via code action
local params = vim.lsp.util.make_range_params(0, "utf-8")
params.context = { only = { "source.organizeImports" } }
local result = vim.lsp.buf_request_sync(0, "textDocument/codeAction", params, 1000)
for _, res in pairs(result or {}) do
for _, action in pairs(res.result or {}) do
if action.edit then
vim.lsp.util.apply_workspace_edit(action.edit, "utf-8")
elseif action.command then
vim.lsp.buf.execute_command(action.command)
end
end
end
-- Format
vim.lsp.buf.format({ async = false })
end,
})

150
lua/plugins/oil.lua Normal file
View File

@@ -0,0 +1,150 @@
-- Track the last focused code window (not oil, not terminal)
_G.last_code_win = nil
vim.api.nvim_create_autocmd("WinEnter", {
group = vim.api.nvim_create_augroup("track_code_win", { clear = true }),
callback = function()
local buf = vim.api.nvim_get_current_buf()
local ft = vim.bo[buf].filetype
local bt = vim.bo[buf].buftype
if ft ~= "oil" and bt ~= "terminal" then
_G.last_code_win = vim.api.nvim_get_current_win()
end
end,
})
require("oil").setup({
watch_for_changes = true,
keymaps = {
["<CR>"] = false, -- disable default, we override below
["<C-t>"] = false, -- don't override global <C-t> (popup terminal)
},
})
-- Override <CR> in oil buffers to open in last code window
vim.api.nvim_create_autocmd("FileType", {
group = vim.api.nvim_create_augroup("oil_open_in_code", { clear = true }),
pattern = "oil",
callback = function(ev)
-- Go up a directory with backspace or -
vim.keymap.set("n", "<BS>", function()
require("oil").open(require("oil").get_current_dir() .. "..")
end, { buffer = ev.buf, desc = "Go up directory" })
vim.keymap.set("n", "-", function()
require("oil").open(require("oil").get_current_dir() .. "..")
end, { buffer = ev.buf, desc = "Go up directory" })
-- Delete file under cursor
vim.keymap.set("n", "<leader>d", function()
local oil = require("oil")
local entry = oil.get_cursor_entry()
if not entry then return end
local dir = oil.get_current_dir()
if not dir then return end
local path = dir .. entry.name
vim.ui.input({ prompt = "Delete " .. entry.name .. "? (y/n): " }, function(input)
if input == "y" then
vim.fn.delete(path, entry.type == "directory" and "rf" or "")
oil.discard_all_changes()
end
end)
end, { buffer = ev.buf, desc = "Delete file/dir" })
-- Open file in code window, then auto-focus back to code
vim.keymap.set("n", "<CR>", function()
local oil = require("oil")
local entry = oil.get_cursor_entry()
if not entry then return end
-- Directories: navigate normally in oil
if entry.type == "directory" then
oil.select()
return
end
local dir = oil.get_current_dir()
if not dir then return end
local filepath = dir .. entry.name
local target_win = nil
-- Try last focused code window
if _G.last_code_win and vim.api.nvim_win_is_valid(_G.last_code_win) then
local buf = vim.api.nvim_win_get_buf(_G.last_code_win)
if vim.bo[buf].filetype ~= "oil" and vim.bo[buf].buftype ~= "terminal" then
target_win = _G.last_code_win
end
end
-- Fallback: find any non-oil, non-terminal window
if not target_win then
local cur_win = vim.api.nvim_get_current_win()
for _, win in ipairs(vim.api.nvim_tabpage_list_wins(0)) do
if win ~= cur_win and vim.api.nvim_win_get_config(win).relative == "" then
local buf = vim.api.nvim_win_get_buf(win)
if vim.bo[buf].filetype ~= "oil" and vim.bo[buf].buftype ~= "terminal" then
target_win = win
break
end
end
end
end
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
and vim.api.nvim_buf_line_count(old_buf) <= 1
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 })
end
else
-- No code window exists: create a split to the right
vim.cmd("rightbelow vsplit " .. vim.fn.fnameescape(filepath))
end
end, { buffer = ev.buf, desc = "Open file in code window" })
end,
})
-- Toggle file tree on the left with <leader>e
_G.oil_tree_win = nil
local function open_tree()
vim.cmd("topleft vsplit | vertical resize 30")
_G.oil_tree_win = vim.api.nvim_get_current_win()
require("oil").open()
end
vim.keymap.set("n", "<leader>e", function()
if _G.oil_tree_win and vim.api.nvim_win_is_valid(_G.oil_tree_win) then
vim.api.nvim_win_close(_G.oil_tree_win, true)
_G.oil_tree_win = nil
else
open_tree()
end
end, { desc = "Toggle file tree" })
-- Open file tree on startup (deferred so oil is fully loaded)
vim.api.nvim_create_autocmd("VimEnter", {
group = vim.api.nvim_create_augroup("oil_startup", { clear = true }),
callback = function()
vim.schedule(function()
open_tree()
vim.cmd("wincmd l")
end)
end,
})
-- Keep oil tree pinned at 30 columns after layout changes
vim.api.nvim_create_autocmd("WinResized", {
group = vim.api.nvim_create_augroup("oil_pin_width", { clear = true }),
callback = function()
if _G.oil_tree_win and vim.api.nvim_win_is_valid(_G.oil_tree_win) then
vim.api.nvim_win_set_width(_G.oil_tree_win, 30)
end
end,
})

1
lua/plugins/overseer.lua Normal file
View File

@@ -0,0 +1 @@
require("overseer").setup()

5
lua/plugins/theme.lua Normal file
View File

@@ -0,0 +1,5 @@
require("kanagawa").setup({
theme = "wave",
})
vim.cmd.colorscheme("kanagawa")

View File

@@ -0,0 +1,3 @@
-- nvim-treesitter on 0.12+ only provides :TSInstall for parser management
-- Treesitter highlighting is enabled automatically when a parser is available
-- Run :TSInstall go bash yaml lua to install parsers