feat: add Harpoon and Training mode plugins

- Add Harpoon for quick file navigation
- Add Training mode for learning vim motions without crutches
- Configure keymaps for both plugins
- Add Nix integration for training mode toggle

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
dlond 2025-09-05 18:18:51 +12:00 committed by Daniel Lond
parent 9e6b01e176
commit 161310a072
6 changed files with 446 additions and 0 deletions

View File

@ -34,5 +34,35 @@ end
-- Reload LSP keybind
vim.keymap.set('n', '<leader>lr', reload_lsp, { desc = '[L]SP [R]eload all servers' })
-- Buffer management keymaps
vim.keymap.set('n', '<leader>bb', '<cmd>Telescope buffers<CR>', { desc = '[B]rowse [B]uffers' })
vim.keymap.set('n', '[b', '<cmd>bprevious<CR>', { desc = 'Previous buffer' })
vim.keymap.set('n', ']b', '<cmd>bnext<CR>', { desc = 'Next buffer' })
vim.keymap.set('n', '<leader>bd', '<cmd>bdelete<CR>', { desc = '[B]uffer [D]elete' })
vim.keymap.set('n', '<leader>ba', '<cmd>%bd|e#<CR>', { desc = '[B]uffers close [A]ll but current' })
vim.keymap.set('n', '<leader>bn', '<cmd>enew<CR>', { desc = '[B]uffer [N]ew' })
-- Quick buffer switching with numbers
for i = 1, 9 do
vim.keymap.set('n', '<leader>' .. i, '<cmd>buffer ' .. i .. '<CR>', { desc = 'Switch to buffer ' .. i })
end
-- Alternate file (toggle between two most recent files)
vim.keymap.set('n', '<leader><leader>', '<C-^>', { desc = 'Toggle alternate file' })
-- Window management keymaps
vim.keymap.set('n', '<leader>ws', '<cmd>split<CR>', { desc = '[W]indow [S]plit horizontal' })
vim.keymap.set('n', '<leader>wv', '<cmd>vsplit<CR>', { desc = '[W]indow [V]ertical split' })
vim.keymap.set('n', '<leader>wc', '<cmd>close<CR>', { desc = '[W]indow [C]lose' })
vim.keymap.set('n', '<leader>wo', '<cmd>only<CR>', { desc = '[W]indow [O]nly (close others)' })
vim.keymap.set('n', '<leader>ww', '<C-w>w', { desc = '[W]indow cycle' })
vim.keymap.set('n', '<leader>w=', '<C-w>=', { desc = '[W]indow balance sizes' })
-- Window resizing with arrow keys
vim.keymap.set('n', '<C-Up>', '<cmd>resize +2<CR>', { desc = 'Increase window height' })
vim.keymap.set('n', '<C-Down>', '<cmd>resize -2<CR>', { desc = 'Decrease window height' })
vim.keymap.set('n', '<C-Left>', '<cmd>vertical resize -2<CR>', { desc = 'Decrease window width' })
vim.keymap.set('n', '<C-Right>', '<cmd>vertical resize +2<CR>', { desc = 'Increase window width' })
-- Standard practice for Lua modules that don't need to return complex data
return {}

View File

@ -4,6 +4,9 @@
vim.g.mapleader = ' '
vim.g.maplocalleader = ' '
-- Load Nix-controlled settings if available
pcall(require, 'nix-settings')
-- Place custom vim options here
-- Set based on your font installation

View File

@ -0,0 +1,106 @@
local M = {}
function M.setup()
local harpoon = require 'harpoon'
harpoon:setup {
settings = {
save_on_toggle = false,
sync_on_ui_close = false,
key = function()
return vim.loop.cwd()
end,
},
}
-- Add current file to list
vim.keymap.set('n', '<leader>ma', function()
harpoon:list():add()
vim.notify('Added to Harpoon', vim.log.levels.INFO)
end, { desc = '[M]ark [A]dd file (Harpoon)' })
-- Toggle quick menu
vim.keymap.set('n', '<C-e>', function()
harpoon.ui:toggle_quick_menu(harpoon:list())
end, { desc = 'Toggle Harpoon menu' })
-- Quick navigation to files 1-4 using leader+m prefix
vim.keymap.set('n', '<leader>m1', function()
harpoon:list():select(1)
end, { desc = 'Jump to mark 1' })
vim.keymap.set('n', '<leader>m2', function()
harpoon:list():select(2)
end, { desc = 'Jump to mark 2' })
vim.keymap.set('n', '<leader>m3', function()
harpoon:list():select(3)
end, { desc = 'Jump to mark 3' })
vim.keymap.set('n', '<leader>m4', function()
harpoon:list():select(4)
end, { desc = 'Jump to mark 4' })
-- Alternative quick access with Alt/Option key (doesn't conflict)
vim.keymap.set('n', '<M-1>', function()
harpoon:list():select(1)
end, { desc = 'Harpoon file 1' })
vim.keymap.set('n', '<M-2>', function()
harpoon:list():select(2)
end, { desc = 'Harpoon file 2' })
vim.keymap.set('n', '<M-3>', function()
harpoon:list():select(3)
end, { desc = 'Harpoon file 3' })
vim.keymap.set('n', '<M-4>', function()
harpoon:list():select(4)
end, { desc = 'Harpoon file 4' })
-- Navigate between harpoon files
vim.keymap.set('n', '[m', function()
harpoon:list():prev()
end, { desc = 'Previous marked file' })
vim.keymap.set('n', ']m', function()
harpoon:list():next()
end, { desc = 'Next marked file' })
-- Show harpoon files in Telescope
local conf = require('telescope.config').values
local function toggle_telescope(harpoon_files)
local file_paths = {}
for _, item in ipairs(harpoon_files.items) do
table.insert(file_paths, item.value)
end
require('telescope.pickers')
.new({}, {
prompt_title = 'Harpoon',
finder = require('telescope.finders').new_table {
results = file_paths,
},
previewer = conf.file_previewer {},
sorter = conf.generic_sorter {},
})
:find()
end
vim.keymap.set('n', '<leader>mm', function()
toggle_telescope(harpoon:list())
end, { desc = '[M]arked files in Telescope' })
-- Clear all marks
vim.keymap.set('n', '<leader>mc', function()
harpoon:list():clear()
vim.notify('Cleared all Harpoon marks', vim.log.levels.INFO)
end, { desc = '[M]arks [C]lear all' })
-- Remove current file from harpoon
vim.keymap.set('n', '<leader>mr', function()
harpoon:list():remove()
vim.notify('Removed from Harpoon', vim.log.levels.INFO)
end, { desc = '[M]ark [R]emove current file' })
end
return M

View File

@ -0,0 +1,278 @@
local M = {}
-- Statistics tracking
local stats = {
inefficient_moves = 0,
efficient_moves = 0,
start_time = os.time(),
}
-- Training mode state
local training_enabled = false
-- Movement key counters for spam detection
local key_counts = { h = 0, j = 0, k = 0, l = 0 }
-- Hard mode: Disable inefficient keys
local function setup_hard_mode()
-- Disable arrow keys completely
vim.keymap.set({ 'n', 'v', 'i' }, '<Up>', '<Nop>', { desc = 'Use k instead!' })
vim.keymap.set({ 'n', 'v', 'i' }, '<Down>', '<Nop>', { desc = 'Use j instead!' })
vim.keymap.set({ 'n', 'v', 'i' }, '<Left>', '<Nop>', { desc = 'Use h instead!' })
vim.keymap.set({ 'n', 'v', 'i' }, '<Right>', '<Nop>', { desc = 'Use l instead!' })
-- Make holding j/k/h/l painful (warns after 5 repeats)
for _, key in ipairs { 'h', 'j', 'k', 'l' } do
vim.keymap.set('n', key, function()
key_counts[key] = key_counts[key] + 1
if key_counts[key] > 5 then
vim.notify(string.format('Stop spamming %s! Use counts (5%s) or better navigation!', key, key), vim.log.levels.WARN)
key_counts[key] = 0
end
return key
end, { expr = true })
end
-- Reset counter when using other movements
for _, good_move in ipairs { '<C-d>', '<C-u>', '}', '{', 'gg', 'G' } do
vim.keymap.set('n', good_move, function()
key_counts = { h = 0, j = 0, k = 0, l = 0 }
stats.efficient_moves = stats.efficient_moves + 1
return good_move
end, { expr = true })
end
end
-- Smart search helpers
local function setup_smart_search()
-- Search word under cursor
vim.keymap.set('n', '<leader>*', '*N', { desc = 'Search word under cursor' })
-- Search selected text
vim.keymap.set('v', '//', [[y/\V<C-R>=escape(@",'/\')<CR><CR>]], { desc = 'Search selection' })
-- Replace word under cursor (the smart way)
vim.keymap.set('n', '<leader>R', function()
local word = vim.fn.expand '<cword>'
vim.ui.input({ prompt = 'Replace "' .. word .. '" with: ' }, function(replacement)
if replacement then
vim.cmd(':%s/\\<' .. word .. '\\>/' .. replacement .. '/g')
vim.notify(string.format('Replaced all instances of "%s" with "%s"', word, replacement))
end
end)
end, { desc = 'Replace word under cursor globally' })
-- cgn workflow helper
vim.keymap.set('n', '<leader>C', '*Ncgn', { desc = 'Change word under cursor (cgn workflow)' })
end
-- Efficiency helpers
local function setup_efficiency_helpers()
-- Movement reminder
vim.keymap.set('n', '<leader>tm', function()
local hints = {
'=== VERTICAL MOVEMENT ===',
'gg/G - Top/bottom of file',
'50G or 50% - Go to line 50',
'{ } - Paragraph jumps',
'[[ ]] - Function/section jumps',
'H M L - High/Middle/Low of screen',
'',
'=== HORIZONTAL MOVEMENT ===',
'f<char> / F<char> - Find forward/backward',
't<char> / T<char> - Till forward/backward',
'; , - Repeat f/t forward/backward',
'0 $ - Start/end of line',
'^ g_ - First/last non-blank',
'',
'=== WORD MOVEMENT ===',
'w/W - Next word/WORD',
'b/B - Back word/WORD',
'e/E - End of word/WORD',
'ge/gE - End of previous word/WORD',
'',
'=== SEARCH MOVEMENT ===',
'* # - Search word forward/backward',
'g* g# - Search partial word',
'n N - Next/previous match',
}
vim.notify(table.concat(hints, '\n'), vim.log.levels.INFO)
end, { desc = '[T]raining [M]ovement hints' })
-- Text object practice
vim.keymap.set('n', '<leader>ti', function()
local hints = {
'=== TEXT OBJECTS ===',
'ciw - Change inside word',
'ci" - Change inside quotes',
'ci( or cib - Change inside parentheses',
'ci{ or ciB - Change inside braces',
'cit - Change inside tags',
'cip - Change inside paragraph',
'',
'=== VARIATIONS ===',
'c - Change',
'd - Delete',
'y - Yank',
'v - Visual select',
'',
'i - Inside (excludes delimiters)',
'a - Around (includes delimiters)',
}
vim.notify(table.concat(hints, '\n'), vim.log.levels.INFO)
end, { desc = '[T]ext object [I]nfo' })
end
-- Track statistics
local function setup_statistics()
-- Track efficient movements
local efficient_patterns = { '*', '#', 'cgn', 'ciw', 'ci"', "ci'", 'cib', 'ciB', 'f', 'F', 't', 'T', '}', '{', ']]', '[[' }
for _, pattern in ipairs(efficient_patterns) do
vim.keymap.set('n', pattern, function()
stats.efficient_moves = stats.efficient_moves + 1
return pattern
end, { expr = true, silent = true })
end
end
-- Visual feedback for good movements
local function setup_visual_feedback()
vim.api.nvim_create_autocmd('CursorMoved', {
group = vim.api.nvim_create_augroup('TrainingMode', { clear = true }),
callback = function()
if not training_enabled then
return
end
local col = vim.fn.col '.'
local line = vim.fn.line '.'
-- Check if cursor moved significantly (likely used good navigation)
if math.abs(line - (vim.b.last_line or line)) > 5 then
-- Don't notify, just track it
stats.efficient_moves = stats.efficient_moves + 1
end
vim.b.last_line = line
end,
})
end
-- Disable hard mode
local function disable_hard_mode()
-- Remove hard mode restrictions
pcall(vim.keymap.del, { 'n', 'v', 'i' }, '<Up>')
pcall(vim.keymap.del, { 'n', 'v', 'i' }, '<Down>')
pcall(vim.keymap.del, { 'n', 'v', 'i' }, '<Left>')
pcall(vim.keymap.del, { 'n', 'v', 'i' }, '<Right>')
-- Remove hjkl spam detection
for _, key in ipairs { 'h', 'j', 'k', 'l' } do
pcall(vim.keymap.del, 'n', key)
end
end
-- Main setup function
function M.setup()
-- Always setup smart search helpers (they're just helpful)
setup_smart_search()
setup_efficiency_helpers()
setup_visual_feedback()
vim.notify('Training mode available! Press <leader>tt to toggle', vim.log.levels.INFO)
end
-- Toggle training mode
function M.toggle()
training_enabled = not training_enabled
if training_enabled then
setup_hard_mode()
setup_statistics()
stats.start_time = os.time() -- Reset timer
vim.notify('Training mode ENABLED! 💪\nArrows disabled, hjkl spam detection on!', vim.log.levels.INFO)
else
disable_hard_mode()
vim.notify('Training mode DISABLED. Keep practicing those efficient movements!', vim.log.levels.INFO)
end
end
-- Show statistics
function M.show_stats()
local time_elapsed = os.time() - stats.start_time
local total_moves = stats.efficient_moves + stats.inefficient_moves
local efficiency = total_moves > 0 and (stats.efficient_moves / total_moves * 100) or 0
vim.notify(
string.format(
'📊 Session Statistics:\n' ..
'Time: %d min\n' ..
'Efficient moves: %d\n' ..
'Inefficient moves: %d\n' ..
'Efficiency: %.1f%%\n\n' ..
'Keep practicing! 🎯',
time_elapsed / 60,
stats.efficient_moves,
stats.inefficient_moves,
efficiency
),
vim.log.levels.INFO
)
end
-- Random challenge
function M.challenge()
local challenges = {
'Jump to line 50 without counting lines (use 50G)',
'Find the next "function" (use /function)',
'Change the word in quotes (use ci")',
'Delete until the next comma (use dt,)',
'Jump to matching bracket (use %)',
'Select entire paragraph (use vap)',
'Change word and repeat with . (use *cgn)',
'Jump to next blank line (use })',
'Delete entire function (use dap or di{)',
'Find and change next "TODO" (use /TODO<CR>cgn)',
}
local challenge = challenges[math.random(#challenges)]
vim.notify('🎮 Challenge: ' .. challenge, vim.log.levels.INFO)
end
-- Show cheatsheet
function M.cheatsheet()
local cheatsheet = [[
=== STOP DOING === | === START DOING ===
jjjjjjjj | 10j, }, <C-d>
wwwww | 3w, f<char>
manually typing search | *, viw/, <leader>sg
:%s/old/new/g | *cgn then .
:q then git | :Git (Fugitive)
arrow keys | hjkl with counts
=== POWER MOVES ===
cgn - Change next occurrence (use with *)
. - Repeat last change
* - Search word under cursor
ciw - Change inside word
f/t - Find/till character
% - Jump to matching bracket
<C-d> - Half page down
<C-u> - Half page up
=== TRAINING COMMANDS ===
<leader>tt - Toggle training mode
<leader>ts - Show statistics
<leader>tg - Get random challenge
<leader>tm - Movement hints
<leader>ti - Text object info
<leader>* - Search word under cursor
<leader>R - Replace word globally
<leader>C - Start cgn workflow
]]
vim.notify(cheatsheet, vim.log.levels.INFO)
end
return M

View File

@ -0,0 +1,10 @@
-- Harpoon - Quick file navigation
return {
'ThePrimeagen/harpoon',
branch = 'harpoon2',
dependencies = { 'nvim-lua/plenary.nvim' },
event = 'VeryLazy',
config = function()
require('plugins.config.harpoon').setup()
end,
}

View File

@ -0,0 +1,19 @@
-- Neovim Training Wheels - Build better vim habits
-- Load nix settings if available
pcall(require, 'nix-settings')
return {
'dlond/training.nvim',
enabled = vim.g.training_mode_enabled or false, -- Controlled by Nix
lazy = false,
dir = vim.fn.stdpath('config') .. '/lua/plugins/training', -- Local "plugin"
config = function()
require('plugins.config.training').setup()
end,
keys = {
{ '<leader>tt', function() require('plugins.config.training').toggle() end, desc = '[T]raining [T]oggle' },
{ '<leader>ts', function() require('plugins.config.training').show_stats() end, desc = '[T]raining [S]tats' },
{ '<leader>tg', function() require('plugins.config.training').challenge() end, desc = '[T]raining [G]ame' },
{ '<leader>?', function() require('plugins.config.training').cheatsheet() end, desc = 'Show efficiency cheatsheet' },
},
}