summaryrefslogtreecommitdiff
path: root/config/nvim
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-09-17 19:45:23 +0100
committerIgor Soarez <igor@soarez.org>2026-09-17 19:45:23 +0100
commit13cc065ec014b86369dfe4ad60d1a43fa06fd251 (patch)
tree684690c61b7b59758c2aac132c65b81c86de0608 /config/nvim
dotfiles
Shell, editor and terminal configuration, the tools that go with them, and szecret, an age-based secret manager whose store lives elsewhere. History restarts here. What came before was not written to be read by anyone else, and carried machine names, addresses and an encrypted secret store; it is kept privately instead. What is left is meant to be safe on any machine, including one whose owner is not me: see CLAUDE.md for the rule it is written to.
Diffstat (limited to 'config/nvim')
-rw-r--r--config/nvim/colors/dark.lua12
-rw-r--r--config/nvim/colors/default.lua11
-rw-r--r--config/nvim/colors/light.lua20
-rw-r--r--config/nvim/colors/molokai.lua5
-rw-r--r--config/nvim/colors/onehalfdark.lua3
-rw-r--r--config/nvim/init.lua702
-rw-r--r--config/nvim/local-plugins/cfg-watch/lua/cfg_watch/init.lua58
-rw-r--r--config/nvim/lua/.gitignore1
-rw-r--r--config/nvim/lua/config/lazy.lua37
-rw-r--r--config/nvim/lua/plugins/colorschemes.lua23
-rw-r--r--config/nvim/lua/plugins/completion.lua159
-rw-r--r--config/nvim/lua/plugins/editing.lua127
-rw-r--r--config/nvim/lua/plugins/editor.lua29
-rw-r--r--config/nvim/lua/plugins/fzf.lua148
-rw-r--r--config/nvim/lua/plugins/git.lua63
-rw-r--r--config/nvim/lua/plugins/lsp.lua281
-rw-r--r--config/nvim/lua/plugins/reload.lua28
-rw-r--r--config/nvim/lua/plugins/testing.lua41
-rw-r--r--config/nvim/lua/plugins/ui.lua335
-rw-r--r--config/nvim/lua/plugins/which-key.lua30
-rw-r--r--config/nvim/stylua.toml18
21 files changed, 2131 insertions, 0 deletions
diff --git a/config/nvim/colors/dark.lua b/config/nvim/colors/dark.lua
new file mode 100644
index 0000000..1b59858
--- /dev/null
+++ b/config/nvim/colors/dark.lua
@@ -0,0 +1,12 @@
+vim.g.ayucolor = "dark"
+vim.opt.background = "dark"
+vim.cmd("colorscheme ayu")
+-- vim.cmd("colorscheme github") -- Alternative color scheme (commented out)
+
+-- vim.g.airline_theme = "github"
+-- vim.g.lightline = { colorscheme = "github" }
+
+-- GitHub color theme settings
+-- vim.g.github_colors_soft = 1 -- Slightly darker background like GitHub inline code blocks
+-- vim.g.github_colors_block_diffmark = 0 -- More blocky diff markers in the sign column (e.g., GitGutter)
+
diff --git a/config/nvim/colors/default.lua b/config/nvim/colors/default.lua
new file mode 100644
index 0000000..44f59dd
--- /dev/null
+++ b/config/nvim/colors/default.lua
@@ -0,0 +1,11 @@
+-- This file should only be used by the theme script with variables injected
+-- If variables are not set, use fallback values
+---@diagnostic disable-next-line: undefined-global
+local mode = theme_mode or "dark"
+---@diagnostic disable-next-line: undefined-global
+local colors = theme_colors or "ayu"
+
+vim.g.ayucolor = mode
+vim.opt.background = mode
+vim.cmd("colorscheme " .. colors)
+
diff --git a/config/nvim/colors/light.lua b/config/nvim/colors/light.lua
new file mode 100644
index 0000000..ee2056c
--- /dev/null
+++ b/config/nvim/colors/light.lua
@@ -0,0 +1,20 @@
+vim.g.ayucolor = "light"
+vim.opt.background = "light"
+vim.cmd("colorscheme onehalflight")
+
+-- Alternative color schemes
+-- vim.cmd("colorscheme github")
+-- vim.g.airline_theme = "github"
+-- vim.g.lightline = { colorscheme = "github" }
+
+-- GitHub theme customizations
+-- vim.g.github_colors_soft = 1 -- Use a slightly darker background like GitHub inline code blocks
+-- vim.g.github_colors_block_diffmark = 0 -- Use blocky diff markers in the sign column (e.g., GitGutter)
+
+-- OneDark theme configuration
+-- vim.g.onedark_config = { style = "light" }
+-- vim.cmd("colorscheme onedark")
+
+-- Another alternative colorscheme
+-- vim.cmd("colorscheme PaperColor")
+
diff --git a/config/nvim/colors/molokai.lua b/config/nvim/colors/molokai.lua
new file mode 100644
index 0000000..9c63e5d
--- /dev/null
+++ b/config/nvim/colors/molokai.lua
@@ -0,0 +1,5 @@
+vim.g.ayucolor = "dark"
+vim.opt.background = "dark"
+-- vim.cmd("colorscheme monokai")
+-- vim.cmd("colorscheme deep-space")
+vim.cmd("colorscheme happy_hacking")
diff --git a/config/nvim/colors/onehalfdark.lua b/config/nvim/colors/onehalfdark.lua
new file mode 100644
index 0000000..3656341
--- /dev/null
+++ b/config/nvim/colors/onehalfdark.lua
@@ -0,0 +1,3 @@
+vim.g.ayucolor = "dark"
+vim.opt.background = "dark"
+vim.cmd("colorscheme onehalfdark")
diff --git a/config/nvim/init.lua b/config/nvim/init.lua
new file mode 100644
index 0000000..3f094e8
--- /dev/null
+++ b/config/nvim/init.lua
@@ -0,0 +1,702 @@
+------------------------------------------------------
+---- 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", "<C-=>", function()
+ change_scale_factor(1.05)
+ end)
+ vim.keymap.set("n", "<C-->", function()
+ change_scale_factor(1 / 1.05)
+ end)
+ vim.keymap.set("n", "<C-0>", 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", "<D-n>", function()
+ vim.fn.jobstart("neovide --fork", { detach = true })
+ end) -- Open new instance
+ vim.keymap.set("n", "<D-s>", ":w<CR>") -- Save
+ vim.keymap.set("v", "<D-c>", '"+y') -- Copy
+ vim.keymap.set("n", "<D-v>", '"+P') -- Paste normal mode
+ vim.keymap.set("v", "<D-v>", '"+P') -- Paste visual mode
+ vim.keymap.set("c", "<D-v>", "<C-R>+") -- Paste command mode
+ vim.keymap.set("i", "<D-v>", "<C-R>+") -- 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 <Leader><) 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 <Leader>; so <Leader>t is free for tests (neotest)
+vim.keymap.set("n", "<Leader>;v", ":vsplit term://$SHELL<CR>i", { desc = "Terminal in vertical split" })
+vim.keymap.set("n", "<Leader>;s", ":belowright split term://$SHELL<CR>i", { desc = "Terminal in horizontal split" })
+-- Toggleable, reusable terminal (snacks) — hide/show without re-spawning
+vim.keymap.set("n", "<Leader>;t", function()
+ Snacks.terminal.toggle()
+end, { desc = "Toggle terminal" })
+vim.keymap.set("t", "<Esc>", "<C-\\><C-n>", { 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", "<Leader>nn", function()
+ run_cur_self(nil)
+end, { desc = "Run current file" })
+vim.keymap.set("n", "<Leader>nv", function()
+ run_cur_self("v")
+end, { desc = "Run current file (vertical split)" })
+vim.keymap.set("n", "<Leader>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("<Leader>dl", "virtual_lines")
+mapDiagnosticsToggle("<Leader>dt", "virtual_text")
+mapDiagnosticsToggle("<Leader>ds", "signs")
+mapDiagnosticsToggle("<Leader>du", "underline")
+vim.keymap.set("n", "<Leader>dd", function()
+ vim.diagnostic.enable(not vim.diagnostic.is_enabled())
+end, { desc = "Toggle diagnostics" })
+vim.keymap.set("n", "<Leader>dq", function()
+ vim.diagnostic.setqflist()
+end, { desc = "Show all diagnostics in quickfix" })
+vim.keymap.set("n", "<Leader>dh", function()
+ vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled({}))
+end, { desc = "Toggle inlay hints" })
+
+------------------------------------------------------
+---- Productivity ------------------------------------
+------------------------------------------------------
+
+vim.keymap.set("n", "<Leader>,", ":e $MYVIMRC<CR>", { noremap = true, silent = true })
+vim.keymap.set("n", "<Leader><", 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
+ -- <plugin> 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", "<Leader>w", ":update<CR>", { desc = "Save file if modified" })
+vim.keymap.set("n", "<Leader>W", ":write<CR>", { desc = "Save file" })
+vim.keymap.set("n", "<Leader>\\", ":nohl<CR>", { desc = "Clear search highlights" })
+
+-- Quickfix navigation
+vim.keymap.set("n", "]q", ":cnext<CR>", { desc = "Quickfix next" })
+vim.keymap.set("n", "[q", ":cprevious<CR>", { 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<CR>", { desc = "Quickfix open" })
+vim.keymap.set("n", "][", ":cclose<CR>", { desc = "Quickfix close" })
+
+-- Location list navigation
+vim.keymap.set("n", "]l", ":lnext<CR>", { noremap = true, silent = true, desc = "Location list next" })
+vim.keymap.set("n", "[l", ":lprevious<CR>", { 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",
+ "<Leader><Leader>l",
+ ":let @+ = expand('%') . ':' . line('.')<CR>",
+ { noremap = true, silent = true }
+)
+
+-- %% expands to dirname of current file
+vim.cmd([[cnoremap <expr> %% getcmdtype() == ':' ? expand('%:p:h').'/' : '%%']])
+
+-- Redraw screen
+vim.keymap.set("n", "<C-n>", "<C-l>", { noremap = true, silent = true })
+
+-- Disable distracting features in current buffer
+vim.keymap.set("n", "<Leader>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", "<Leader>cT")
+
+ -- TypeScript compilation: <Leader>cT (requires tsconfig.json)
+ if project_has_file("tsconfig.json") then
+ vim.keymap.set("n", "<Leader>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 <C-l> console.log()<Esc>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.
diff --git a/config/nvim/local-plugins/cfg-watch/lua/cfg_watch/init.lua b/config/nvim/local-plugins/cfg-watch/lua/cfg_watch/init.lua
new file mode 100644
index 0000000..3b5c9d6
--- /dev/null
+++ b/config/nvim/local-plugins/cfg-watch/lua/cfg_watch/init.lua
@@ -0,0 +1,58 @@
+local M = {}
+
+local function load_config_path(filepath)
+ local uv = vim.uv or vim.loop
+
+ if uv.fs_stat(filepath) == nil then
+ local dir = filepath:match("(.*/)")
+ if dir and uv.fs_stat(dir) == nil then
+ assert(uv.fs_mkdir(dir, 448)) -- 0o700
+ end
+ local file, err = io.open(filepath, "w")
+ if not file then
+ vim.schedule(function()
+ vim.notify("Error creating file: " .. filepath .. "\n" .. err, vim.log.levels.ERROR)
+ end)
+ return
+ end
+ file:write("-- Created by cfg-watch\n")
+ file:close()
+ else
+ vim.schedule(function()
+ local ok, err = pcall(dofile, filepath)
+ if not ok then
+ vim.schedule(function()
+ vim.notify("Error loading config: " .. filepath .. "\n" .. err, vim.log.levels.ERROR)
+ end)
+ end
+ end)
+ end
+end
+
+function M.load_colors()
+ local filepath = vim.fn.stdpath("data") .. "/colors.lua" -- ~/.local/share/nvim/colors.lua
+ load_config_path(filepath)
+
+ local ok, fwatch = pcall(require, "fwatch")
+ if ok and fwatch and fwatch.once then
+ fwatch.once(filepath, {
+ on_event = function() M.load_colors() end,
+ on_error = function(err)
+ vim.notify("fwatch error: " .. err, vim.log.levels.ERROR)
+ end,
+ })
+ else
+ vim.notify("cfg-watch: fwatch not available", vim.log.levels.ERROR)
+ end
+
+ vim.schedule(function()
+ vim.cmd("hi Normal guibg=NONE ctermbg=NONE")
+ end)
+end
+
+function M.load_config(name)
+ local filepath = vim.fn.stdpath("config") .. "/lua/" .. name .. ".lua"
+ load_config_path(filepath)
+end
+
+return M
diff --git a/config/nvim/lua/.gitignore b/config/nvim/lua/.gitignore
new file mode 100644
index 0000000..8ad8db1
--- /dev/null
+++ b/config/nvim/lua/.gitignore
@@ -0,0 +1 @@
+local.lua
diff --git a/config/nvim/lua/config/lazy.lua b/config/nvim/lua/config/lazy.lua
new file mode 100644
index 0000000..58636f9
--- /dev/null
+++ b/config/nvim/lua/config/lazy.lua
@@ -0,0 +1,37 @@
+-- Check if Lazy.nvim is installed, if not, install it
+local lazypath = vim.fn.stdpath("data") .. "/lazy/lazy.nvim" -- Lazy.nvim install path
+if not (vim.uv or vim.loop).fs_stat(lazypath) then
+ local lazyrepo = "https://github.com/folke/lazy.nvim.git"
+ local out = vim.fn.system({ "git", "clone", "--filter=blob:none", "--branch=stable", lazyrepo, lazypath })
+ if vim.v.shell_error ~= 0 then
+ vim.api.nvim_echo({
+ { "Failed to clone lazy.nvim:\n", "ErrorMsg" },
+ { out, "WarningMsg" },
+ { "\nPress any key to exit..." },
+ }, true, {})
+ vim.fn.getchar()
+ os.exit(1)
+ end
+end
+vim.opt.rtp:prepend(lazypath)
+
+-- Load Lazy.nvim and install plugins if necessary
+require("lazy").setup({
+ spec = {
+ -- import plugins from ~/.config/nvim/lua/plugins/
+ -- eg ~/.config/nvim/lua/plugins/completion.lua
+ { import = "plugins" },
+ },
+ install = {
+ -- install missing plugins on startup. This doesn't increase startup time.
+ missing = true,
+ },
+ -- automatically check for plugin updates
+ checker = {
+ enabled = true, -- automatically check for plugin updates
+ concurrency = nil, -- set to 1 to check for updates very slowly
+ notify = false, -- get a notification when new updates are found
+ frequency = 6 * 30 * 24 * 3600, -- check for updates every 6 months
+ check_pinned = false, -- check for pinned packages that can't be updated
+ },
+})
diff --git a/config/nvim/lua/plugins/colorschemes.lua b/config/nvim/lua/plugins/colorschemes.lua
new file mode 100644
index 0000000..005403d
--- /dev/null
+++ b/config/nvim/lua/plugins/colorschemes.lua
@@ -0,0 +1,23 @@
+local plugins = {
+ { "sonph/onehalf", rtp = "vim/" },
+ { "altercation/vim-colors-solarized" },
+ { "nlknguyen/papercolor-theme" },
+ { "tanvirtin/monokai.nvim" },
+ { "tomasr/molokai" },
+ { "cohlin/vim-colorschemes" },
+ { "reedes/vim-colors-pencil" },
+ { "ayu-theme/ayu-vim" },
+ { "overcache/NeoSolarized" },
+ { "cormacrelf/vim-colors-github" },
+ { "rafi/awesome-vim-colorschemes" },
+}
+
+-- These are lazy-loaded: cfg-watch (lua/plugins/reload.lua) lists them as
+-- dependencies, so they are loaded right before ~/.local/share/nvim/colors.lua
+-- runs `:colorscheme`. Keeping them lazy keeps them off the startup path while
+-- still letting the dynamic light/dark theme switcher reach any of them.
+for _, plugin in ipairs(plugins) do
+ plugin.lazy = true
+end
+
+return plugins
diff --git a/config/nvim/lua/plugins/completion.lua b/config/nvim/lua/plugins/completion.lua
new file mode 100644
index 0000000..fa037cd
--- /dev/null
+++ b/config/nvim/lua/plugins/completion.lua
@@ -0,0 +1,159 @@
+local plugins = {
+ -- Autocompletion and Snippets
+ {
+ "Saghen/blink.cmp",
+ version = "1.*", -- Use a stable release version
+ main = "blink.cmp",
+ opts = {
+ keymap = { -- https://cmp.saghen.dev/configuration/keymap.html
+ preset = "default",
+ ["<Tab>"] = { "accept", "fallback" },
+ },
+ sources = {
+ -- Enable completion from lazydev, LSP, filesystem paths, snippets, buffer
+ -- (and optionally supermaven)
+ -- default = { "lazydev", "lsp", "path", "snippets", "buffer", "supermaven" },
+ default = { "lazydev", "lsp", "path", "snippets", "buffer" },
+ providers = {
+ -- Neovim Lua API / plugin spec completions (folke/lazydev.nvim)
+ lazydev = {
+ name = "LazyDev",
+ module = "lazydev.integrations.blink",
+ score_offset = 100, -- show lazydev items above LSP
+ },
+ buffer = {
+ -- Optional: Configure buffer-specific settings
+ -- max_async_buffer_size = 200000, -- 200KB default
+ -- use_cache = true, -- Cache words for better performance
+ },
+ -- lsp = { fallbacks = {} }, -- always show the buffer source
+ supermaven = {
+ enabled = false,
+ name = "supermaven",
+ module = "blink-cmp-supermaven",
+ async = true,
+
+ },
+ },
+ },
+ completion = {
+ -- Enable ghost text to show completions inline
+ ghost_text = {
+ -- enabled = true,
+ enabled = false,
+ },
+ -- Configure the completion menu
+ menu = {
+ auto_show = true, -- Automatically show the menu
+ },
+ list = {
+ selection = {
+ preselect = true,
+ auto_insert = false,
+ },
+ },
+ -- Avoid unnecessary requests for performance
+ trigger = {
+ -- prefetch_on_insert = true, -- When true, will prefetch the completion items when entering insert mode
+ show_in_snippet = false, -- When false, will not show the completion window automatically when in a snippet
+ show_on_backspace = false, -- When true, will show completion window after backspacing
+ show_on_backspace_in_keyword = false, -- When true, will show completion window after backspacing into a keyword
+ show_on_backspace_after_accept = false, -- When true, will show the completion window after accepting a completion and then backspacing into a keyword
+ show_on_backspace_after_insert_enter = false, -- When true, will show the completion window after entering insert mode and backspacing into keyword
+ show_on_keyword = false, -- When true, will show the completion window after typing any of alphanumerics, `-` or `_`
+ show_on_trigger_character = false, -- When true, will show the completion window after typing a trigger character
+ show_on_insert = false, -- When true, will show the completion window after entering insert mode
+ -- LSPs can indicate when to show the completion window via trigger characters
+ -- however, some LSPs (i.e. tsserver) return characters that would essentially
+ -- always show the window. We block these by default.
+ -- show_on_blocked_trigger_characters = { " ", "\n", "\t" },
+ -- You can also block per filetype with a function:
+ -- show_on_blocked_trigger_characters = function(ctx)
+ -- if vim.bo.filetype == 'markdown' then return { ' ', '\n', '\t', '.', '/', '(', '[' } end
+ -- return { ' ', '\n', '\t' }
+ -- end,
+ -- When both this and show_on_trigger_character are true, will show the completion window
+ -- when the cursor comes after a trigger character after accepting an item
+ show_on_accept_on_trigger_character = false,
+ -- When both this and show_on_trigger_character are true, will show the completion window
+ -- when the cursor comes after a trigger character when entering insert mode
+ show_on_insert_on_trigger_character = false,
+ -- List of trigger characters (on top of `show_on_blocked_trigger_characters`) that won't trigger
+ -- the completion window when the cursor comes after a trigger character when
+ -- entering insert mode/accepting an item
+ -- show_on_x_blocked_trigger_characters = { "'", '"', "(" },
+ -- or a function, similar to show_on_blocked_trigger_character
+ },
+ },
+ signature = {
+ enabled = true,
+ window = {
+ show_documentation = false,
+ },
+ },
+ -- Disable blink.cmp for command-line mode to let native wildmenu work
+ cmdline = {
+ enabled = false,
+ },
+ },
+ },
+
+ -- Supermaven was acquired (and killed) by Cursor
+ {
+ "supermaven-inc/supermaven-nvim",
+ event = "InsertEnter",
+ opts = {
+ keymaps = {
+ accept_suggestion = "<Tab>",
+ clear_suggestion = "<C-]>",
+ accept_word = "<C-j>",
+ },
+ ignore_filetypes = {},
+ disable_inline_completion = false,
+ disable_keymaps = false,
+ condition = function()
+ return false
+ end,
+ log_level = "info",
+ },
+ config = function(_, opts)
+ require("supermaven-nvim").setup(opts)
+ -- Clear supermaven ghost text when entering command-line mode
+ -- to prevent interference with wildmenu completion
+ vim.api.nvim_create_autocmd("CmdlineEnter", {
+ callback = function()
+ local ok, preview = pcall(require, "supermaven-nvim.completion_preview")
+ if ok and preview.on_dispose_inlay then
+ preview.on_dispose_inlay()
+ end
+ end,
+ })
+ end,
+ },
+ {
+ "huijiro/blink-cmp-supermaven",
+ },
+
+ -- Lua LSP completion + types for the Neovim API and lazy plugin specs.
+ -- Replaces the old lua_ls `globals` hack for editing this config.
+ {
+ "folke/lazydev.nvim",
+ ft = "lua",
+ opts = {
+ library = {
+ -- Load luvit types when the `vim.uv` word is found
+ { path = "${3rd}/luv/library", words = { "vim%.uv" } },
+ -- Types for snacks.nvim (used throughout this config)
+ { path = "snacks.nvim", words = { "Snacks" } },
+ },
+ },
+ },
+}
+
+for _, plugin in ipairs(plugins) do
+ if not plugin.priority then
+ plugin.priority = 2000
+ end
+end
+
+return plugins
diff --git a/config/nvim/lua/plugins/editing.lua b/config/nvim/lua/plugins/editing.lua
new file mode 100644
index 0000000..af7a867
--- /dev/null
+++ b/config/nvim/lua/plugins/editing.lua
@@ -0,0 +1,127 @@
+local plugins = {
+ { "godlygeek/tabular" }, -- text filtering and alignment
+ {
+ "Wansmer/treesj", -- splitting/joining blocks of code
+ dependencies = { "nvim-treesitter/nvim-treesitter" },
+ config = function()
+ require("treesj").setup({
+ -- use_default_keymaps = false,
+ })
+ vim.keymap.set("n", "<leader>jj", require("treesj").join)
+ vim.keymap.set("n", "<leader>js", require("treesj").split)
+ vim.keymap.set("n", "<leader>jm", require("treesj").toggle)
+ vim.keymap.set("n", "<leader>jM", function()
+ require("treesj").toggle({ split = { recursive = true } })
+ end)
+ end,
+ },
+ { "tpope/vim-repeat" }, -- enable plugins to repeat actions
+ { "tpope/vim-sleuth" }, -- heuristically set buffer options
+ -- Add/delete/replace surrounding pairs: sa (add), sd (delete), sr (replace).
+ -- Replaces tpope/vim-surround; consolidates onto mini.* with built-in
+ -- dot-repeat. NOTE: the `s` prefix shadows native `s` (substitute) — use `cl`.
+ {
+ "nvim-mini/mini.surround",
+ event = "VeryLazy",
+ opts = {},
+ },
+ -- Auto close/rename HTML/JSX/TSX/Astro tags
+ {
+ "windwp/nvim-ts-autotag",
+ ft = {
+ "html", "xml", "markdown", "svelte", "vue",
+ "javascript", "javascriptreact", "typescript", "typescriptreact", "astro",
+ },
+ opts = {},
+ },
+ -- Context-aware commentstring (e.g. {/* */} inside JSX, not //)
+ {
+ "folke/ts-comments.nvim",
+ event = "VeryLazy",
+ opts = {},
+ },
+ -- Extended text objects: af/if (function), ac/ic (class),
+ -- ao/io (block/conditional/loop), plus args/quotes/brackets.
+ -- Takes over the old fold if/af mappings.
+ {
+ "nvim-mini/mini.ai",
+ event = "VeryLazy",
+ dependencies = {
+ "nvim-treesitter/nvim-treesitter-textobjects",
+ },
+ opts = function()
+ local ai = require("mini.ai")
+ return {
+ n_lines = 500,
+ custom_textobjects = {
+ f = ai.gen_spec.treesitter({ a = "@function.outer", i = "@function.inner" }),
+ c = ai.gen_spec.treesitter({ a = "@class.outer", i = "@class.inner" }),
+ o = ai.gen_spec.treesitter({
+ a = { "@block.outer", "@conditional.outer", "@loop.outer" },
+ i = { "@block.inner", "@conditional.inner", "@loop.inner" },
+ }),
+ },
+ }
+ end,
+ },
+ -- Treesitter textobjects (main): move between and swap functions/classes/args,
+ -- and make ; , (and f F t T) repeat the last move.
+ {
+ "nvim-treesitter/nvim-treesitter-textobjects",
+ branch = "main",
+ event = "VeryLazy",
+ config = function()
+ require("nvim-treesitter-textobjects").setup({
+ move = { set_jumps = true }, -- record moves in the jumplist
+ })
+
+ local move = require("nvim-treesitter-textobjects.move")
+ local swap = require("nvim-treesitter-textobjects.swap")
+ local rep = require("nvim-treesitter-textobjects.repeatable_move")
+
+ -- Repeat the last textobject move with ; and , ...
+ vim.keymap.set({ "n", "x", "o" }, ";", rep.repeat_last_move)
+ vim.keymap.set({ "n", "x", "o" }, ",", rep.repeat_last_move_opposite)
+ -- ... and upgrade f/F/t/T to repeat with the same ; ,
+ vim.keymap.set({ "n", "x", "o" }, "f", rep.builtin_f_expr, { expr = true })
+ vim.keymap.set({ "n", "x", "o" }, "F", rep.builtin_F_expr, { expr = true })
+ vim.keymap.set({ "n", "x", "o" }, "t", rep.builtin_t_expr, { expr = true })
+ vim.keymap.set({ "n", "x", "o" }, "T", rep.builtin_T_expr, { expr = true })
+
+ -- Jump between functions / classes ( ]] [[ are taken by quickfix ).
+ -- Note: ]c / [c shadow diff-mode change navigation; swap to ]C/[C if
+ -- you'd rather keep that.
+ local jumps = {
+ ["]f"] = { move.goto_next_start, "@function.outer", "Next function start" },
+ ["[f"] = { move.goto_previous_start, "@function.outer", "Prev function start" },
+ ["]F"] = { move.goto_next_end, "@function.outer", "Next function end" },
+ ["[F"] = { move.goto_previous_end, "@function.outer", "Prev function end" },
+ ["]c"] = { move.goto_next_start, "@class.outer", "Next class start" },
+ ["[c"] = { move.goto_previous_start, "@class.outer", "Prev class start" },
+ }
+ for lhs, m in pairs(jumps) do
+ vim.keymap.set({ "n", "x", "o" }, lhs, function()
+ m[1](m[2], "textobjects")
+ end, { desc = m[3] })
+ end
+
+ -- Swap the argument under the cursor with the next / previous one
+ vim.keymap.set("n", "<leader>sa", function()
+ swap.swap_next("@parameter.inner")
+ end, { desc = "Swap argument with next" })
+ vim.keymap.set("n", "<leader>sA", function()
+ swap.swap_previous("@parameter.inner")
+ end, { desc = "Swap argument with previous" })
+ end,
+ },
+ -- Comment.nvim and nvim-ts-context-commentstring removed
+ -- Neovim 0.10+ has built-in gc/gcc commenting with native treesitter commentstring support
+}
+
+for _, plugin in ipairs(plugins) do
+ if not plugin.priority then
+ plugin.priority = 2000
+ end
+end
+
+return plugins
diff --git a/config/nvim/lua/plugins/editor.lua b/config/nvim/lua/plugins/editor.lua
new file mode 100644
index 0000000..4e1cb4d
--- /dev/null
+++ b/config/nvim/lua/plugins/editor.lua
@@ -0,0 +1,29 @@
+return {
+ -- Highlight and navigate TODO / FIXME / HACK / BUG comments
+ {
+ "folke/todo-comments.nvim",
+ event = "VeryLazy",
+ dependencies = { "nvim-lua/plenary.nvim" },
+ opts = {},
+ keys = {
+ { "]t", function() require("todo-comments").jump_next() end, desc = "Next todo comment" },
+ { "[t", function() require("todo-comments").jump_prev() end, desc = "Previous todo comment" },
+ { "<leader>qt", "<cmd>TodoTrouble<cr>", desc = "Todo (Trouble)" },
+ },
+ },
+ -- Pretty, navigable lists for diagnostics, references, symbols, quickfix.
+ -- Lives under <leader>q ("lists") to avoid the <leader>x buffer-close prefix.
+ {
+ "folke/trouble.nvim",
+ cmd = "Trouble",
+ dependencies = { "nvim-tree/nvim-web-devicons" },
+ opts = {},
+ keys = {
+ { "<leader>qd", "<cmd>Trouble diagnostics toggle<cr>", desc = "Diagnostics (Trouble)" },
+ { "<leader>qb", "<cmd>Trouble diagnostics toggle filter.buf=0<cr>", desc = "Buffer diagnostics (Trouble)" },
+ { "<leader>qs", "<cmd>Trouble symbols toggle<cr>", desc = "Symbols (Trouble)" },
+ { "<leader>ql", "<cmd>Trouble lsp toggle<cr>", desc = "LSP defs/refs (Trouble)" },
+ { "<leader>qq", "<cmd>Trouble qflist toggle<cr>", desc = "Quickfix list (Trouble)" },
+ },
+ },
+}
diff --git a/config/nvim/lua/plugins/fzf.lua b/config/nvim/lua/plugins/fzf.lua
new file mode 100644
index 0000000..8ef0104
--- /dev/null
+++ b/config/nvim/lua/plugins/fzf.lua
@@ -0,0 +1,148 @@
+return {
+ {
+ "ibhagwan/fzf-lua",
+ dependencies = { "nvim-tree/nvim-web-devicons" },
+ config = function()
+ local fzf = require("fzf-lua")
+ fzf.setup({
+ defaults = {
+ file_icons = true,
+ },
+ grep = {
+ rg_opts = table.concat({
+ "--color=never",
+ "--no-heading",
+ "--with-filename",
+ "--line-number",
+ "--column",
+ "--smart-case",
+ "--glob=!.git/**",
+ "--glob=!package-lock.json",
+ "--glob=!lazy-lock.json",
+ "--glob=!node_modules/**",
+ }, " "),
+ },
+ keymap = {
+ builtin = {
+ ["<C-h>"] = "toggle-help",
+ },
+ fzf = {
+ ["ctrl-q"] = "select-all+accept",
+ },
+ },
+ })
+
+ -- General pickers
+ vim.keymap.set("n", "<Leader>f", fzf.resume, { desc = "Resume search" })
+ vim.keymap.set("n", "<C-p>", fzf.files, { desc = "Find file" })
+ vim.keymap.set("n", "<Leader>a", fzf.live_grep, { desc = "Find text" })
+ vim.keymap.set("n", "\\b", fzf.buffers, { desc = "Find buffer" })
+ vim.keymap.set("n", "\\h", fzf.help_tags, { desc = "Find help" })
+ vim.keymap.set("n", "\\k", fzf.keymaps, { desc = "Find keymaps" })
+ vim.keymap.set("n", "<Leader><Leader>t", fzf.colorschemes, { desc = "Select colorscheme" })
+
+ -- Git
+ vim.keymap.set("n", "\\g", fzf.git_status, { desc = "Find changed file" })
+
+ -- LSP pickers
+ vim.keymap.set("n", "\\r", function()
+ fzf.lsp_references({ includeDeclaration = false })
+ end, { desc = "Find references" })
+ vim.keymap.set("n", "\\c", fzf.lsp_incoming_calls, { desc = "Find incoming calls" })
+ vim.keymap.set("n", "\\o", fzf.lsp_outgoing_calls, { desc = "Find outgoing calls" })
+ vim.keymap.set("n", "\\d", fzf.lsp_definitions, { desc = "Find definitions" })
+ vim.keymap.set("n", "\\t", fzf.lsp_typedefs, { desc = "Find type definitions" })
+ vim.keymap.set("n", "\\i", fzf.lsp_implementations, { desc = "Find implementations" })
+ vim.keymap.set("n", "\\s", fzf.lsp_document_symbols, { desc = "Find document symbols" })
+ vim.keymap.set("n", "\\S", fzf.lsp_live_workspace_symbols, { desc = "Find workspace symbols" })
+
+ -- Search nvim config
+ vim.keymap.set("n", "\\C", function()
+ fzf.live_grep({
+ cwd = vim.fn.expand("~/.config/nvim"),
+ rg_opts = "--follow " .. table.concat({
+ "--color=never",
+ "--no-heading",
+ "--with-filename",
+ "--line-number",
+ "--column",
+ "--smart-case",
+ }, " "),
+ })
+ end, { desc = "Search config" })
+
+ -- Custom directory picker
+ local function reset_lsp_workspace_folders()
+ local clients = vim.lsp.get_clients()
+ if #clients == 0 then
+ vim.notify("No active LSP clients found", vim.log.levels.WARN)
+ return
+ end
+ for _, client in ipairs(clients) do
+ local current_folders = client.workspace_folders or {}
+ for _, folder in ipairs(current_folders) do
+ vim.lsp.buf.remove_workspace_folder(folder.name)
+ end
+ vim.lsp.buf.add_workspace_folder(vim.fn.getcwd())
+ vim.notify(
+ string.format("Reset workspace folders for %s to: %s", client.name, vim.fn.getcwd()),
+ vim.log.levels.INFO
+ )
+ end
+ end
+
+ local function build_find_dir_cmd()
+ local home = vim.fn.expand("~")
+ local homeSrc = vim.fn.expand("~/src")
+ local sense = "/Volumes/Sense"
+ local cmd = "find"
+ local search_paths = {}
+ local excluded_dirs = {
+ "*/cloud-copy*",
+ "*/node_modules*",
+ "*/.venv*",
+ "*/venv*",
+ }
+ if vim.fn.isdirectory(sense) == 1 then
+ table.insert(search_paths, sense)
+ table.insert(excluded_dirs, homeSrc)
+ else
+ table.insert(search_paths, homeSrc)
+ end
+ table.insert(search_paths, vim.fn.expand("~/.local/share/nvim"))
+
+ local args = { cmd }
+ for _, path in ipairs(search_paths) do
+ table.insert(args, path)
+ end
+ table.insert(args, "-type")
+ table.insert(args, "d")
+ table.insert(args, "-maxdepth")
+ table.insert(args, "4")
+ for _, dir in ipairs(excluded_dirs) do
+ table.insert(args, "-not")
+ table.insert(args, "-path")
+ table.insert(args, dir)
+ end
+ return table.concat(args, " ")
+ end
+
+ vim.keymap.set("n", "<Leader>p", function()
+ fzf.fzf_exec(build_find_dir_cmd(), {
+ prompt = "Change Directory> ",
+ actions = {
+ ["default"] = function(selected)
+ local dir = selected[1]
+ vim.cmd("cd " .. vim.fn.fnameescape(dir))
+ vim.opt.titlestring = vim.fn.fnamemodify(dir, ":t")
+ vim.opt.title = true
+ reset_lsp_workspace_folders()
+ vim.cmd("LspRestart")
+ print("Changed directory to: " .. dir)
+ end,
+ },
+ })
+ end, { desc = "Change working directory" })
+ end,
+ },
+}
diff --git a/config/nvim/lua/plugins/git.lua b/config/nvim/lua/plugins/git.lua
new file mode 100644
index 0000000..ad120f4
--- /dev/null
+++ b/config/nvim/lua/plugins/git.lua
@@ -0,0 +1,63 @@
+return {
+ {
+ "lewis6991/gitsigns.nvim",
+ config = function()
+ -- Git hunk navigation
+ local gs = require("gitsigns")
+ gs.setup({
+ signs_staged_enable = true,
+ signcolumn = true, -- Toggle with `:Gitsigns toggle_signs`
+ numhl = true, -- Toggle with `:Gitsigns toggle_numhl`
+ linehl = false, -- Toggle with `:Gitsigns toggle_linehl`
+ word_diff = false, -- Toggle with `:Gitsigns toggle_word_diff`
+ current_line_blame_opts = {
+ virt_text_pos = 'right_align', -- 'eol' | 'overlay' | 'right_align'
+ },
+ -- current_line_blame = true,
+ -- current_line_blame_formatter = '<author>, <author_time:%R> - <summary>',
+ })
+ vim.keymap.set("n", "[g", gs.prev_hunk, { desc = "Goto previous git hunk" })
+ vim.keymap.set("n", "]g", gs.next_hunk, { desc = "Goto next git hunk" })
+
+ -- vim.keymap.set("n", "<Leader>cg", gitsigns.preview_hunk, { desc = "Preview git hunk" })
+ vim.keymap.set("n", "<Leader>gd", function() gs.preview_hunk_inline() end, { desc = "Preview hunk inline" })
+ vim.keymap.set("n", "<Leader>gD", function() gs.preview_hunk() end, { desc = "Preview hunk popup" })
+ vim.keymap.set("n", "<Leader>gf", function() gs.diffthis() end, { desc = "Diff file" })
+ vim.keymap.set("n", "<Leader>gr", function() gs.reset_hunk() end, { desc = "Revert hunk" })
+ vim.keymap.set("n", "<Leader>gs", function() gs.stage_hunk() end, { desc = "Stage hunk" })
+ vim.keymap.set("n", "<Leader>ga", function() gs.stage_buffer() end, { desc = "Stage buffer" })
+ vim.keymap.set("n", "<Leader>gB", function() gs.toggle_current_line_blame() end, { desc = "Toggle current line blame (virtual text)" })
+ vim.keymap.set("n", "<Leader>gb", function() gs.blame() end, { desc = "Open blame" })
+
+ -- Select hunks as a text object.
+ vim.keymap.set({'o', 'x'}, 'ih', '<Cmd>Gitsigns select_hunk<CR>')
+
+ -- Open current file/line (or visual selection) on the git remote
+ vim.keymap.set({ "n", "x" }, "<Leader>go", function() Snacks.gitbrowse() end, { desc = "Open in remote (browser)" })
+ end,
+ },
+ -- Lazy-loaded: kept only for its `:Git <arbitrary>` escape hatch and :Gblame,
+ -- since neogit + diffview + gitsigns cover day-to-day staging/diff/blame.
+ {
+ "tpope/vim-fugitive",
+ cmd = { "Git", "G", "Gdiffsplit", "Gread", "Gwrite", "Gedit", "Gblame", "Gclog" },
+ },
+ {
+ "NeogitOrg/neogit",
+ lazy = true,
+ dependencies = {
+ "nvim-lua/plenary.nvim", -- required
+ "sindrets/diffview.nvim", -- optional - Diff integration
+
+ -- Only one of these is needed.
+ "ibhagwan/fzf-lua", -- optional
+ -- "nvim-telescope/telescope.nvim", -- optional
+ -- "nvim-mini/mini.pick", -- optional
+ -- "folke/snacks.nvim", -- optional
+ },
+ cmd = "Neogit",
+ keys = {
+ { "<leader>gg", "<cmd>Neogit<cr>", desc = "Show Neogit UI" }
+ }
+ },
+}
diff --git a/config/nvim/lua/plugins/lsp.lua b/config/nvim/lua/plugins/lsp.lua
new file mode 100644
index 0000000..c3853b3
--- /dev/null
+++ b/config/nvim/lua/plugins/lsp.lua
@@ -0,0 +1,281 @@
+-- LSP attach handler using LspAttach autocmd (NeoVim 0.11+ recommended pattern)
+vim.api.nvim_create_autocmd('LspAttach', {
+ callback = function(args)
+ local client = vim.lsp.get_client_by_id(args.data.client_id)
+ local bufnr = args.buf
+
+ -- Enable completion triggered by <c-x><c-o>
+ vim.bo[bufnr].omnifunc = "v:lua.vim.lsp.omnifunc"
+
+ vim.keymap.set("n", "K", function() vim.lsp.buf.hover() end, { buffer = bufnr, desc = "Hover info" })
+ vim.keymap.set("n", "<c-k>", function() vim.lsp.buf.signature_help() end, { buffer = bufnr, desc = "Signature help" })
+ vim.keymap.set("n", "<Leader>L", function() vim.lsp.buf.format({ async = true }) end, { buffer = bufnr, desc = "LSP format" })
+
+ -- TypeScript/JavaScript specific: organize imports
+ if client and client.name == "ts_ls" then
+ vim.keymap.set("n", "gro", function()
+ client:exec_cmd({
+ command = "_typescript.organizeImports",
+ arguments = { vim.api.nvim_buf_get_name(0) },
+ title = "",
+ })
+ end, { buffer = bufnr, desc = "Organize imports" })
+ end
+ end,
+})
+
+local function configure_lsp() -- Setup LSP configurations using vim.lsp.config (NeoVim 0.11+)
+ -- Completion capabilities from blink.cmp (extends Neovim's defaults with the
+ -- snippet/resolve/tag support that the hand-rolled table used to set).
+ local capabilities = require("blink.cmp").get_lsp_capabilities()
+
+ -- Global config for all LSP servers
+ vim.lsp.config('*', {
+ capabilities = capabilities,
+ })
+
+ -- Configure lua-language-server
+ vim.lsp.config('lua_ls', {
+ settings = {
+ Lua = {
+ diagnostics = {
+ -- `vim`/`require` are covered by lazydev.nvim + LuaLS defaults.
+ -- `hs` is the Hammerspoon global, which lazydev doesn't know about.
+ globals = {
+ "hs",
+ },
+ disable = {
+ "unused-local",
+ },
+ },
+ },
+ },
+ })
+
+ -- Configure bash-language-server
+ vim.lsp.config('bashls', {
+ settings = {
+ bashIde = {
+ enableExplainshellEndpoint = false,
+ explainshellEndpoint = "",
+ globPattern = "*@(.sh|.bash|.zsh|bash*|zsh*)",
+ shfmt = {
+ path = "shfmt",
+ args = { "-i", "2", "-ci", "-sr" },
+ },
+ },
+ },
+ })
+
+ -- Configure typescript-language-server
+ vim.lsp.config('ts_ls', {})
+
+ -- Configure astro-language-server
+ vim.lsp.config('astro', {})
+
+ -- Configure gopls (Go language server)
+ vim.lsp.config('gopls', {
+ settings = {
+ gopls = {
+ analyses = {
+ unusedparams = true,
+ },
+ staticcheck = true,
+ gofumpt = true,
+ },
+ },
+ })
+
+ -- Configure pyright (Python language server)
+ vim.lsp.config('pyright', {
+ settings = {
+ python = {
+ analysis = {
+ autoSearchPaths = true,
+ useLibraryCodeForTypes = true,
+ diagnosticMode = "workspace",
+ },
+ },
+ },
+ })
+
+ -- Configure jdtls (Java language server)
+ vim.lsp.config('jdtls', {})
+
+ -- Enable all configured servers
+ -- Note: This doesn't force installation, it just enables them if installed
+ vim.lsp.enable({ 'lua_ls', 'bashls', 'ts_ls', 'astro', 'gopls', 'pyright', 'jdtls' })
+
+ -- ESLint LSP is temporarily disabled (causes errors with call hierarchy)
+ -- To re-enable, remove the line below and add 'eslint' to vim.lsp.enable()
+ vim.lsp.enable('eslint', false)
+end
+
+return {
+ {
+ "nvim-treesitter/nvim-treesitter",
+ branch = "main", -- Use main branch for Neovim 0.12+ compatibility
+ build = ":TSUpdate",
+ config = function()
+ -- Add nvim-treesitter's runtime directory to runtimepath for queries (folds, indents, etc.)
+ local ts_runtime = vim.fn.stdpath("data") .. "/lazy/nvim-treesitter/runtime"
+ if vim.fn.isdirectory(ts_runtime) == 1 then
+ vim.opt.runtimepath:prepend(ts_runtime)
+ end
+
+ -- The `main` branch has no `ensure_installed` option, so we install any
+ -- missing parsers ourselves. Add/remove languages here as needed.
+ local TS = require("nvim-treesitter")
+ local ensure_installed = {
+ "astro", "go", "html", "javascript", "lua", "python", "tsx", "typescript",
+ }
+ local installed = TS.get_installed("parsers")
+ local to_install = vim.tbl_filter(function(lang)
+ return not vim.tbl_contains(installed, lang)
+ end, ensure_installed)
+ if #to_install > 0 then
+ TS.install(to_install, { summary = true })
+ end
+
+ -- On the `main` branch, treesitter highlighting must be started per
+ -- buffer (it is NOT automatic). Start it whenever a parser is available.
+ vim.api.nvim_create_autocmd("FileType", {
+ group = vim.api.nvim_create_augroup("sz_treesitter_start", { clear = true }),
+ callback = function(args)
+ pcall(vim.treesitter.start, args.buf)
+ end,
+ })
+
+ -- Show all available code actions (including organize imports)
+ vim.keymap.set(
+ "n",
+ "<leader>ca",
+ vim.lsp.buf.code_action,
+ { noremap = true, silent = true, desc = "Show code actions" }
+ )
+ vim.keymap.set("v", "<C-k>", vim.lsp.buf.code_action,
+ { noremap = true, silent = true, desc = "Show code actions" })
+ end,
+ },
+ {
+ "williamboman/mason.nvim",
+ main = "mason",
+ opts = {
+ registries = {
+ "github:mason-org/mason-registry",
+ "github:Crashdummyy/mason-registry", -- Roslyn LSP
+ },
+ },
+ },
+ {
+ "williamboman/mason-lspconfig.nvim",
+ main = "mason-lspconfig",
+ opts = {
+ -- Only ensure core language servers are installed on ALL machines
+ -- Other servers will be auto-installed when you open a file of that type
+ ensure_installed = { "lua_ls", "bashls" },
+ },
+ },
+ {
+ "WhoIsSethDaniel/mason-tool-installer.nvim",
+ dependencies = { "williamboman/mason.nvim" },
+ config = function()
+ require("mason-tool-installer").setup({
+ -- Core tools - always installed
+ ensure_installed = {
+ "stylua", -- Lua formatting
+ "shfmt", -- Bash formatting
+ "prettier", -- JS/TS/Astro formatting
+ },
+ -- Optional tools - install with: :MasonToolsInstall
+ -- Or add them here when working on those projects
+ -- "black", -- Python formatting
+ -- "isort", -- Python imports sorting
+ -- "gofumpt", -- Go formatting (stricter than gofmt)
+ -- "goimports", -- Go imports formatting
+ -- "csharpier", -- C# formatting
+ -- "netcoredbg", -- C#/.NET debugging
+ -- "google-java-format", -- Java formatting
+ })
+ end,
+ },
+ {
+ "neovim/nvim-lspconfig",
+ config = configure_lsp,
+ },
+ {
+ "seblj/roslyn.nvim",
+ ft = "cs",
+ dependencies = {
+ "nvim-lua/plenary.nvim",
+ },
+ opts = {
+ filewatching = "roslyn",
+ -- Disable Razor extension (only needed for Blazor)
+ razor = {
+ enabled = false,
+ },
+ config = {
+ settings = {
+ ["csharp|inlay_hints"] = {
+ csharp_enable_inlay_hints_for_implicit_object_creation = true,
+ csharp_enable_inlay_hints_for_implicit_variable_types = true,
+ csharp_enable_inlay_hints_for_lambda_parameter_types = true,
+ csharp_enable_inlay_hints_for_types = true,
+ dotnet_enable_inlay_hints_for_indexer_parameters = true,
+ dotnet_enable_inlay_hints_for_literal_parameters = true,
+ dotnet_enable_inlay_hints_for_object_creation_parameters = true,
+ dotnet_enable_inlay_hints_for_other_parameters = true,
+ dotnet_enable_inlay_hints_for_parameters = true,
+ dotnet_suppress_inlay_hints_for_parameters_that_differ_only_by_suffix = true,
+ dotnet_suppress_inlay_hints_for_parameters_that_match_argument_name = true,
+ dotnet_suppress_inlay_hints_for_parameters_that_match_method_intent = true,
+ },
+ ["csharp|completion"] = {
+ dotnet_show_completion_items_from_unimported_namespaces = true,
+ dotnet_show_name_completion_suggestions = true,
+ },
+ ["csharp|code_lens"] = {
+ dotnet_enable_references_code_lens = true,
+ dotnet_enable_tests_code_lens = true,
+ },
+ },
+ },
+ },
+ },
+ {
+ "stevearc/conform.nvim",
+ event = { "BufReadPre", "BufNewFile" },
+ config = function()
+ local conform = require("conform")
+ conform.setup({
+ formatters_by_ft = {
+ astro = { "prettier" },
+ bash = { "shfmt" },
+ cs = { "csharpier" },
+ css = { "prettier" },
+ go = { "goimports", "gofumpt" }, -- Run goimports then gofumpt
+ html = { "prettier" },
+ java = { "google-java-format" },
+ javascript = { "prettier" },
+ javascriptreact = { "prettier" },
+ json = { "prettier" },
+ lua = { "stylua" },
+ markdown = { "prettier" },
+ python = { "isort", "black" }, -- Run isort then black
+ typescript = { "prettier" },
+ typescriptreact = { "prettier" },
+ },
+ -- Set to `true` to format on save, or provide a function for conditional formatting
+ -- format_on_save = {
+ -- timeout_ms = 500,
+ -- lsp_fallback = true, -- Use LSP formatting as a fallback if no external formatter is found
+ -- },
+ })
+
+ vim.keymap.set({ "n", "v" }, "<leader>l", function()
+ conform.format({ async = true, lsp_format = "fallback" })
+ end, { desc = "Format file or range" })
+ end,
+ },
+}
diff --git a/config/nvim/lua/plugins/reload.lua b/config/nvim/lua/plugins/reload.lua
new file mode 100644
index 0000000..6bd1366
--- /dev/null
+++ b/config/nvim/lua/plugins/reload.lua
@@ -0,0 +1,28 @@
+return {
+ {
+ dir = vim.fn.stdpath("config") .. "/local-plugins/cfg-watch",
+ name = "cfg-watch",
+ event = "VeryLazy", -- fires after init.lua finishes and UI is ready
+ -- Colorscheme plugins (defined in lua/plugins/colorschemes.lua as lazy=true)
+ -- are listed here so they load before load_colors() runs `:colorscheme`.
+ dependencies = {
+ { "rktjmp/fwatch.nvim" },
+ "sonph/onehalf",
+ "altercation/vim-colors-solarized",
+ "nlknguyen/papercolor-theme",
+ "tanvirtin/monokai.nvim",
+ "tomasr/molokai",
+ "cohlin/vim-colorschemes",
+ "reedes/vim-colors-pencil",
+ "ayu-theme/ayu-vim",
+ "overcache/NeoSolarized",
+ "cormacrelf/vim-colors-github",
+ "rafi/awesome-vim-colorschemes",
+ },
+ config = function()
+ local lc = require("cfg_watch")
+ lc.load_colors()
+ lc.load_config("local")
+ end,
+ },
+}
diff --git a/config/nvim/lua/plugins/testing.lua b/config/nvim/lua/plugins/testing.lua
new file mode 100644
index 0000000..c402bb1
--- /dev/null
+++ b/config/nvim/lua/plugins/testing.lua
@@ -0,0 +1,41 @@
+return {
+ {
+ "nvim-neotest/neotest",
+ dependencies = {
+ "nvim-neotest/nvim-nio",
+ "nvim-lua/plenary.nvim",
+ "nvim-treesitter/nvim-treesitter",
+ "nvim-neotest/neotest-jest",
+ -- "marilari88/neotest-vitest",
+ },
+ cmd = { "Neotest" },
+ keys = {
+ { "<Leader>ta", function() require("neotest").run.run(vim.fn.getcwd()) end, desc = "Run all tests" },
+ { "<Leader>tf", function() require("neotest").run.run(vim.fn.expand("%")) end, desc = "Run tests in file" },
+ { "<Leader>tn", function() require("neotest").run.run() end, desc = "Run nearest test" },
+ { "<Leader>to", function() require("neotest").output.open({ enter = true }) end, desc = "Show test output" },
+ { "<Leader>tS", function() require("neotest").summary.toggle() end, desc = "Toggle test summary" },
+ },
+ config = function()
+ require("neotest").setup({
+ adapters = {
+ require("neotest-jest")({
+ jestCommand = "npm test --",
+ jestArguments = function(defaultArguments, context)
+ return defaultArguments
+ end,
+ -- jestConfigFile = "custom.jest.config.ts",
+ -- env = { CI = true },
+ cwd = function(path)
+ return vim.fn.getcwd()
+ end,
+ filter_dir = function(name, rel_path, root)
+ return name ~= "node_modules" and name ~= "dist" and name ~= "build"
+ end,
+ }),
+ -- require("neotest-vitest"),
+ },
+ })
+ end,
+ },
+}
diff --git a/config/nvim/lua/plugins/ui.lua b/config/nvim/lua/plugins/ui.lua
new file mode 100644
index 0000000..d035789
--- /dev/null
+++ b/config/nvim/lua/plugins/ui.lua
@@ -0,0 +1,335 @@
+return {
+ {
+ "folke/snacks.nvim",
+ priority = 1000,
+ lazy = false,
+ ---@type snacks.Config
+ opts = {
+ indent = {
+ enabled = true,
+ animate = {
+ enabled = false,
+ },
+ },
+ -- Disable expensive features on very large / minified files
+ bigfile = { enabled = true },
+ -- Render a file before plugins fully load, for faster opening
+ quickfile = { enabled = true },
+ -- LSP reference highlighting under the cursor (CursorHold-based);
+ -- replaces the hand-rolled CursorMoved autocmd in init.lua
+ words = { enabled = true },
+ -- Treesitter scope detection: ii/ai indent text-objects + [i/]i jumps
+ scope = { enabled = true },
+ -- Unified gutter (signs+number+fold+git). The statuscolumn option
+ -- itself is set in init.lua.
+ statuscolumn = { enabled = true },
+ },
+ keys = {
+ { "<Leader>it", function()
+ if Snacks.indent.enabled then
+ Snacks.indent.disable()
+ else
+ Snacks.indent.enable()
+ end
+ end, desc = "Toggle indent guides" },
+ },
+ },
+ {
+ "nvim-lualine/lualine.nvim",
+ config = function()
+ local enable_web_dev_icons = vim.g.enable_web_dev_icons ~= 0
+ local lualine_custom_theme = require("lualine.themes.auto")
+ lualine_custom_theme.normal.c.bg = "none"
+ require("lualine").setup({
+ options = {
+ icons_enabled = enable_web_dev_icons,
+ theme = lualine_custom_theme,
+ component_separators = "|",
+ section_separators = { left = "", right = "" },
+ disabled_filetypes = {},
+ always_divide_middle = true,
+ globalstatus = true,
+ },
+ sections = {
+ lualine_a = { "mode" },
+ lualine_b = { "branch", "diff", "diagnostics" },
+ lualine_c = { "filename" },
+ lualine_x = { "encoding", "fileformat", "filetype" },
+ lualine_y = {
+ function()
+ return vim.fn.fnamemodify(vim.fn.getcwd(), ":t")
+ end,
+ },
+ lualine_z = { "progress", "location" },
+ },
+ inactive_sections = {
+ lualine_a = {},
+ lualine_b = {},
+ lualine_c = { "filename" },
+ lualine_x = { "location" },
+ lualine_y = {},
+ lualine_z = {},
+ },
+ tabline = {},
+ extensions = {},
+ })
+ end,
+ },
+ { "nvim-tree/nvim-web-devicons" },
+ { "nvim-mini/mini.icons", version = false },
+ {
+ "j-hui/fidget.nvim",
+ main = "fidget",
+ opts = {},
+ },
+ {
+ "romgrk/barbar.nvim",
+ version = "^1.0.0",
+ dependencies = {
+ "nvim-tree/nvim-web-devicons", -- for file icons
+ "lewis6991/gitsigns.nvim", -- optional for git status
+ },
+ init = function()
+ vim.g.barbar_auto_setup = false
+ end,
+ config = function()
+ require("barbar").setup({
+ -- Enable/disable animations
+ animation = true,
+ -- Enable/disable auto-hiding the tab bar when there is a single buffer
+ auto_hide = true,
+ -- Enable/disable current/total tabpages indicator (top right corner)
+ tabpages = true,
+ -- Enables/disable clickable tabs
+ clickable = true,
+ -- Configure icons on the bufferline
+ icons = {
+ -- Configure the base icons on the bufferline
+ buffer_index = false,
+ buffer_number = false,
+ button = "", -- Empty string disables close button
+ -- Enables / disables diagnostic symbols
+ diagnostics = {
+ [vim.diagnostic.severity.ERROR] = { enabled = true },
+ [vim.diagnostic.severity.WARN] = { enabled = true },
+ [vim.diagnostic.severity.INFO] = { enabled = false },
+ [vim.diagnostic.severity.HINT] = { enabled = false },
+ },
+ gitsigns = {
+ added = { enabled = true, icon = "+" },
+ changed = { enabled = true, icon = "~" },
+ deleted = { enabled = true, icon = "-" },
+ },
+ filetype = {
+ -- Sets the icon's highlight group.
+ -- If false, will use nvim-web-devicons colors
+ custom_colors = false,
+ -- Requires `nvim-web-devicons` if `true`
+ enabled = false,
+ },
+ separator = { left = "▎", right = "" },
+ -- Configure the icons on the bufferline when modified
+ modified = { button = "" },
+ -- Configure the icons on the bufferline based on the visibility of a buffer
+ pinned = { button = "📌" },
+ -- Configure the icons on the bufferline for various kinds of tabs
+ preset = "default",
+ -- Configure the icons on the bufferline for alternate layout
+ alternate = { filetype = { enabled = false } },
+ -- Configure the icons on the bufferline when the buffer is currently active
+ current = { buffer_index = false },
+ -- Configure the icons on the bufferline for inactive tabs
+ -- inactive = { button = "×" },
+ inactive = {},
+ -- Configure the icons on the bufferline for visible tabs
+ visible = { modified = { buffer_number = false } },
+ },
+ -- If true, new buffers will be inserted at the start/end of the list.
+ -- Default is to insert after current buffer.
+ insert_at_end = false,
+ insert_at_start = false,
+ -- Sets the maximum padding width with which to surround each tab
+ maximum_padding = 1,
+ -- Sets the minimum padding width with which to surround each tab
+ minimum_padding = 1,
+ -- Sets the maximum buffer name length
+ maximum_length = 30,
+ -- Sets the minimum buffer name length
+ minimum_length = 0,
+ -- If set, the letters for each buffer in buffer-pick mode will be
+ -- assigned based on their name. Otherwise or in case all letters are
+ -- already assigned, the behavior is to assign letters in order of
+ -- usability (see order below)
+ semantic_letters = true,
+ -- Sets the name of unnamed buffers. By default format is "[Buffer X]"
+ -- where X is the buffer number. But only a static string is accepted here.
+ no_name_title = nil,
+ -- Set the filetypes which barbar will offset itself for
+ sidebar_filetypes = {
+ NvimTree = { text = "Files", align = "left" },
+ },
+ })
+
+ -- Buffer navigation with <Leader> + number
+ for i = 1, 8 do
+ vim.keymap.set("n", "<Leader>" .. i, ":BufferGoto " .. i .. "<CR>")
+ end
+ vim.keymap.set("n", "<Leader>9", ":BufferLast<CR>", { desc = "Go to last buffer" })
+ vim.keymap.set("n", "<Leader>]", ":BufferNext<CR>")
+ vim.keymap.set("n", "<Leader>[", ":BufferPrevious<CR>")
+ -- vim.keymap.set("n", "<A-<>", ":BufferMovePrevious<CR>")
+ -- vim.keymap.set("n", "<A->>", ":BufferMoveNext<CR>")
+ -- vim.keymap.set("n", "<A-p>", ":BufferPin<CR>")
+ vim.keymap.set("n", "<Leader>x", ":BufferClose<CR>", { desc = "Close buffer" })
+ vim.keymap.set("n", "<Leader>X", ":BufferCloseAllButCurrent<CR>", { desc = "Close other buffers" })
+ end,
+ },
+ { "fweep/vim-zsh-path-completion" },
+ {
+ "nvim-tree/nvim-tree.lua",
+ cmd = { "NvimTreeToggle", "NvimTreeFindFile" },
+ keys = {
+ { "<Leader>/", ":NvimTreeToggle .<CR>", desc = "Toggle file tree" },
+ { "<Leader>?", ":NvimTreeFindFile<CR>", desc = "Find current file in tree" },
+ },
+ config = function()
+ vim.g.enable_web_dev_icons = true
+ local enable_web_dev_icons = vim.g.enable_web_dev_icons ~= 0
+ require("nvim-tree").setup({
+ actions = {
+ open_file = {
+ window_picker = {
+ enable = false,
+ },
+ },
+ },
+ renderer = {
+ highlight_git = true,
+ root_folder_label = false, -- Set to false to hide the root folder name, or a function to customize display
+ icons = {
+ show = {
+ file = enable_web_dev_icons,
+ folder = enable_web_dev_icons,
+ folder_arrow = enable_web_dev_icons,
+ git = false,
+ },
+ },
+ -- special_files = {},
+ },
+ view = {
+ -- signcolumn = 'no',
+ },
+ })
+ end,
+ },
+ {
+ "folke/noice.nvim",
+ event = "VeryLazy",
+ dependencies = {
+ -- if you lazy-load any plugin below, make sure to add proper `module="..."` entries
+ "MunifTanjim/nui.nvim",
+ -- OPTIONAL:
+ -- `nvim-notify` is only needed, if you want to use the notification view.
+ -- If not available, we use `mini` as the fallback
+ "rcarriga/nvim-notify",
+ },
+ config = function()
+ require("notify").setup({
+ background_colour = "#000000",
+ })
+
+ local noice = require("noice")
+ noice.setup({
+ messages = {
+ view = "mini",
+ view_error = "mini", -- view for errors
+ view_warn = "mini", -- view for warnings
+ view_search = false,
+ },
+ lsp = {
+ -- override markdown rendering so that **cmp** and other plugins use **Treesitter**
+ override = {
+ ["vim.lsp.util.convert_input_to_markdown_lines"] = true,
+ ["vim.lsp.util.stylize_markdown"] = true,
+ ["cmp.entry.get_documentation"] = true, -- requires hrsh7th/nvim-cmp
+ },
+ },
+ -- you can enable a preset for easier configuration
+ presets = {
+ -- bottom_search = true, -- use a classic bottom cmdline for search
+ command_palette = true, -- position the cmdline and popupmenu together
+ long_message_to_split = true, -- long messages will be sent to a split
+ inc_rename = false, -- enables an input dialog for inc-rename.nvim
+ lsp_doc_border = false, -- add a border to hover docs and signature help
+ },
+ routes = {
+ -- <C-g> buffer path → mini view, kept visible for 10s.
+ -- Must come first so it wins (and stops) before the generic
+ -- notify route below; route opts.timeout sets autohide.
+ {
+ view = "cmdline_popup",
+ filter = {
+ event = "notify",
+ cond = function(m)
+ return m.opts ~= nil and m.opts.buffer_path == true
+ end,
+ },
+ opts = { timeout = 10000 },
+ },
+
+ { view = "mini", filter = { event = "notify", find = "^Register" } },
+
+ -- 1) all normal Vim messages → mini
+ -- { filter = { event = "msg_show" }, view = "mini" },
+ -- 2) mode/recording messages → mini
+ -- { filter = { event = "msg_showmode" }, view = "mini" },
+ -- 3) anything sent via vim.notify() → mini
+ { filter = { event = "notify" }, view = "mini" },
+ -- 4) LSP progress to mini (leave hover/signature in their own views)
+ -- { filter = { event = "lsp", kind = "progress" }, view = "mini" },
+ },
+ })
+
+ vim.api.nvim_create_autocmd("RecordingEnter", {
+ group = vim.api.nvim_create_augroup("NoiceMacroNotification", { clear = true }),
+ callback = function()
+ _MACRO_RECORDING_STATUS = true
+ noice.notify(string.format("Register: %s", vim.fn.reg_recording()), vim.log.levels.INFO, {
+ title = "Recording macro",
+ view = "mini",
+ keep = function()
+ return _MACRO_RECORDING_STATUS
+ end,
+ })
+ end,
+ })
+ vim.api.nvim_create_autocmd("RecordingLeave", {
+ group = vim.api.nvim_create_augroup("NoiceMacroNotificationDismiss", { clear = true }),
+ callback = function()
+ _MACRO_RECORDING_STATUS = false
+ noice.notify(string.format("Register: %s", vim.fn.reg_recording()), vim.log.levels.INFO, {
+ title = "Saved macro",
+ view = "mini",
+ timeout = 2000,
+ })
+ end,
+ })
+
+ -- setup :Noice dismiss
+ vim.keymap.set("n", "<Leader>nd", ":Noice dismiss<CR>", { desc = "Dismiss notifications" })
+
+ -- <C-g>: show full buffer path. Emit a notify (instead of the native
+ -- msg_show) so noice's msg_show dedup can't drop repeated presses, and
+ -- tag it so the route below sends it to the mini view with a long timeout.
+ vim.keymap.set("n", "<C-g>", function()
+ local path = vim.fn.expand("%:p")
+ if path == "" then
+ path = "[No Name]"
+ end
+ vim.notify(path, vim.log.levels.INFO, { buffer_path = true })
+ end, { desc = "Show full buffer path" })
+ end,
+ },
+ -- Inlay hints are toggled natively via <Leader>dh (see init.lua); the
+ -- MysticalDevil/inlay-hints.nvim plugin is no longer needed on 0.10+.
+}
diff --git a/config/nvim/lua/plugins/which-key.lua b/config/nvim/lua/plugins/which-key.lua
new file mode 100644
index 0000000..8462cbf
--- /dev/null
+++ b/config/nvim/lua/plugins/which-key.lua
@@ -0,0 +1,30 @@
+return {
+ {
+ "folke/which-key.nvim",
+ event = "VeryLazy",
+ keys = {
+ {
+ "<leader>@",
+ function()
+ local which_key = require("which-key")
+ which_key.show({ global = false })
+ end,
+ desc = "Buffer Local Keymaps (which-key)",
+ },
+ },
+ config = function()
+ local wk = require("which-key")
+ wk.add({
+ { "<leader>c", group = "Code actions" },
+ { "<leader>g", group = "Git actions" },
+ { "<leader>d", group = "Diagnostics" },
+ { "<leader>t", group = "Tests" },
+ { "<leader>;", group = "Terminal" },
+ { "<leader>n", group = "Run" },
+ { "<leader>j", group = "Split/Join" },
+ { "<leader>q", group = "Lists (Trouble)" },
+ { "<leader>s", group = "Swap args" },
+ })
+ end,
+ },
+}
diff --git a/config/nvim/stylua.toml b/config/nvim/stylua.toml
new file mode 100644
index 0000000..2717ca2
--- /dev/null
+++ b/config/nvim/stylua.toml
@@ -0,0 +1,18 @@
+# StyLua configuration for Neovim Lua files
+# See: https://github.com/JohnnyMorganz/StyLua
+
+# Maximum line length before wrapping
+column_width = 120
+
+# Indentation settings
+indent_type = "Spaces"
+indent_width = 4
+
+# Quote style for strings
+quote_style = "AutoPreferDouble"
+
+# Call parentheses style
+call_parentheses = "Always"
+
+# Collapse simple statements onto a single line
+collapse_simple_statement = "Never"