278 lines
8.6 KiB
Lua
278 lines
8.6 KiB
Lua
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 |