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:
parent
9e6b01e176
commit
161310a072
|
@ -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 {}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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,
|
||||
}
|
|
@ -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' },
|
||||
},
|
||||
}
|
Loading…
Reference in New Issue