------------------------------------------------------ ---- Runtime ----------------------------------------- ------------------------------------------------------ -- Ensure XDG environment variables are set vim.env.XDG_CONFIG_HOME = vim.env.XDG_CONFIG_HOME or vim.env.HOME .. "/.config" vim.env.XDG_CACHE_HOME = vim.env.XDG_CACHE_HOME or vim.env.HOME .. "/.cache" vim.env.XDG_DATA_HOME = vim.env.XDG_DATA_HOME or vim.env.HOME .. "/.local/share" -- Ensure the Neovim cache directory exists local nvim_cache_dir = vim.env.XDG_CACHE_HOME .. "/nvim" if vim.fn.isdirectory(nvim_cache_dir) == 0 then vim.fn.mkdir(nvim_cache_dir, "p") end -- Ensure necessary paths are in $PATH local paths = { "/usr/local/bin", "/opt/brew/bin", "/opt/homebrew/bin" } for _, path in ipairs(paths) do if not string.match(vim.env.PATH, path) then vim.env.PATH = path .. ":" .. vim.env.PATH end end -- Add node_modules/.bin/ to PATH for Node.js projects if not string.match(vim.env.PATH, "node_modules/%.bin") then vim.env.PATH = vim.env.PATH .. ":node_modules/.bin/" end -- Speed up startup by explicitly setting the Python interpreter local python_paths = { "/opt/brew/bin/python3", "/opt/homebrew/bin/python3", "/usr/local/bin/python3", "/usr/bin/python3", } for _, path in ipairs(python_paths) do if vim.fn.executable(path) == 1 then vim.g.python3_host_prog = path break end end -- Disable unused language providers (no Ruby/Perl remote plugins here). -- Skips provider probing at startup and quiets :checkhealth. vim.g.loaded_ruby_provider = 0 vim.g.loaded_perl_provider = 0 ------------------------------------------------------ ---- Basics ------------------------------------------ ------------------------------------------------------ -- colorscheme is set from -- ~/.local/share/nvim/colors.lua -- Set leader key vim.g.mapleader = " " -- Sensible defaults vim.opt.encoding = "utf-8" vim.opt.mouse = "a" -- Use mouse in terminal vim.opt.showcmd = true vim.opt.ruler = true -- Show cursor position vim.opt.number = true -- Show line numbers -- vim.opt.cursorline = true -- Highlight current line vim.opt.showmatch = true -- Highlight matching brackets/braces vim.opt.matchtime = 5 -- Duration to show matching brace (1/10 sec) -- vim.opt.shortmess:append("I") -- Disable intro message / splash screen vim.opt.splitbelow = true -- New horizontal splits go below vim.opt.splitright = true -- New vertical splits go right vim.opt.backspace = { "indent", "eol", "start" } -- Backspace works across lines vim.opt.hidden = true -- Allow changing buffers without saving vim.opt.errorbells = false vim.opt.autoread = true -- Auto read when file changes elsewhere vim.opt.autowrite = true -- Auto write before :next, :make, etc. vim.opt.cmdheight = 2 -- More space for messages vim.opt.scrolloff = 5 -- Start scrolling when within 5 lines near top/bottom vim.opt.laststatus = 2 -- Always display the status line vim.opt.termguicolors = true -- Use 24-bit colors in the terminal vim.opt.updatetime = 200 -- Snappier CursorHold + gitsigns (default 4000) vim.opt.timeoutlen = 300 -- Faster which-key / mapped-sequence timeout (default 1000) vim.o.winborder = "rounded" -- Consistent border on all floating windows (0.11+) vim.opt.splitkeep = "screen" -- Keep text visually stable when opening/closing splits -- Unified sign/number/fold/git gutter (snacks.statuscolumn, enabled in ui.lua) vim.o.statuscolumn = [[%!v:lua.require'snacks.statuscolumn'.get()]] -- Bash-like autocomplete for commands vim.opt.wildmode = { "list:longest", "list:full" } -- First tab completes longest, second cycles vim.opt.wildmenu = true -- Indentation vim.opt.expandtab = true vim.opt.tabstop = 4 vim.opt.softtabstop = 4 vim.opt.shiftwidth = 4 vim.opt.autoindent = true -- Invisibles vim.opt.listchars = { tab = "▸ ", eol = "¬", nbsp = "☠", trail = "⍁" } vim.opt.list = false -- Wrapping vim.opt.wrap = true vim.opt.linebreak = true vim.opt.showbreak = "…" vim.opt.formatoptions:remove("t") -- vim.opt.textwidth = 80 -- vim.opt.colorcolumn = "80" -- Autocomplete vim.opt.completeopt = { "longest", "preview", "menu" } vim.opt.dictionary:remove("/usr/share/dict/words") vim.opt.dictionary:append("/usr/share/dict/words") vim.opt.thesaurus:remove("/usr/share/dict/mthesaur") vim.opt.thesaurus:append("/usr/share/dict/mthesaur") -- Searching vim.opt.ignorecase = true vim.opt.smartcase = true vim.opt.incsearch = true vim.opt.hlsearch = true -- Folding vim.opt.foldmethod = "expr" vim.opt.foldexpr = "v:lua.vim.treesitter.foldexpr()" vim.opt.foldtext = "v:lua.vim.treesitter.foldtext()" vim.opt.foldnestmax = 4 vim.opt.foldminlines = 3 vim.opt.foldlevel = 5 vim.opt.foldtext = [[substitute(getline(v:foldstart),'\\t',repeat('\ ',&tabstop),'g').'…'.trim(getline(v:foldend)) . ' (#' . (v:foldend - v:foldstart + 1) . ' lines)']] vim.opt.fillchars = "fold: " -- Swap and Undo Directories local swapdir = os.getenv("XDG_CACHE_HOME") .. "/nvim/swap//" if vim.fn.isdirectory(swapdir) == 0 then vim.fn.mkdir(swapdir, "p") end vim.opt.swapfile = true vim.opt.directory = swapdir local undodir = os.getenv("XDG_CACHE_HOME") .. "/nvim/undo//" if vim.fn.isdirectory(undodir) == 0 then vim.fn.mkdir(undodir, "p") end vim.opt.undofile = true vim.opt.undodir = undodir -- File Search Path vim.opt.path = ".,,**" -- Disable netrw, we're using nvim-tree instead vim.g.loaded_netrw = 1 vim.g.loaded_netrwPlugin = 1 -- Set SHELL command if vim.fn.executable("/usr/local/bin/zsh") == 1 then vim.opt.shell = "/usr/local/bin/zsh -l" elseif vim.fn.executable("/usr/bin/zsh") == 1 then vim.opt.shell = "/usr/bin/zsh -l" end -- Terminal settings vim.g.terminal_scrollback_buffer_size = 10000 -- Store reasonable amount of terminal history -- Built-in :grep command if vim.fn.executable("rg") == 1 then vim.opt.grepprg = "rg --vimgrep --smart-case --no-heading" vim.opt.grepformat = "%f:%l:%c:%m,%f:%l:%m" elseif vim.fn.executable("ag") == 1 then vim.opt.grepprg = "ag --vimgrep --smart-case --noheading" vim.opt.grepformat = "%f:%l:%c:%m,%f:%l:%m" -- this has not been tested end ------------------------------------------------------ ---- Neovide (GUI) ----------------------------------- ------------------------------------------------------ if vim.g.neovide then -- Put anything you want to happen only in Neovide here -- vim.o.guifont = "LiterationMono Nerd Font Mono:14" -- vim.o.guifont = "SauceCodePro NF:h16" vim.o.guifont = "Agave Nerd Font Mono:h16" -- vim.o.guifont = "SFMono Nerd Font:h16" -- vim.o.guifont = "FiraCode Nerd Font:h16" vim.g.neovide_opacity = 0.8 vim.g.transparency = 1 vim.g.neovide_scale_factor = 1.0 local change_scale_factor = function(delta) vim.g.neovide_scale_factor = vim.g.neovide_scale_factor * delta end vim.keymap.set("n", "", function() change_scale_factor(1.05) end) vim.keymap.set("n", "", function() change_scale_factor(1 / 1.05) end) vim.keymap.set("n", "", function() change_scale_factor(1) end) vim.g.neovide_window_blurred = true vim.g.neovide_show_border = false vim.g.neovide_input_macos_option_key_is_meta = "only_left" -- vim.g.neovide_cursor_animation_length = 0.150 vim.g.neovide_cursor_animation_length = 0.025 vim.g.neovide_cursor_short_animation_length = 0.5 vim.g.neovide_cursor_trail_size = 0.2 vim.g.neovide_cursor_vfx_mode = "pixiedust" vim.keymap.set("n", "", function() vim.fn.jobstart("neovide --fork", { detach = true }) end) -- Open new instance vim.keymap.set("n", "", ":w") -- Save vim.keymap.set("v", "", '"+y') -- Copy vim.keymap.set("n", "", '"+P') -- Paste normal mode vim.keymap.set("v", "", '"+P') -- Paste visual mode vim.keymap.set("c", "", "+") -- Paste command mode vim.keymap.set("i", "", "+") -- Paste insert mode local dirs = { "/Volumes/Sense/src", "~/src" } for _, dir in ipairs(dirs) do if vim.fn.isdirectory(dir) == 1 then vim.cmd("cd " .. dir) break end end end ------------------------------------------------------ ---- lazy.nvim --------------------------------------- ------------------------------------------------------ -- this will load ~/.config/nvim/lua/config/lazy.lua -- Guarded so re-sourcing init.lua (via <) does not call lazy.setup() -- again; only the options/keymaps/autocmds below get re-applied. if not vim.g.lazy_bootstrapped then vim.g.lazy_bootstrapped = true require("config.lazy") end ------------------------------------------------------ ---- Terminal ---------------------------------------- ------------------------------------------------------ -- Terminal lives under ; so t is free for tests (neotest) vim.keymap.set("n", ";v", ":vsplit term://$SHELLi", { desc = "Terminal in vertical split" }) vim.keymap.set("n", ";s", ":belowright split term://$SHELLi", { desc = "Terminal in horizontal split" }) -- Toggleable, reusable terminal (snacks) — hide/show without re-spawning vim.keymap.set("n", ";t", function() Snacks.terminal.toggle() end, { desc = "Toggle terminal" }) vim.keymap.set("t", "", "", { desc = "Exit terminal mode" }) ------------------------------------------------------ ---- Running the current file ------------------------ ------------------------------------------------------ -- Run a command in a terminal, optionally in a split: split_mode = "v" | "h" | nil local function run_cur(cmd, split_mode) if split_mode == "v" then vim.cmd("vsplit") elseif split_mode == "h" then vim.cmd("split") end vim.cmd("terminal " .. cmd) end -- Determine the interpreter and run the current file local function run_cur_self(split_mode) vim.cmd("write") -- Save the file local filetype = vim.bo.filetype local interpreter if vim.fn.filereadable("run.sh") == 1 then interpreter = "bash run.sh" elseif vim.fn.filereadable("sh/run.sh") == 1 then interpreter = "bash sh/run.sh" elseif filetype == "sh" then interpreter = "bash" elseif filetype:match("javascript") then interpreter = "node" elseif filetype:match("scala") then interpreter = "scala" elseif filetype == "python" then interpreter = "pyexec" elseif filetype:match("lua") then interpreter = "lua" elseif filetype == "make" then run_cur("make", split_mode) -- or, if the file is executable, run it directly elseif vim.fn.executable(vim.fn.expand("%")) == 1 then interpreter = "" else print("No interpreter set for filetype=" .. filetype) return end -- Use absolute path and shell-escape the filename local file = vim.fn.shellescape(vim.fn.expand("%:p")) run_cur(interpreter .. " " .. file, split_mode) end vim.keymap.set("n", "nn", function() run_cur_self(nil) end, { desc = "Run current file" }) vim.keymap.set("n", "nv", function() run_cur_self("v") end, { desc = "Run current file (vertical split)" }) vim.keymap.set("n", "ns", function() run_cur_self("h") end, { desc = "Run current file (horizontal split)" }) ------------------------------------------------------ ---- Diagnostics ------------------------------------- ------------------------------------------------------ vim.diagnostic.config({ virtual_lines = { current_line = true }, -- full diagnostic inline, cursor line only virtual_text = false, signs = false, underline = true, update_in_insert = false, severity_sort = true, float = { border = "rounded", source = true, -- always show the source (was the deprecated "always") header = "", prefix = "", }, }) local function mapDiagnosticsToggle(key, diagnostic_option) vim.keymap.set("n", key, function() local current_config = vim.diagnostic.config() or {} local new_config = not current_config[diagnostic_option] vim.diagnostic.config({ [diagnostic_option] = new_config }) end, { desc = "Toggle diagnostic " .. diagnostic_option }) end mapDiagnosticsToggle("dl", "virtual_lines") mapDiagnosticsToggle("dt", "virtual_text") mapDiagnosticsToggle("ds", "signs") mapDiagnosticsToggle("du", "underline") vim.keymap.set("n", "dd", function() vim.diagnostic.enable(not vim.diagnostic.is_enabled()) end, { desc = "Toggle diagnostics" }) vim.keymap.set("n", "dq", function() vim.diagnostic.setqflist() end, { desc = "Show all diagnostics in quickfix" }) vim.keymap.set("n", "dh", function() vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled({})) end, { desc = "Toggle inlay hints" }) ------------------------------------------------------ ---- Productivity ------------------------------------ ------------------------------------------------------ vim.keymap.set("n", ",", ":e $MYVIMRC", { noremap = true, silent = true }) vim.keymap.set("n", "<", function() -- Re-applies options/keymaps/autocmds. The lazy bootstrap is guarded above, -- so this does not re-run lazy.setup(). For plugin changes use :Lazy reload -- or restart. dofile(vim.env.MYVIMRC) vim.notify("Core config reloaded", vim.log.levels.INFO) end, { desc = "Reload core config (options/keymaps/autocmds)" }) vim.keymap.set("n", "w", ":update", { desc = "Save file if modified" }) vim.keymap.set("n", "W", ":write", { desc = "Save file" }) vim.keymap.set("n", "\\", ":nohl", { desc = "Clear search highlights" }) -- Quickfix navigation vim.keymap.set("n", "]q", ":cnext", { desc = "Quickfix next" }) vim.keymap.set("n", "[q", ":cprevious", { desc = "Quickfix previous" }) vim.keymap.set("n", "[[", function() local qf_exists = false for _, win in pairs(vim.fn.getwininfo()) do if win["quickfix"] == 1 then qf_exists = true break end end if qf_exists then vim.cmd("cclose") else vim.cmd("copen") end end, { desc = "Quickfix toggle" }) vim.keymap.set("n", "[]", ":copen", { desc = "Quickfix open" }) vim.keymap.set("n", "][", ":cclose", { desc = "Quickfix close" }) -- Location list navigation vim.keymap.set("n", "]l", ":lnext", { noremap = true, silent = true, desc = "Location list next" }) vim.keymap.set("n", "[l", ":lprevious", { noremap = true, silent = true, desc = "Location list previous" }) -- Common typing mistakes vim.cmd([[cnoreabbrev E e]]) vim.cmd([[inoreabbrev requrie require]]) vim.cmd([[inoreabbrev lenght length]]) -- Copy current filename with line number vim.keymap.set( "n", "l", ":let @+ = expand('%') . ':' . line('.')", { noremap = true, silent = true } ) -- %% expands to dirname of current file vim.cmd([[cnoremap %% getcmdtype() == ':' ? expand('%:p:h').'/' : '%%']]) -- Redraw screen vim.keymap.set("n", "", "", { noremap = true, silent = true }) -- Disable distracting features in current buffer vim.keymap.set("n", "C", function() -- Disable gitsigns require("gitsigns").setup({ signcolumn = false }) -- Disable snacks.indent for current buffer vim.b.snacks_indent = false -- Disable line numbers and listchars vim.opt_local.number = false vim.opt_local.list = false vim.opt_local.showtabline = 0 -- Hide bufferline -- disable diagnostics vim.cmd("nohlsearch") -- cclear search highlight vim.diagnostic.enable(false) vim.opt_local.cursorline = false -- Disable cursorline require("lualine").hide() vim.notify("Disabled distractions in current buffer", vim.log.levels.INFO) end, { desc = "Disable distractions" }) ------------------------------------------------------ ---- Autocommands ------------------------------------ ------------------------------------------------------ local api = vim.api -- Primary autocmd group. clear=true means re-sourcing the config replaces these -- handlers instead of stacking duplicates. local szAutoGroup = api.nvim_create_augroup("szAutoGroup", { clear = true }) -- Trigger autoread when files change on disk api.nvim_create_autocmd({ "FocusGained", "BufEnter", "CursorHold", "CursorHoldI" }, { group = szAutoGroup, pattern = "*", command = "if mode() !~ '\\v(c|r.?|!|t)' && getcmdwintype() == '' | checktime | endif", }) -- Notification after file change api.nvim_create_autocmd("FileChangedShellPost", { group = szAutoGroup, pattern = "*", callback = function() vim.notify("File changed on disk. Buffer reloaded.", vim.log.levels.WARN) end, }) -- Terminal buffers: no line numbers, soft wrap api.nvim_create_autocmd("TermOpen", { group = szAutoGroup, pattern = "*", callback = function() vim.opt_local.number = false vim.opt_local.relativenumber = false vim.opt_local.wrap = true end, }) -- Project-specific keybindings local function project_has_file(filename) return vim.fn.filereadable(vim.fn.getcwd() .. "/" .. filename) == 1 end local function setup_project_keybindings() -- Clear any existing project keybindings pcall(vim.keymap.del, "n", "cT") -- TypeScript compilation: cT (requires tsconfig.json) if project_has_file("tsconfig.json") then vim.keymap.set("n", "cT", function() vim.opt.makeprg = "npx tsc --noEmit" vim.opt.errorformat = "%f(%l,%c): %trror TS%n: %m,%f(%l,%c): %tarning TS%n: %m" vim.cmd("make!") vim.cmd("copen") end, { desc = "TypeScript compile to quickfix" }) end end api.nvim_create_autocmd("VimEnter", { group = szAutoGroup, callback = setup_project_keybindings, }) api.nvim_create_autocmd("DirChanged", { group = szAutoGroup, callback = setup_project_keybindings, }) -- Filetype detection & settings api.nvim_create_autocmd({ "BufRead", "BufNewFile", "BufEnter" }, { group = szAutoGroup, pattern = "*/.kube/config", command = "setlocal nowrap filetype=yaml", }) api.nvim_create_autocmd({ "BufRead", "BufNewFile", "BufEnter" }, { group = szAutoGroup, pattern = "*gitconfig*,*git/config*", command = "setlocal filetype=gitconfig", }) api.nvim_create_autocmd("FileType", { group = szAutoGroup, pattern = "matlab", command = "setlocal commentstring=%\\ %s", }) api.nvim_create_autocmd("FileType", { group = szAutoGroup, pattern = "tf", command = "setlocal commentstring=#\\ %s", }) api.nvim_create_autocmd("FileType", { group = szAutoGroup, pattern = "tla", command = "setlocal commentstring=\\\\*\\ %s", }) api.nvim_create_autocmd("FileType", { group = szAutoGroup, pattern = "sql", command = "setlocal commentstring=--\\ %s", }) api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, { group = szAutoGroup, pattern = "*Dockerfile*", command = "set filetype=Dockerfile", }) api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, { group = szAutoGroup, pattern = "*Jenkinsfile*", command = "set filetype=groovy", }) api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, { group = szAutoGroup, pattern = "*.keymap", command = "set filetype=dts | setlocal commentstring=//\\ %s", }) api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, { group = szAutoGroup, pattern = "*.mjml", command = "set filetype=html", }) -- Allow comments in JSON files api.nvim_create_autocmd("FileType", { group = szAutoGroup, pattern = "json", command = [[syntax match Comment +\/\/.\+$+]], }) -- Insert console.log() in JS/TS files api.nvim_create_autocmd("FileType", { group = szAutoGroup, pattern = { "javascript", "typescript", "javascriptreact", "typescriptreact" }, command = "inoremap console.log()i", }) -- Restore cursor to file position from the previous editing session api.nvim_create_autocmd("BufReadPost", { group = szAutoGroup, callback = function(args) local mark = api.nvim_buf_get_mark(args.buf, '"') local line_count = api.nvim_buf_line_count(args.buf) if mark[1] > 0 and mark[1] <= line_count then api.nvim_win_set_cursor(0, mark) -- defer centering slightly so it's applied after render vim.schedule(function() vim.cmd("normal! zz") end) end end, }) -- QuickFix window behavior api.nvim_create_autocmd("QuickFixCmdPost", { group = szAutoGroup, pattern = "*grep*", command = "cwindow", }) -- Only allow a single tab when starting Neovim api.nvim_create_autocmd("VimEnter", { group = szAutoGroup, pattern = "*", command = "silent! tabonly", }) -- Open help in a vertical split api.nvim_create_autocmd("FileType", { group = szAutoGroup, pattern = "help", command = "wincmd L", }) -- Auto resize splits when the terminal window is resized api.nvim_create_autocmd("VimResized", { group = szAutoGroup, command = "wincmd =", }) -- Highlight yanked text api.nvim_create_autocmd("TextYankPost", { group = api.nvim_create_augroup("highlight_yank", { clear = true }), pattern = "*", desc = "highlight selection on yank", callback = function() vim.hl.on_yank({ timeout = 200, visual = true }) end, }) -- Swapfile recovery api.nvim_create_autocmd("SwapExists", { group = szAutoGroup, pattern = "*", command = "let b:swapchoice = '?' | let b:swapname = v:swapname", }) api.nvim_create_autocmd("BufReadPost", { group = szAutoGroup, pattern = "*", command = "let b:swapchoice_likely = (&l:ro ? 'o' : 'r')", }) api.nvim_create_autocmd("BufEnter", { group = szAutoGroup, pattern = "*", command = "let b:swapchoice_likely = (&l:ro ? 'o' : 'e')", }) api.nvim_create_autocmd("BufWinEnter", { group = szAutoGroup, pattern = "*", command = [[ if exists('b:swapchoice') && exists('b:swapchoice_likely') | let b:swapchoice = b:swapchoice_likely | unlet b:swapchoice_likely | endif ]], }) -- Open a diff against the original file version local function diff_orig() vim.cmd("vert new") vim.opt_local.buftype = "nofile" vim.cmd("r #") vim.cmd("0d_") vim.cmd("diffthis") vim.cmd("wincmd p") vim.cmd("diffthis") end -- Handle swap file recovery local function handle_recover() vim.fn.system( "diff - " .. vim.fn.shellescape(vim.fn.expand("%:p")), table.concat(vim.fn.getline(1, "$") --[[@as string[] ]], "\n") .. "\n" ) if vim.v.shell_error ~= 0 then diff_orig() else vim.fn.delete(vim.b.swapname) vim.notify("No differences; deleting the old swap file.", vim.log.levels.WARN) api.nvim_feedkeys(" ", "n", false) end end api.nvim_create_autocmd("BufWinEnter", { group = szAutoGroup, pattern = "*", callback = function() if vim.b.swapchoice == "r" then handle_recover() end end, }) -- Show cursorline only in the active window local active_cursorline = api.nvim_create_augroup("active_cursorline", { clear = true }) api.nvim_create_autocmd({ "WinEnter", "BufEnter" }, { group = active_cursorline, callback = function() vim.opt_local.cursorline = true end, }) api.nvim_create_autocmd({ "WinLeave", "BufLeave" }, { group = active_cursorline, callback = function() vim.opt_local.cursorline = false end, }) -- LSP reference highlighting under the cursor is handled by snacks.words -- (configured in lua/plugins/ui.lua), which uses CursorHold rather than firing -- an LSP request on every cursor move.