diff --git a/lua/core/keymaps.lua b/lua/core/keymaps.lua index 495d79f0..a77f4763 100644 --- a/lua/core/keymaps.lua +++ b/lua/core/keymaps.lua @@ -34,5 +34,35 @@ end -- Reload LSP keybind vim.keymap.set('n', 'lr', reload_lsp, { desc = '[L]SP [R]eload all servers' }) +-- Buffer management keymaps +vim.keymap.set('n', 'bb', 'Telescope buffers', { desc = '[B]rowse [B]uffers' }) +vim.keymap.set('n', '[b', 'bprevious', { desc = 'Previous buffer' }) +vim.keymap.set('n', ']b', 'bnext', { desc = 'Next buffer' }) +vim.keymap.set('n', 'bd', 'bdelete', { desc = '[B]uffer [D]elete' }) +vim.keymap.set('n', 'ba', '%bd|e#', { desc = '[B]uffers close [A]ll but current' }) +vim.keymap.set('n', 'bn', 'enew', { desc = '[B]uffer [N]ew' }) + +-- Quick buffer switching with numbers +for i = 1, 9 do + vim.keymap.set('n', '' .. i, 'buffer ' .. i .. '', { desc = 'Switch to buffer ' .. i }) +end + +-- Alternate file (toggle between two most recent files) +vim.keymap.set('n', '', '', { desc = 'Toggle alternate file' }) + +-- Window management keymaps +vim.keymap.set('n', 'ws', 'split', { desc = '[W]indow [S]plit horizontal' }) +vim.keymap.set('n', 'wv', 'vsplit', { desc = '[W]indow [V]ertical split' }) +vim.keymap.set('n', 'wc', 'close', { desc = '[W]indow [C]lose' }) +vim.keymap.set('n', 'wo', 'only', { desc = '[W]indow [O]nly (close others)' }) +vim.keymap.set('n', 'ww', 'w', { desc = '[W]indow cycle' }) +vim.keymap.set('n', 'w=', '=', { desc = '[W]indow balance sizes' }) + +-- Window resizing with arrow keys +vim.keymap.set('n', '', 'resize +2', { desc = 'Increase window height' }) +vim.keymap.set('n', '', 'resize -2', { desc = 'Decrease window height' }) +vim.keymap.set('n', '', 'vertical resize -2', { desc = 'Decrease window width' }) +vim.keymap.set('n', '', 'vertical resize +2', { desc = 'Increase window width' }) + -- Standard practice for Lua modules that don't need to return complex data return {} diff --git a/lua/core/options.lua b/lua/core/options.lua index 169b8309..8325e768 100644 --- a/lua/core/options.lua +++ b/lua/core/options.lua @@ -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 diff --git a/lua/plugins/config/harpoon.lua b/lua/plugins/config/harpoon.lua new file mode 100644 index 00000000..1a7efb63 --- /dev/null +++ b/lua/plugins/config/harpoon.lua @@ -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', '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', '', 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', 'm1', function() + harpoon:list():select(1) + end, { desc = 'Jump to mark 1' }) + + vim.keymap.set('n', 'm2', function() + harpoon:list():select(2) + end, { desc = 'Jump to mark 2' }) + + vim.keymap.set('n', 'm3', function() + harpoon:list():select(3) + end, { desc = 'Jump to mark 3' }) + + vim.keymap.set('n', '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', '', function() + harpoon:list():select(1) + end, { desc = 'Harpoon file 1' }) + + vim.keymap.set('n', '', function() + harpoon:list():select(2) + end, { desc = 'Harpoon file 2' }) + + vim.keymap.set('n', '', function() + harpoon:list():select(3) + end, { desc = 'Harpoon file 3' }) + + vim.keymap.set('n', '', 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', 'mm', function() + toggle_telescope(harpoon:list()) + end, { desc = '[M]arked files in Telescope' }) + + -- Clear all marks + vim.keymap.set('n', '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', '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 \ No newline at end of file diff --git a/lua/plugins/config/training.lua b/lua/plugins/config/training.lua new file mode 100644 index 00000000..598e61be --- /dev/null +++ b/lua/plugins/config/training.lua @@ -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' }, '', '', { desc = 'Use k instead!' }) + vim.keymap.set({ 'n', 'v', 'i' }, '', '', { desc = 'Use j instead!' }) + vim.keymap.set({ 'n', 'v', 'i' }, '', '', { desc = 'Use h instead!' }) + vim.keymap.set({ 'n', 'v', 'i' }, '', '', { 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 { '', '', '}', '{', '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', '*', '*N', { desc = 'Search word under cursor' }) + + -- Search selected text + vim.keymap.set('v', '//', [[y/\V=escape(@",'/\')]], { desc = 'Search selection' }) + + -- Replace word under cursor (the smart way) + vim.keymap.set('n', 'R', function() + local word = vim.fn.expand '' + 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', 'C', '*Ncgn', { desc = 'Change word under cursor (cgn workflow)' }) +end + +-- Efficiency helpers +local function setup_efficiency_helpers() + -- Movement reminder + vim.keymap.set('n', '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 / F - Find forward/backward', + 't / T - 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', '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' }, '') + pcall(vim.keymap.del, { 'n', 'v', 'i' }, '') + pcall(vim.keymap.del, { 'n', 'v', 'i' }, '') + pcall(vim.keymap.del, { 'n', 'v', 'i' }, '') + + -- 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 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 /TODOcgn)', + } + + 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, }, +wwwww | 3w, f +manually typing search | *, viw/, 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 + - Half page down + - Half page up + +=== TRAINING COMMANDS === +tt - Toggle training mode +ts - Show statistics +tg - Get random challenge +tm - Movement hints +ti - Text object info +* - Search word under cursor +R - Replace word globally +C - Start cgn workflow +]] + + vim.notify(cheatsheet, vim.log.levels.INFO) +end + +return M \ No newline at end of file diff --git a/lua/plugins/spec/harpoon.lua b/lua/plugins/spec/harpoon.lua new file mode 100644 index 00000000..0e0871be --- /dev/null +++ b/lua/plugins/spec/harpoon.lua @@ -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, +} \ No newline at end of file diff --git a/lua/plugins/spec/training.lua b/lua/plugins/spec/training.lua new file mode 100644 index 00000000..13e4767f --- /dev/null +++ b/lua/plugins/spec/training.lua @@ -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 = { + { 'tt', function() require('plugins.config.training').toggle() end, desc = '[T]raining [T]oggle' }, + { 'ts', function() require('plugins.config.training').show_stats() end, desc = '[T]raining [S]tats' }, + { 'tg', function() require('plugins.config.training').challenge() end, desc = '[T]raining [G]ame' }, + { '?', function() require('plugins.config.training').cheatsheet() end, desc = 'Show efficiency cheatsheet' }, + }, +} \ No newline at end of file