From 509b16f5430b265df1397b2126f3781884e5f07b Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Sat, 8 Aug 2026 13:57:02 +0100 Subject: [PATCH 1/4] Add gitsigns via vim.pack, track the pack lockfile Restores plugin management after the v0.12 config rewrite dropped the earlier `vim.pack.add` calls. Plugins now live in `config/plugins/`, one file per plugin, so adding the next is a single require. Sets `` to Space for the gitsigns hunk mappings, and pins 'signcolumn' open so git and diagnostic signs don't shift text. The lockfile moves into the repo and is symlinked by bootstrap.sh, as the vim.pack docs recommend, giving reproducible plugin revisions across machines and a rollback path via git. vim.pack writes it with a truncating open, so it follows the symlink rather than replacing it. Drops the orphaned nvim-tree entry, unreferenced since the rewrite. Co-Authored-By: Claude Opus 5 (1M context) --- bootstrap.sh | 1 + neovim/config/init.lua | 1 + neovim/config/options.lua | 6 ++++ neovim/config/plugins/gitsigns.lua | 55 ++++++++++++++++++++++++++++++ neovim/config/plugins/init.lua | 1 + neovim/nvim-pack-lock.json | 8 +++++ 6 files changed, 72 insertions(+) create mode 100644 neovim/config/plugins/gitsigns.lua create mode 100644 neovim/config/plugins/init.lua create mode 100644 neovim/nvim-pack-lock.json diff --git a/bootstrap.sh b/bootstrap.sh index bed56a8..61f3a61 100755 --- a/bootstrap.sh +++ b/bootstrap.sh @@ -38,6 +38,7 @@ echo "Setting up NeoVim" test -d $HOME/.config/nvim || mkdir -p $HOME/.config/nvim test -d $HOME/.local/share/nvim || mkdir -p $HOME/.local/share/nvim test -L $HOME/.config/nvim/init.lua || ln -f -s $BASEDIR/neovim/init.lua $HOME/.config/nvim/init.lua +test -L $HOME/.config/nvim/nvim-pack-lock.json || ln -f -s $BASEDIR/neovim/nvim-pack-lock.json $HOME/.config/nvim/nvim-pack-lock.json test -d $HOME/.config/nvim/lua || mkdir -p $HOME/.config/nvim/lua test -L $HOME/.config/nvim/lua/config || ln -f -s $BASEDIR/neovim/config $HOME/.config/nvim/lua/config test -d $HOME/.config/nvim/lsp || mkdir -p $HOME/.config/nvim/lsp diff --git a/neovim/config/init.lua b/neovim/config/init.lua index f9db727..379cf15 100644 --- a/neovim/config/init.lua +++ b/neovim/config/init.lua @@ -1,3 +1,4 @@ require('config.options') +require('config.plugins') require('config.diagnostics') require('config.lsp') diff --git a/neovim/config/options.lua b/neovim/config/options.lua index 5b0dff4..b66b348 100644 --- a/neovim/config/options.lua +++ b/neovim/config/options.lua @@ -1,8 +1,14 @@ +-- Set before any plugin defines a mapping +vim.g.mapleader = ' ' +vim.g.maplocalleader = '\\' + vim.o.autocomplete = true vim.o.pumborder = 'rounded' vim.o.pummaxwidth = 40 vim.o.completeopt = 'menu,menuone,noinsert,nearest' vim.o.number = true +-- Always shown, so git/diagnostic signs don't shift the text around +vim.o.signcolumn = 'yes' vim.o.spelllang = 'en_gb' vim.o.spell = true vim.o.termguicolors = true diff --git a/neovim/config/plugins/gitsigns.lua b/neovim/config/plugins/gitsigns.lua new file mode 100644 index 0000000..58df528 --- /dev/null +++ b/neovim/config/plugins/gitsigns.lua @@ -0,0 +1,55 @@ +vim.pack.add({ 'https://github.com/lewis6991/gitsigns.nvim' }) + +require('gitsigns').setup({ + on_attach = function(bufnr) + local gitsigns = require('gitsigns') + + local function map(mode, lhs, rhs, desc) + vim.keymap.set(mode, lhs, rhs, { buffer = bufnr, desc = desc }) + end + + -- Navigation, falling back to vim's own diff motions in diff mode + map('n', ']c', function() + if vim.wo.diff then + vim.cmd.normal({ ']c', bang = true }) + else + gitsigns.nav_hunk('next') + end + end, 'Next git hunk') + + map('n', '[c', function() + if vim.wo.diff then + vim.cmd.normal({ '[c', bang = true }) + else + gitsigns.nav_hunk('prev') + end + end, 'Previous git hunk') + + -- Actions + map('n', 'hs', gitsigns.stage_hunk, 'Stage hunk') + map('n', 'hr', gitsigns.reset_hunk, 'Reset hunk') + + map('v', 'hs', function() + gitsigns.stage_hunk({ vim.fn.line('.'), vim.fn.line('v') }) + end, 'Stage selected hunk') + + map('v', 'hr', function() + gitsigns.reset_hunk({ vim.fn.line('.'), vim.fn.line('v') }) + end, 'Reset selected hunk') + + map('n', 'hS', gitsigns.stage_buffer, 'Stage buffer') + map('n', 'hR', gitsigns.reset_buffer, 'Reset buffer') + map('n', 'hp', gitsigns.preview_hunk, 'Preview hunk') + map('n', 'hd', gitsigns.diffthis, 'Diff against index') + + map('n', 'hb', function() + gitsigns.blame_line({ full = true }) + end, 'Blame line') + + map('n', 'hq', gitsigns.setqflist, 'Hunks to quickfix') + + -- Toggles + map('n', 'tb', gitsigns.toggle_current_line_blame, 'Toggle line blame') + map('n', 'td', gitsigns.toggle_deleted, 'Toggle deleted lines') + end, +}) diff --git a/neovim/config/plugins/init.lua b/neovim/config/plugins/init.lua new file mode 100644 index 0000000..3ae1436 --- /dev/null +++ b/neovim/config/plugins/init.lua @@ -0,0 +1 @@ +require('config.plugins.gitsigns') diff --git a/neovim/nvim-pack-lock.json b/neovim/nvim-pack-lock.json new file mode 100644 index 0000000..73f107c --- /dev/null +++ b/neovim/nvim-pack-lock.json @@ -0,0 +1,8 @@ +{ + "plugins": { + "gitsigns.nvim": { + "rev": "31d6fb2d618bca1482b9f274751ead5f03461408", + "src": "https://github.com/lewis6991/gitsigns.nvim" + } + } +} From 5d0688c827b529b9a78efc03e67c269890a68833 Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Sat, 8 Aug 2026 14:26:59 +0100 Subject: [PATCH 2/4] Add which-key and nvim-web-devicons, tidy brew casks which-key needs an icon backend and treats mini.icons and nvim-web-devicons as equally viable optional deps, so devicons covers it. Both are pure APIs with no visible effect on their own; devicons self-initialises on require, so no setup() call. Group labels for the h and t prefixes live in the which-key config rather than next to the gitsigns maps they describe, keeping which-key's config in one place. Worth moving if more plugins start contributing prefixes. The Nerd Font the glyphs need was installed by hand on this machine but missing from brew.sh, so a fresh bootstrap would have rendered every icon as tofu. Records it as a cask alongside the ghostty font-family setting that already expects it. Also drops the claude-code cask, which was never actually installed here: brew tracks stable releases, and we want newer ones, so it comes from Claude's own installer instead. Left a comment so it doesn't get re-added. Co-Authored-By: Claude Opus 5 (1M context) --- brew.sh | 6 +++++- neovim/config/plugins/init.lua | 2 ++ neovim/config/plugins/web-devicons.lua | 3 +++ neovim/config/plugins/which-key.lua | 11 +++++++++++ neovim/nvim-pack-lock.json | 8 ++++++++ 5 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 neovim/config/plugins/web-devicons.lua create mode 100644 neovim/config/plugins/which-key.lua diff --git a/brew.sh b/brew.sh index deee053..2b49611 100755 --- a/brew.sh +++ b/brew.sh @@ -69,7 +69,11 @@ brew install zoxide # Install some casks brew install --cask 1password-cli brew install --cask airbuddy -brew install --cask claude-code +# Deliberately no claude-code cask: brew tracks stable, we want newer +# releases, so it's installed via Claude's own installer into ~/.local/bin + +# Terminal font for ghostty; also supplies the glyphs nvim-web-devicons needs +brew install --cask font-hack-nerd-font brew install --cask ngrok brew install --cask silentknight brew install --cask sublime-text diff --git a/neovim/config/plugins/init.lua b/neovim/config/plugins/init.lua index 3ae1436..d3e6515 100644 --- a/neovim/config/plugins/init.lua +++ b/neovim/config/plugins/init.lua @@ -1 +1,3 @@ require('config.plugins.gitsigns') +require('config.plugins.web-devicons') +require('config.plugins.which-key') diff --git a/neovim/config/plugins/web-devicons.lua b/neovim/config/plugins/web-devicons.lua new file mode 100644 index 0000000..e6428b1 --- /dev/null +++ b/neovim/config/plugins/web-devicons.lua @@ -0,0 +1,3 @@ +-- Filetype icons, used by file trees, pickers and statuslines. +-- Needs a Nerd Font in the terminal; setup() is optional so isn't called. +vim.pack.add({ 'https://github.com/nvim-tree/nvim-web-devicons' }) diff --git a/neovim/config/plugins/which-key.lua b/neovim/config/plugins/which-key.lua new file mode 100644 index 0000000..0eb1d9c --- /dev/null +++ b/neovim/config/plugins/which-key.lua @@ -0,0 +1,11 @@ +vim.pack.add({ 'https://github.com/folke/which-key.nvim' }) + +local wk = require('which-key') + +wk.setup() + +-- Names for the gitsigns prefixes, so the popup groups them sensibly +wk.add({ + { 'h', group = 'hunks' }, + { 't', group = 'toggles' }, +}) diff --git a/neovim/nvim-pack-lock.json b/neovim/nvim-pack-lock.json index 73f107c..cb2f5a2 100644 --- a/neovim/nvim-pack-lock.json +++ b/neovim/nvim-pack-lock.json @@ -3,6 +3,14 @@ "gitsigns.nvim": { "rev": "31d6fb2d618bca1482b9f274751ead5f03461408", "src": "https://github.com/lewis6991/gitsigns.nvim" + }, + "nvim-web-devicons": { + "rev": "2ae6958df7ced50baac5035cec0c15799eedfbf7", + "src": "https://github.com/nvim-tree/nvim-web-devicons" + }, + "which-key.nvim": { + "rev": "3aab2147e74890957785941f0c1ad87d0a44c15a", + "src": "https://github.com/folke/which-key.nvim" } } } From e8e9bf066302262e57abe405bebaa94f9712415f Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Sat, 8 Aug 2026 17:27:37 +0100 Subject: [PATCH 3/4] Fix which-key not seeing gitsigns maps, add diagnostic maps The gitsigns keymaps were defined in on_attach, so they were buffer-local and created asynchronously - gitsigns shells out to git before attaching. which-key had already built its keymap tree for the buffer by then, and it only invalidates that cache on BufReadPost, BufNew and LspAttach, so maps added afterwards stayed invisible. Buf:get() returns cached mode state unless explicitly passed `update`, which is why re-entering the buffer didn't help either. Mapping globally instead means the maps exist before which-key builds any tree. The gitsigns API no-ops in buffers it hasn't attached to (checked stage/reset/preview/blame/nav in a non-git directory), so the only change is that the maps now exist everywhere rather than just in git-tracked buffers. Also puts the diagnostic float and quickfix list on d. Nvim already binds d and ]d/[d for these, so this is a rebinding for discoverability rather than new capability. Co-Authored-By: Claude Opus 5 (1M context) --- neovim/config/diagnostics.lua | 5 ++ neovim/config/plugins/gitsigns.lua | 89 +++++++++++++++-------------- neovim/config/plugins/which-key.lua | 3 +- 3 files changed, 53 insertions(+), 44 deletions(-) diff --git a/neovim/config/diagnostics.lua b/neovim/config/diagnostics.lua index d09ad3f..366a246 100644 --- a/neovim/config/diagnostics.lua +++ b/neovim/config/diagnostics.lua @@ -16,3 +16,8 @@ vim.diagnostic.config({ }, }, }) + +-- Nvim already binds ]d / [d to jump and d to show the diagnostic under +-- the cursor; these put the same actions on so which-key lists them. +vim.keymap.set('n', 'dd', vim.diagnostic.open_float, { desc = 'Show diagnostic' }) +vim.keymap.set('n', 'dq', vim.diagnostic.setqflist, { desc = 'Diagnostics to quickfix' }) diff --git a/neovim/config/plugins/gitsigns.lua b/neovim/config/plugins/gitsigns.lua index 58df528..f1f352e 100644 --- a/neovim/config/plugins/gitsigns.lua +++ b/neovim/config/plugins/gitsigns.lua @@ -1,55 +1,58 @@ vim.pack.add({ 'https://github.com/lewis6991/gitsigns.nvim' }) -require('gitsigns').setup({ - on_attach = function(bufnr) - local gitsigns = require('gitsigns') +local gitsigns = require('gitsigns') - local function map(mode, lhs, rhs, desc) - vim.keymap.set(mode, lhs, rhs, { buffer = bufnr, desc = desc }) - end +gitsigns.setup() - -- Navigation, falling back to vim's own diff motions in diff mode - map('n', ']c', function() - if vim.wo.diff then - vim.cmd.normal({ ']c', bang = true }) - else - gitsigns.nav_hunk('next') - end - end, 'Next git hunk') +-- Mapped globally rather than from on_attach. Gitsigns attaches asynchronously, +-- after which-key has already built its keymap tree for the buffer, and +-- which-key only rebuilds that on BufReadPost/BufNew/LspAttach - so +-- buffer-local maps added later never show up in its popup. The gitsigns API +-- no-ops in buffers it hasn't attached to, so global maps are safe. +local function map(mode, lhs, rhs, desc) + vim.keymap.set(mode, lhs, rhs, { desc = desc }) +end - map('n', '[c', function() - if vim.wo.diff then - vim.cmd.normal({ '[c', bang = true }) - else - gitsigns.nav_hunk('prev') - end - end, 'Previous git hunk') +-- Navigation, falling back to vim's own diff motions in diff mode +map('n', ']c', function() + if vim.wo.diff then + vim.cmd.normal({ ']c', bang = true }) + else + gitsigns.nav_hunk('next') + end +end, 'Next git hunk') - -- Actions - map('n', 'hs', gitsigns.stage_hunk, 'Stage hunk') - map('n', 'hr', gitsigns.reset_hunk, 'Reset hunk') +map('n', '[c', function() + if vim.wo.diff then + vim.cmd.normal({ '[c', bang = true }) + else + gitsigns.nav_hunk('prev') + end +end, 'Previous git hunk') - map('v', 'hs', function() - gitsigns.stage_hunk({ vim.fn.line('.'), vim.fn.line('v') }) - end, 'Stage selected hunk') +-- Actions +map('n', 'hs', gitsigns.stage_hunk, 'Stage hunk') +map('n', 'hr', gitsigns.reset_hunk, 'Reset hunk') - map('v', 'hr', function() - gitsigns.reset_hunk({ vim.fn.line('.'), vim.fn.line('v') }) - end, 'Reset selected hunk') +map('v', 'hs', function() + gitsigns.stage_hunk({ vim.fn.line('.'), vim.fn.line('v') }) +end, 'Stage selected hunk') - map('n', 'hS', gitsigns.stage_buffer, 'Stage buffer') - map('n', 'hR', gitsigns.reset_buffer, 'Reset buffer') - map('n', 'hp', gitsigns.preview_hunk, 'Preview hunk') - map('n', 'hd', gitsigns.diffthis, 'Diff against index') +map('v', 'hr', function() + gitsigns.reset_hunk({ vim.fn.line('.'), vim.fn.line('v') }) +end, 'Reset selected hunk') - map('n', 'hb', function() - gitsigns.blame_line({ full = true }) - end, 'Blame line') +map('n', 'hS', gitsigns.stage_buffer, 'Stage buffer') +map('n', 'hR', gitsigns.reset_buffer, 'Reset buffer') +map('n', 'hp', gitsigns.preview_hunk, 'Preview hunk') +map('n', 'hd', gitsigns.diffthis, 'Diff against index') - map('n', 'hq', gitsigns.setqflist, 'Hunks to quickfix') +map('n', 'hb', function() + gitsigns.blame_line({ full = true }) +end, 'Blame line') - -- Toggles - map('n', 'tb', gitsigns.toggle_current_line_blame, 'Toggle line blame') - map('n', 'td', gitsigns.toggle_deleted, 'Toggle deleted lines') - end, -}) +map('n', 'hq', gitsigns.setqflist, 'Hunks to quickfix') + +-- Toggles +map('n', 'tb', gitsigns.toggle_current_line_blame, 'Toggle line blame') +map('n', 'td', gitsigns.toggle_deleted, 'Toggle deleted lines') diff --git a/neovim/config/plugins/which-key.lua b/neovim/config/plugins/which-key.lua index 0eb1d9c..2b6bf9e 100644 --- a/neovim/config/plugins/which-key.lua +++ b/neovim/config/plugins/which-key.lua @@ -4,8 +4,9 @@ local wk = require('which-key') wk.setup() --- Names for the gitsigns prefixes, so the popup groups them sensibly +-- Names for the prefixes, so the popup groups them sensibly wk.add({ + { 'd', group = 'diagnostics' }, { 'h', group = 'hunks' }, { 't', group = 'toggles' }, }) From 865b52c2e58d4724d4c279d9ec8859f0f88a28c4 Mon Sep 17 00:00:00 2001 From: Jonny Barnes Date: Sat, 8 Aug 2026 18:39:48 +0100 Subject: [PATCH 4/4] Add fzf-lua for project navigation Wraps the fzf binary already installed via brew, alongside rg and fd, so it needs no plugin dependencies - unlike telescope, which would also pull in plenary. Picks up nvim-web-devicons for file icons, which is the first thing actually using that plugin. Maps live under f: files, live grep, buffers, grep the word under the cursor, and help tags. The intent is to stop treating netrw as the way into a project and switch to jumping by name, leaving buffers open and bouncing between them, rather than quitting out of nvim to change file. Netrw is untouched and still on :Ex for browsing an unfamiliar tree. Co-Authored-By: Claude Opus 5 (1M context) --- neovim/config/plugins/fzf-lua.lua | 15 +++++++++++++++ neovim/config/plugins/init.lua | 1 + neovim/config/plugins/which-key.lua | 1 + neovim/nvim-pack-lock.json | 4 ++++ 4 files changed, 21 insertions(+) create mode 100644 neovim/config/plugins/fzf-lua.lua diff --git a/neovim/config/plugins/fzf-lua.lua b/neovim/config/plugins/fzf-lua.lua new file mode 100644 index 0000000..8b56161 --- /dev/null +++ b/neovim/config/plugins/fzf-lua.lua @@ -0,0 +1,15 @@ +vim.pack.add({ 'https://github.com/ibhagwan/fzf-lua' }) + +local fzf = require('fzf-lua') + +fzf.setup() + +local function map(lhs, rhs, desc) + vim.keymap.set('n', lhs, rhs, { desc = desc }) +end + +map('ff', fzf.files, 'Find files') +map('fg', fzf.live_grep, 'Grep project') +map('fb', fzf.buffers, 'Switch buffer') +map('fw', fzf.grep_cword, 'Grep word under cursor') +map('fh', fzf.helptags, 'Search help') diff --git a/neovim/config/plugins/init.lua b/neovim/config/plugins/init.lua index d3e6515..b9bcfe9 100644 --- a/neovim/config/plugins/init.lua +++ b/neovim/config/plugins/init.lua @@ -1,3 +1,4 @@ +require('config.plugins.fzf-lua') require('config.plugins.gitsigns') require('config.plugins.web-devicons') require('config.plugins.which-key') diff --git a/neovim/config/plugins/which-key.lua b/neovim/config/plugins/which-key.lua index 2b6bf9e..634bd35 100644 --- a/neovim/config/plugins/which-key.lua +++ b/neovim/config/plugins/which-key.lua @@ -7,6 +7,7 @@ wk.setup() -- Names for the prefixes, so the popup groups them sensibly wk.add({ { 'd', group = 'diagnostics' }, + { 'f', group = 'find' }, { 'h', group = 'hunks' }, { 't', group = 'toggles' }, }) diff --git a/neovim/nvim-pack-lock.json b/neovim/nvim-pack-lock.json index cb2f5a2..4fdde6e 100644 --- a/neovim/nvim-pack-lock.json +++ b/neovim/nvim-pack-lock.json @@ -1,5 +1,9 @@ { "plugins": { + "fzf-lua": { + "rev": "8671012a7b2a1648595952c655039d51fb301c14", + "src": "https://github.com/ibhagwan/fzf-lua" + }, "gitsigns.nvim": { "rev": "31d6fb2d618bca1482b9f274751ead5f03461408", "src": "https://github.com/lewis6991/gitsigns.nvim"